NOASSERTIONupdated 1mo ago
Let be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke /scripts/swiftui-ctx; do not assume the command is on PATH.
What can you do with Audit Swiftui Swiftdata?
name: audit-swiftui-swiftdata description: Audit macOS SwiftUI swiftdata for correctness, current APIs, and production conventions. Use for that domain or as part of a full audit.
Bundled resource root
Let <swiftui-plugin-root> be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke <swiftui-plugin-root>/scripts/swiftui-ctx; do not assume the command is on PATH.
Audit SwiftUI SwiftData
AUDIT-ONLY Β· macOS-only Β· SwiftUI-only. Run this on a finished or in-progress macOS SwiftUI
project to detect β and where mechanical, fix β every way SwiftData goes wrong on a macOS 14+ target:
let on a relationship, a relationship assigned in init, a missing initializer, the positional
@Relationship(.cascade) type error, a non-optional to-one relationship, a container-crashing
preview, a fatalError on ModelContainer, off-actor @Model mutation, a missing save(), an
unordered relationship array, and an ungated macOS-26 @Model subclass. Findings are written to disk
in the toolkit's unified schema; the one mechanical defect (@Relationship(.cascade)) is fixed under
the fix-safety protocol. This is never a from-scratch data-model generator.
SwiftData is a thin macro faΓ§ade over Core Data: the Swift-language semantics an LLM reasons
about (let is immutable, a non-optional is non-optional, init assigns stored properties) are
silently violated by the Core Data machinery underneath, and almost none of the violations produce a
compiler diagnostic. The code compiles, looks idiomatic, then crashes at runtime, loses data on
relaunch, or kills the preview canvas. Apple's own samples make it worse β they show no @Model
initializer, ship a non-compiling @Relationship(.cascade), and recommend fatalError on the
container. Be suspicious wherever the compiler stayed silent.
Boundary / seam note (stay in lane)
- Core Data
NSManagedObject/NSPersistentContaineris out of scope. If audited code uses raw Core Data, note it in one line β do not audit Core Data here. - The concurrency isolation hazard itself (
@Modelis non-Sendable,@MainActorboundaries) belongs toaudit-swiftui-concurrency-safety, which flags the race; this skill prescribes the@ModelActorfix shape. On an off-context-mutation site, emit across_ref: concurrency-safety(percross-ref-graph.md) β concurrency owns the race, swiftdata owns the data-correct fix. - Preview-container construction mechanics (in-memory container, sample factory) are owned by
audit-swiftui-previews; this skill detects the model-design reason a preview crashes (sd-06) and routes preview-rig depth there with across_ref: previews. - Store location / group-container entitlement is owned by
audit-swiftui-sandbox-files; this skill flags the multi-process container smell (sd-12) and cross_refs it. - The blanket "is every OS-floored API gated" sweep belongs to
audit-swiftui-availability-gating; this skill owns the macOS-26@Model-inheritance gate (sd-11) in depth and defers other gating there.
The eight invariants (non-negotiable)
- Relationships are always
var, defaulted βleton a bidirectional@Relationshipcompiles clean, then crashes at runtime (an opaqueKeyPathβReferenceWritableKeyPathcast failure). - Never assign a relationship in
initβself.floors = floorsbypasses SwiftData's hooks, the child FK savesNULL, and the relationship is empty on relaunch.append(contentsOf:)is fine. - Every
@Modelneeds an explicitinit, and@Relationship(deleteRule:)is named β the positional@Relationship(.cascade)from Apple's docs is a compile-time type error. - To-one relationships are optional (
Person?) β a non-optional to-one is an implicitly-unwrapped trap: the FK is nullable, so a read while it isNULLis a nil-unwrap crash. - Previews need an in-memory container (
ModelConfiguration(isStoredInMemoryOnly: true)) with sample data inserted, or the canvas crashes ("failed to find a currently active container"). - Never
fatalErroronModelContainercreation β itsinitthrows for recoverable reasons (schema mismatch, no disk, concurrent migration); classify and recover. - Mutate
@Modeloff-main only inside a@ModelActor; hand offPersistentIdentifier(Sendable), never the non-Sendable@Model. - Call
try modelContext.save()explicitly β auto-save is tens of seconds and a fast Quit / window close drops it. Order reads with@Query(sort:); relationship-array order is not persisted.
Full βββ
for each: the routed references/*.md below.
Defect index (sd-01 β¦ sd-12)
id Β· tell Β· severity Β· fix Β· open reference. Severities: hard-fail (build break or runtime
crash / data loss β never correct), warning (compiles but wrong), advisory (judgment / perf).
auto = mechanical single-answer fix; flag = show the β
, dev applies.
| id | One-line tell | Sev | Fix | Reference |
|---|---|---|---|---|
| sd-01 | let on a bidirectional @Relationship property (runtime cast crash) |
hard-fail | flag | model-shape-and-relationships.md |
| sd-02 | a relationship assigned in init (self.x = y) β child FK saved NULL, empty on relaunch |
hard-fail | flag | model-shape-and-relationships.md |
| sd-03 | @Model class with stored properties but no init( (Apple's incomplete sample) |
warning | flag | model-shape-and-relationships.md |
| sd-04 | @Relationship(.cascade) positional (.cascade is a DeleteRule, slot wants .Option) β type error |
hard-fail | auto | model-shape-and-relationships.md |
| sd-05 | non-optional to-one @Model relationship (var owner: Person) β implicitly-unwrapped nil-crash |
warning | flag | model-shape-and-relationships.md |
| sd-06 | #Preview constructs a @Model with no in-memory ModelContainer β canvas crash |
warning | flag | container-and-preview.md |
| sd-07 | fatalError (or try!) on ModelContainer creation outside a preview β recoverable error crashes blind |
warning | flag | container-and-preview.md |
| sd-08 | indexing a relationship array (.floors[0]) / ForEach over a relationship with no @Query(sort:) |
warning | flag | query-and-persistence.md |
| sd-09 | off-actor @Model mutation in Task/Task.detached/DispatchQueue with no @ModelActor |
hard-fail | flag | concurrency-and-saving.md |
| sd-10 | a mutation path with no try modelContext.save() (silent loss on Quit / window close) |
advisory | flag | concurrency-and-saving.md |
| sd-11 | a @Model subclass ungated / its types not all registered (macOS-26 inheritance) |
warning | flag | query-and-persistence.md |
| sd-12 | one container opened by app + widget/menu-bar helper with no lock-file serialization | advisory | flag | container-and-preview.md |
Two claims are corpus-thin β carry with care. @ModelActor and the off-context race (sd-09) are
real but sparse in the practice corpus (swiftui-ctx lookup ModelActor returns not-found β that is
low_corpus, not a hallucination; the symbol is macOS 14.0+ per floors-master.md). Lean on
Sosumi for sd-09. The auto-save-window dropping a fast-Quit save (sd-10) is observed practitioner
behavior, not a documented guarantee β carry sd-10 as advisory with source: verify against Xcode 26 SDK unless Sosumi confirms a save() requirement.
The real API, at a glance
Real (exist on macOS 14.0+): @Model, ModelContext, ModelConfiguration(isStoredInMemoryOnly:),
@Relationship(deleteRule:inverse:), @Attribute, @Query, @Query(sort:), .modelContainer(for:),
@ModelActor (macro: converts a Swift actor to conform to protocol ModelActor, giving it its own ModelContext), PersistentIdentifier (the Sendable
hand-off; macOS 13.0+). macOS 15.0+: the variadic ModelContainer(for:configurations:) (on a macOS-14
target use ModelContainer(for:migrationPlan:configurations:) with migrationPlan: nil), #Index,
#Unique, the history API (HistoryDescriptor, fetchHistory(_:)). macOS 26.0+: @Model class
inheritance (every subclass needs @available(macOS 26, *); register base + every subclass in the
container) and HistoryDescriptor.sortBy.
The type error (compiles never): @Relationship(.cascade) β .cascade is a
Schema.Relationship.DeleteRule, the first variadic slot is a Schema.Relationship.Option (only
.unique). Fix: the named @Relationship(deleteRule: .cascade).
Floor values are the reconciled truth in
<swiftui-plugin-root>/references/_shared/floors-master.md β read, never restate. The canonical
invented-name list is <swiftui-plugin-root>/references/_shared/hallucination-blacklist.md. Signatures
and the full βββ
rewrites: the routed references/*.md.
β Correct β the container shape, grounded in real shipping code
The corpus consensus for ModelContainer construction is (for, configurations) at 64%
(swiftui-ctx lookup ModelContainer; next at 9% is bare (for)). The canonical real example
(author-authority 9558, 218β
) is fayazara/bucketdrop:
// https://github.com/fayazara/bucketdrop/blob/92816bedcd2267022ede0c797d12e593f0997e4b/BucketDrop/BucketDropApp.swift#L29
let schema = Schema([UploadedFile.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
// β shipping code here writes `fatalError(...)` β that is exactly sd-07.
// β
classify and recover: a schema mismatch / no-disk / concurrent-migration error is recoverable.
throw error
}
The construction shape try ModelContainer(for: schema, configurations: [config]) is the grounded
β
; the same real file's catch proves sd-07 in the wild (it fatalErrors a recoverable throw).
Source of record: the permalink above + the Sosumi doc doc: link
https://sosumi.ai/documentation/swiftdata/modelcontainer (the variadic (for:configurations:)
overload is macOS 15.0+ per floors-master.md; on a macOS-14 floor use the
(for:migrationPlan:configurations:) overload with migrationPlan: nil).
The 8-step audit workflow (execute verbatim)
- ORIENT.
tree/findthe SwiftUI sources. Read the deployment target (project.pbxprojMACOSX_DEPLOYMENT_TARGET, orPackage.swiftplatforms:). It is load-bearing: sd-11 fires only when the floor includes macOS 26 and a subclass is ungated; the variadicModelContainer(for:configurations:)init needs macOS 15 (note the 14 alternative). Record it. - LOCATE. Run the shared hybrid lint runner:
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-swiftdata --dir <sources> --json /tmp/sd.json --sarif /tmp/sd.sarif. It runs this skill's tier-1 grep tells (lint/grep-tells.tsv) + tier-2 structural ast-grep rules (lint/ast-grep/*.ymlβ thelet-on-relationship, relationship-assigned-in-init, and@Model-subclass rules grep can't express), a per-file parse probe, and emits unified JSON + SARIF. Read itsparse_warningsβ a flagged file did not fully parse, so a structural miss can't masquerade as clean; READ those by hand. The runner only LOCATES β never treat a hit as a finding. Engine + rule-file format + degradation:<swiftui-plugin-root>/references/_shared/lint-architecture.md. - READ. Open every located file in full β never pattern-match-and-patch blind. Whether a
relationship is bidirectional, whether an
initexists, whether aself.x =assigns a relationship vs a value property, and whether aTaskactually mutates a main-context object are all invisible to grep. Build a per-@Modelinventory: each property's kind (value / to-one / to-many relationship), theinit, the container site, the actor isolation, the save sites. - DETECT. Apply the index. Assign each candidate a confidence; report a finding only at 100%
certainty (e.g. a
leton a@Relationship, a positional@Relationship(.cascade), afatalErroronModelContainerin shipping code). - VERIFY. For anything β€ ~70% confidence (a symbol/floor you can't place, a behavior claim), run
both evidence sources. (a) Practice β
bash <swiftui-plugin-root>/scripts/swiftui-ctx lookup <api> --json(andswiftui-ctx deprecated <api>for a currency rule): read itsconsensus(the canonical shape β e.g.ModelContainerconsensus is(for, configurations)at 64%),recommendedpermalink +min_macos,introduced_macos,co_occurs_with, andlow_corpus. Alookupexit 3 (not-found, with a did-you-meansuggestion) corroborates a hallucination β but for a known-sparse symbol (ModelActor) treat not-found aslow_corpus, not a hallucination, and lean on Sosumi. (b) Spec β confirm via Sosumi:curl -sSL https://sosumi.ai/<apple-path>usingreferences/source-directory.mdfor the path and<swiftui-plugin-root>/references/_shared/sosumi-reference.mdfor the protocol (neverWebFetchdeveloper.apple.com). Cross-checkintroduced_macosagainstfloors-master.mdand the Sosumidoc:floor. The CLI contract is<swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md. Promote with the citation or discard. Carry sd-10 (and any unprovable behavior claim) asadvisorywithsource: verify against Xcode 26 SDK. - REPORT. Write each confirmed finding (output contract below). One finding per file, zero-padded,
ordered. Emit a
cross_refon every shared-seam site (sd-09 βconcurrency-safety; sd-06 βpreviews; sd-12 βsandbox-files). Write the run's_index.md. - FIX. Apply corrections under the fix-safety protocol
(
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md): clean-tree gate, findings-first, onlyfix_mode: auto(sd-04@Relationship(.cascade)β@Relationship(deleteRule: .cascade)), one conventional commit per finding citing itsrule_id, never weaken a check. The β "Correct" is not a hand-written snippet β it is the swiftui-ctx consensus shape put in## Correct, backed by a real macOS-era example fetched withbash <swiftui-plugin-root>/scripts/swiftui-ctx file <recommended.id> --smartwhose GitHub permalink (plus the Sosumidoc:) goes in## Sourceas the canonical example. Leaveflag-onlyfindingsopenwith that β in## Correct. - DOUBLE-CHECK. Re-grep each fixed file to confirm the tell no longer matches; record the evidence
in
## Fix applied?. Re-confirm every citation still resolves and still says its floor. If a fix introduced a new tell (e.g. avaryou added to a relationship now needs aninitthatappends, not assigns), loop that file back to DETECT.
Confidence gating (load-bearing)
Report a finding only at 100% certainty. Anything β€ ~70% goes to VERIFY (step 5) before it can
become a finding β never emit a speculative finding. The SwiftData trap is that the compiler is
silent, so the LLM is the only analyst that can tell a relationship from a value property and an
append from an assignment: READ before you report. Auto-fix only the one mechanical defect (sd-04);
everything else is fix_mode: flag-only.
Output contract
Inherits the toolkit's unified contract (full schema + body sections + frontmatter keys:
<swiftui-plugin-root>/references/_shared/finding-schema.md β do not restate it). Specialized for this
domain:
- Findings:
swiftui-audits/swiftdata/<context>/NN-slug.md(one finding per file, zero-padded, ordered). Per-run index:swiftui-audits/swiftdata/_index.md. domain: swiftdata. Frontmatter is the canonical schema;fix_modeisautofor sd-04 only, elseflag-only.availabilityreads fromfloors-master.md.sourceis an Apple URL + access date (fetched via Sosumi) orverify against Xcode 26 SDK. Emitcross_refper the seam notes above.
Starter <context> folders (file here whenβ¦):
<context> |
File a finding here when⦠|
|---|---|
relationship-mutability/ |
a relationship is let, or assigned in init (sd-01, sd-02) |
model-completeness/ |
a @Model lacks an init, or @Relationship(.cascade) is positional (sd-03, sd-04) |
optionality-traps/ |
a to-one relationship is non-optional (sd-05) |
container-lifecycle/ |
a preview lacks an in-memory container, a container fatalErrors, or a multi-process container is unserialized (sd-06, sd-07, sd-12) |
ordering-and-query/ |
a relationship array is indexed / iterated unordered (sd-08) |
concurrency-and-saving/ |
a @Model is mutated off-actor, or a mutation path omits save() (sd-09, sd-10) |
availability-gating/ |
a @Model subclass is ungated or its types unregistered on a macOS-26 floor (sd-11) |
New-folder rule: if a finding does not fit any existing context folder, create a new one under
swiftui-audits/swiftdata/ with a lowercase-hyphen slug naming the sub-category, and note it in the
run's _index.md. Prefer an existing folder when the fit is reasonable; consistency across runs is a
hard requirement. Two runs over the same code produce structurally identical trees.
Reference routing
| File | Open when |
|---|---|
references/model-shape-and-relationships.md |
a @Model definition question β let-vs-var, init-assignment, missing init, positional @Relationship, to-one optionality (sd-01/02/03/04/05) |
references/container-and-preview.md |
ModelContainer creation, the fatalError trap, preview in-memory containers, multi-process serialization (sd-06/07/12) |
references/query-and-persistence.md |
@Query ordering, relationship-array order, and macOS-26 @Model-inheritance gating + registration (sd-08/11) |
references/concurrency-and-saving.md |
off-actor mutation, the @ModelActor fix shape, PersistentIdentifier hand-off, explicit save() and ScenePhase/window-close timing (sd-09/10) |
references/source-directory.md |
step VERIFY β the Apple/WWDC/practitioner source map fetched via Sosumi |
lint/grep-tells.tsv + lint/ast-grep/*.yml |
step LOCATE β this skill's declarative lint rule set fed to the shared runner (tier-1 grep tells + tier-2 structural ast-grep); edit here to tune detection |
Shared toolkit references (point in, never restate):
| Shared file | For |
|---|---|
<swiftui-plugin-root>/references/_shared/floors-master.md |
every floor/availability value (the reconciled truth) |
<swiftui-plugin-root>/references/_shared/hallucination-blacklist.md |
the canonical invented-name list |
<swiftui-plugin-root>/references/_shared/macos-arm-gating.md |
the macOS-arm gating rule (sd-11 subclass gate) |
<swiftui-plugin-root>/references/_shared/finding-schema.md |
the unified finding schema + frontmatter keys |
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md |
the 8-point fix-safety protocol (step 7) |
<swiftui-plugin-root>/references/_shared/sosumi-reference.md |
the Apple-doc spec fetch protocol (step 5 VERIFY) |
<swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md |
the practice-corpus CLI contract β lookup/deprecated/file --smart for the consensus shape + permalinked example (steps 5 VERIFY Β· 7 FIX) |
<swiftui-plugin-root>/references/_shared/cross-ref-graph.md |
seam ownership + cross_ref targets (concurrency-safety Β· previews Β· sandbox-files) |
Detection accelerator
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-swiftdata --dir <files-or-dir> [--json out.json] [--sarif out.sarif] β the toolkit's one shared hybrid lint engine, fed this
skill's declarative rules: tier-1 grep tells (lint/grep-tells.tsv,
sd-03/04/05/06/07/08/09/10/12 + a flat let-near-Relationship net) + tier-2 ast-grep structural
rules (lint/ast-grep/*.yml β sd-01 let-on-@Relationship across the attribute line, sd-02
relationship-assigned-in-init scope, sd-11 @Model-subclass inheritance) that grep cannot express.
It runs a per-file parse probe (surfaces "did not fully parse" so a structural miss can't look
clean), emits unified JSON + SARIF, exits 2 on any hard-fail (sd-04) for a CI gate,
and degrades to grep-only with a notice if ast-grep is unreachable (npx --package @ast-grep/cli ast-grep; faster: brew install ast-grep). It only LOCATES β always READ each hit in full before
reporting (step 3). The thin scripts/sd-lint.sh is a pointer to this runner. Engine + rule-file
format + JSON/SARIF shape + safety rails:
<swiftui-plugin-root>/references/_shared/lint-architecture.md.
Install
Add Audit Swiftui Swiftdata to your client. Pick the one you use.
npx skills add yigitkonur/plugin-swiftuiInstalls every skill in the repository, then prompts for which to keep.
/plugin marketplace add yigitkonur/plugin-swiftuiAdds the repository as a plugin marketplace; install individual plugins with `/plugin install`.
git clone https://github.com/yigitkonur/plugin-swiftui
cp -r plugins/swiftui/skills/audit-swiftui-swiftdata ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
80 / 100
Excellent