chore(axiom-codex): rebuild Codex plugin variant for v27.1.1

This commit is contained in:
Charles Wiltgen
2026-09-19 10:56:35 -07:00
parent aeae3a903a
commit dda05d9e4c
7 changed files with 47 additions and 21 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "axiom",
"version": "27.1.0",
"version": "27.1.1",
"description": "Battle-tested skills for modern iOS development — SwiftUI, concurrency, data, performance, networking, accessibility, and more.",
"author": {
"name": "Charles Wiltgen",
+5 -5
View File
@@ -56,20 +56,20 @@ Skip: `*/Pods/*`, `*/Carthage/*`, `*/.build/*`, `*/DerivedData/*`, `*/scratch/*`
### Pattern 2: `@MainActor` Missing on UI Tests (CRITICAL)
**Issue**: Swift 6 requires explicit actor isolation
**Why flaky**: In Swift 6 language mode this is a compile error, not a flake — the compiler refuses the call. Constructing the `@MainActor` type off-actor is fine; calling an isolated member from a non-isolated test is what fails, and it only becomes a runtime data race in a project still on `-swift-version 5`.
**Why it fails**: Calling a main actor-isolated member from a non-isolated test is a compile error in every language mode — Swift 5 (minimal or complete checking) and Swift 6 alike — so it breaks the build rather than flaking. Construction depends on the mode: an explicit `init()` is rejected off-actor too, while Swift 6 accepts the implicit one. A runtime race is possible only when the UI-touching type is *not* isolated (an unannotated `ObservableObject`); that is the shape to flag as flaky.
**Detection**: Tests accessing UI types without @MainActor
```swift
// FLAKY - Main actor-isolated ViewModel used from a non-isolated test
// BUILD FAILURE - Main actor-isolated ViewModel used from a non-isolated test
@Test func viewModelUpdates() async {
let vm = ContentViewModel() // Constructing a @MainActor type off-actor is fine
let vm = ContentViewModel() // implicit init: accepted in Swift 6; an explicit init() is rejected here too
vm.load() // ERROR: main actor-isolated instance method 'load()' cannot be called from outside of the actor
}
// CORRECT - Proper isolation
@Test @MainActor func viewModelUpdates() async {
let vm = ContentViewModel()
await vm.load()
vm.load()
}
```
@@ -307,7 +307,7 @@ For each match:
vm.load() // ERROR: main actor-isolated instance method cannot be called from outside of the actor
}
```
- **Root cause**: Calling a @MainActor type from a non-isolated test — a compile error in Swift 6, a data race under `-swift-version 5`
- **Root cause**: Calling a @MainActor type from a non-isolated test — a compile error in every language mode
- **Fix**: Add `@MainActor` to test function
## HIGH Issues
+4 -3
View File
@@ -79,11 +79,11 @@ Patterns 1, 2, 3, 6, and 7 are unaffected — the compiler has nothing to say ab
### 1. Missing @MainActor on UI Classes (CRITICAL/HIGH)
**Pattern**: UIViewController, UIView, ObservableObject without @MainActor
**Search**: `class.*UIViewController`, `class.*ObservableObject` — check 5 lines before for @MainActor
**Pattern**: ObservableObject (and other UI-state classes) without @MainActor
**Search**: `class.*ObservableObject` — check 5 lines before for @MainActor
**Issue**: Crashes when UI modified from background threads
**Fix**: Add `@MainActor` to class declaration
**Note**: SwiftUI Views are implicitly @MainActor — not an issue
**Note**: `UIViewController` and `UIView` subclasses are NOT findings — both classes are `NS_SWIFT_UI_ACTOR`, so every subclass inherits `@MainActor` without an annotation. SwiftUI Views are implicitly @MainActor too. Skip this pattern entirely when the target sets `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`: every unannotated class is already @MainActor there.
**Field signal**: Crashes with xcsym `pattern_tag=swift_concurrency_violation` (fires on `_swift_task_isCurrentExecutor` in the exception subtype) almost always trace back to this anti-pattern. If the user has `.ips` artifacts, run `xcsym crash --format=summary <file>` and correlate the crashed frames with grep hits.
### 2. Unsafe Task Self Capture (HIGH/HIGH)
@@ -242,6 +242,7 @@ If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
- Async functions with minimal computation (a single network call, a short string format) — don't flag for missing @concurrent
- @MainActor classes accessing their own properties
- SwiftUI Views (implicitly @MainActor)
- `UIViewController` / `UIView` subclasses without `@MainActor` (inherited from UIKit's `NS_SWIFT_UI_ACTOR`)
- Task captures where self is a struct (value type)
- `@unchecked Sendable` with clear migration comment (downgrade to LOW)
- GCD usage in legacy modules marked for future migration
+7 -7
View File
@@ -74,8 +74,8 @@ Run all 10 detection patterns. For every grep match, use Read to verify the surr
### Pattern 1: ADD COLUMN NOT NULL Without DEFAULT (CRITICAL/HIGH)
**Issue**: SQLite requires DEFAULT for NOT NULL columns added to existing tables. Without it, the migration crashes for any table with existing rows.
**Search**: `ADD\s+COLUMN.*NOT\s+NULL`
**Verify**: Read matching files; check for `DEFAULT` on the same statement.
**Search**: `ADD\s+COLUMN.*NOT\s+NULL`, `add\(column:.*\.notNull\(`
**Verify**: Read matching files; check for `DEFAULT` on the same statement (GRDB: `.defaults(to:)` or `.defaults(sql:)` on the same column).
**Fix**: `ADD COLUMN name TEXT NOT NULL DEFAULT ''`
### Pattern 2: DROP TABLE on User Data (CRITICAL/HIGH)
@@ -94,7 +94,7 @@ Run all 10 detection patterns. For every grep match, use Read to verify the surr
### Pattern 4: ALTER TABLE Without Idempotency Check (CRITICAL/HIGH)
**Issue**: `ADD COLUMN` on a column that already exists fails with "duplicate column name". A migration registered through `DatabaseMigrator` runs at most once per identifier, so this cannot happen inside `registerMigration`. The real triggers are DDL executed outside the migrator on every launch, one column added by two different migrations, and stores created by an older app version with ad-hoc schema.
**Search**: `ADD\s+COLUMN`, `addColumn`
**Search**: `ADD\s+COLUMN`, `addColumn`, `add\(column:`
**Verify**: Read matching files; check for an existence guard (`db.columns(in:)`, `PRAGMA table_info`) or a do-catch. DDL inside `registerMigration` needs no guard.
**Fix**: Guard on introspection — `let exists = try db.columns(in: "users").contains { $0.name == "email" }`, then alter only when `exists` is false. `PRAGMA table_info` or a do-catch around the ALTER also works. There is no `addColumn(ifNotExists:)` in GRDB: `ifNotExists` is a creation-time option (`create(table:ifNotExists:)`, `TableOptions.ifNotExists`), and SQLite's ADD COLUMN has no such clause.
@@ -107,10 +107,10 @@ Run all 10 detection patterns. For every grep match, use Read to verify the surr
### Pattern 6: Foreign Key Added to an Existing Table Without an Orphan Check (HIGH/MEDIUM)
**Issue**: Foreign keys are creation-time in both SQLite and GRDB — `foreignKey(_:references:columns:onDelete:onUpdate:deferred:)` on the table definition; a `TableAlteration` can only add, rename, or drop columns. Adding one to an existing table means recreating it, and orphaned child rows make that recreation fail: GRDB's default deferred checks run `checkForeignKeys()` before the migration commits.
**Search**: `FOREIGN\s+KEY`, `REFERENCES`declared inside `CREATE TABLE`, never added by an ALTER
**Verify**: Read matching files; where the constraint is new on an existing table, check for orphan cleanup or a `PRAGMA foreign_key_check` before the recreation. There is no `addForeignKey` API.
**Fix**: Clean up orphans first, or run `PRAGMA foreign_key_check` to validate before recreating the table.
**Issue**: A foreign key on an *existing* column is creation-time in both SQLite and GRDB — `foreignKey(_:references:columns:onDelete:onUpdate:deferred:)` on the table definition, and there is no `addForeignKey` API — so adding one means recreating the table, and orphaned child rows make that recreation fail: GRDB's default deferred checks run `checkForeignKeys()` before the migration commits. A *new* column can carry a foreign key without a rebuild: `ALTER TABLE … ADD COLUMN … REFERENCES` (GRDB: `t.add(column:).references(...)`). With foreign keys on, SQLite requires that column to default to NULL, so it starts with no orphans.
**Search**: `FOREIGN\s+KEY`, `REFERENCES`in `CREATE TABLE`, on an `ADD COLUMN`, or GRDB `.references(`
**Verify**: Read matching files; where the constraint is new on an existing column, check for orphan cleanup or a `PRAGMA foreign_key_check` before the recreation. A new `ADD COLUMN … REFERENCES` column needs neither.
**Fix**: Clean up orphans first, or run `PRAGMA foreign_key_check` to validate before recreating the table. If the relationship can live on a new column, add it with `ADD COLUMN … REFERENCES` instead of rebuilding.
### Pattern 7: Foreign Key Enforcement Disabled (HIGH/HIGH)
+14 -2
View File
@@ -126,7 +126,18 @@ func migration00X_ChangeColumnType() throws {
### Adding Foreign Key Constraint
SQLite's `ALTER TABLE` has no `ADD CONSTRAINT`, so there are two routes: an indexed column whose relationship the app enforces, or a table rebuild that produces a declared constraint.
SQLite's `ALTER TABLE` has no `ADD CONSTRAINT`, so a constraint can't be attached to a column that already exists.
**When the relationship lives in a new column, add it with its constraint.** `ADD COLUMN … REFERENCES` declares a foreign key SQLite enforces like any other, with no rebuild. With foreign keys on, SQLite requires the new column to default to NULL, so it starts with no orphans; fill it with an `UPDATE`, which the constraint then checks. Rows with no match keep `NULL`.
```sql
ALTER TABLE tracks ADD COLUMN album_id TEXT REFERENCES albums(id) ON DELETE CASCADE;
UPDATE tracks SET album_id = (SELECT id FROM albums WHERE albums.title = tracks.album_name);
```
In GRDB: `t.add(column: "album_id", .text).references("albums", onDelete: .cascade)` inside `db.alter(table:)`.
When the column must be `NOT NULL`, or the constraint belongs on a column that already exists, there are two routes: an indexed column whose relationship the app enforces, or a table rebuild that produces a declared constraint.
#### Route 1 — indexed column, application-level relationship
@@ -366,8 +377,9 @@ What are you trying to do?
├─ Rename column?
│ └─ Add new column → Migrate data → Deprecate old → Done
├─ Add foreign key?
│ ├─ New nullable column? → ADD COLUMN … REFERENCES → Populate with UPDATE → Done
│ ├─ App-level relationship? → Add column → Populate data → Add index → Done
│ └─ Declared constraint? → Rebuild the table (create new → copy → drop old → rename) → Done
│ └─ Declared constraint on an existing or NOT NULL column? → Rebuild the table (create new → copy → drop old → rename) → Done
└─ Complex refactor?
└─ Break into multiple migrations → Test each step → Done
```
+2 -2
View File
@@ -1298,9 +1298,9 @@ final class TrackTests: XCTestCase {
## tvOS
**No local file on tvOS is *guaranteed* to persist, but SwiftData does keep a local store there.** `Documents` and `Caches` exist from the first launch, and `Application Support` is a real, separate directory — SwiftData does not create it for you, and it is not aliased to `Caches`. What differs from iOS is where a default store lands: on tvOS a container's default URL is `Library/Caches/default.store`, the one directory the system may purge, where iOS uses `Library/Application Support/default.store`.
**No local file on tvOS is guaranteed to persist — `Documents`, `Application Support`, and `Caches` can all be deleted while your app isn't running.** SwiftData still keeps a local store there. `Documents` and `Caches` exist from the first launch, and `Application Support` is a real, separate directory — SwiftData does not create it for you, and it is not aliased to `Caches`. What differs from iOS is where a default store lands: on tvOS a container's default URL is `Library/Caches/default.store`, where iOS uses `Library/Application Support/default.store`.
Pass an explicit store URL under Application Support when you need durability, and treat CloudKit sync (`cloudKitDatabase: .private(...)`) as the belt-and-braces option rather than a requirement. See axiom-swift (skills/tvos.md) for full tvOS storage constraints.
Pointing the store at Application Support does not make it durable. Make iCloud the source of truth — CloudKit sync (`cloudKitDatabase: .private(...)`) — and treat the local store as a cache the app can rebuild from it. See axiom-swift (skills/tvos.md) for full tvOS storage constraints.
---
+14 -1
View File
@@ -165,14 +165,27 @@ Button("To Top", systemImage: "chevron.up") { scrollToTop() }
A `Shape` whose corners resolve concentric to the container shape's corners — sharing a center with the container's corner radius — instead of using a hardcoded value. System containers (sheets, glass containers, widgets) provide the container shape automatically; give a custom container one with `.containerShape(_:)`. When a corner sits far from the container's corner the resolved radius can be zero (square corner) — pass `.concentric(minimum:)` to guarantee a floor. If the container shape isn't a `RoundedRectangularShape`, the result is an inset version of the container shape.
The device's screen is a container too: a view that extends to the display's rounded corners (under `.ignoresSafeArea()`) resolves its corners concentric to the hardware's, and on a device with square corners they resolve to zero. So never look up the screen's corner radius — not through a private screen property, not from a per-model table; let the shape resolve it.
```swift
// Inside a sheet/glass container/widget the container shape is provided;
// on a custom container, set .containerShape(.rect(cornerRadius: 32)) on the container.
CardContent()
.padding(12)
.background(ConcentricRectangle(corners: .concentric(minimum: .fixed(8))).fill(.background))
// A custom bottom sheet, shaped like the Notes Format sheet: fixed top corners,
// bottom corners concentric with the device's
SheetContent()
.background(
ConcentricRectangle(uniformTopCorners: .fixed(24), uniformBottomCorners: .concentric)
.fill(.background)
.ignoresSafeArea()
)
```
Give a bottom-attached sheet concentric bottom corners, not zero. Flush with the screen edge they resolve to the display's own radius, so the sheet reads as continuing below the glass; once it's inset (a floating detent, or any padding) they keep the display's center, where a zero radius would show a square corner inside the rounded screen.
On iPhone Duo, concentricity follows each display's corner shape — `ConcentricRectangle` (UIKit: `UICornerConfiguration`) fits both displays without per-device radii. See skills/iphone-duo.md (Match the new corners and support landscape).
| API | Notes |
@@ -184,7 +197,7 @@ On iPhone Duo, concentricity follows each display's corner shape — `Concentric
| `Edge.Corner.Style` | `.fixed(_:)`, `.concentric`, `.concentric(minimum:)`; expressible by int/float literal (`corners: 12`); animatable |
| `RoundedRectangularShape` | Protocol for containers whose corners resolve concentrically (`Capsule` conforms); `corners(in:)``RoundedRectangularShapeCorners?` |
| `GeometryProxy.containerCornerInsets` | Insets of the container's corners, for manual layout near corners |
| `GeometryProxy.concentricCornerRadii` / `concentricCornerRadii(in:)` `OS27` | Read back the resolved concentric radii for a frame |
| `GeometryProxy.concentricCornerRadii` / `concentricCornerRadii(in:)` `OS27` | Read back the resolved concentric radii for a frame without drawing a shape — for custom drawing (`Canvas`), animations, or a surface a `Shape` can't express (the result is optional) |
There is no `.containerConcentric` corner style. Use `RoundedRectangle`/`Capsule` when the radius must not track the container; `ConcentricRectangle` supersedes `ContainerRelativeShape` (iOS 14, rounded-rect only) for concentric nesting.