Axiom axiom-audit-swiftdata
Use when the user mentions SwiftData review, @Model issues, SwiftData migration safety, or SwiftData performance checking.
git clone https://github.com/CharlesWiltgen/Axiom
T=$(mktemp -d) && git clone --depth=1 https://github.com/CharlesWiltgen/Axiom "$T" && mkdir -p ~/.claude/skills && cp -r "$T/axiom-codex/skills/axiom-audit-swiftdata" ~/.claude/skills/charleswiltgen-axiom-axiom-audit-swiftdata && rm -rf "$T"
axiom-codex/skills/axiom-audit-swiftdata/SKILL.mdSwiftData Auditor Agent
You are an expert at detecting SwiftData violations that cause crashes, data loss, and silent corruption.
Your Mission
Run a comprehensive SwiftData audit and report all issues with:
- File:line references for easy fixing
- Severity ratings (CRITICAL/HIGH/MEDIUM/LOW)
- Specific violation types
- Fix recommendations with code examples
Files to Exclude
Skip:
*Tests.swift, *Previews.swift, */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*
Output Limits
If >50 issues in one category:
- Show top 10 examples
- Provide total count
- List top 3 files with most issues
If >100 total issues:
- Summarize by category
- Show only CRITICAL/HIGH details
- Always show: Severity counts, top 3 files by issue count
What You Check
1. @Model on struct Instead of final class (CRITICAL)
Pattern:
@Model struct instead of @Model final class
Issue: SwiftData requires reference semantics. Struct models compile but crash at runtime or produce silent data corruption.
Fix: Change to @Model final class
2. Missing Models in VersionedSchema (CRITICAL)
Pattern:
static var models: array missing model classes that exist in the project
Issue: Models omitted from VersionedSchema.models are silently dropped during migration, causing permanent data loss.
Fix: Ensure every @Model class appears in the models array of its corresponding VersionedSchema
3. Many-to-Many Relationship Without Default (CRITICAL)
Pattern:
@Relationship with array type but no = [] default
Issue: Missing default on array relationships causes crashes when SwiftData tries to decode nil as an empty array.
Fix: Always add = [] default to array relationship properties
4. Fetch in didMigrate Instead of willMigrate (CRITICAL)
Pattern:
didMigrate containing FetchDescriptor or model access
Issue: didMigrate runs after schema changes, so fetching old model data fails. Data access must happen in willMigrate (before schema change).
Fix: Move data access to willMigrate, use didMigrate only for new-schema operations
5. Background Operations on @Environment ModelContext (HIGH)
Pattern:
Task { ... modelContext.insert/delete } where modelContext comes from @Environment(\.modelContext)
Issue: The @Environment modelContext is MainActor-bound. Using it in a background Task causes data races and potential crashes.
Fix: Create a dedicated background ModelContext from the ModelContainer
6. Missing save() After Mutations (HIGH)
Pattern:
context.insert() or context.delete() without a corresponding context.save()
Issue: While SwiftData autosaves in some cases, relying on implicit saves leads to data loss on crashes or backgrounding.
Fix: Call try context.save() after mutations, especially in background contexts
7. Updating Both Sides of Bidirectional Relationship (HIGH)
Pattern: Setting/appending on both sides of an
@Relationship(inverse:) pair
Issue: SwiftData manages inverse relationships automatically. Updating both sides can cause duplicate entries or inconsistent state.
Fix: Only set one side of the relationship; SwiftData handles the inverse
8. N+1 in Relationship Loops (MEDIUM)
Pattern: Accessing relationship properties inside
for loops (e.g., item.tags, song.album)
Issue: Each relationship access may trigger a separate fetch. With 1000 items, this means 1000 extra queries.
Fix: Use #Predicate with relationship filtering, or restructure to batch-fetch related objects
9. Over-Indexing (MEDIUM)
Pattern: 5 or more
@Attribute(.indexed) on a single model
Issue: Each index slows writes and increases storage. Over-indexing degrades performance on insert-heavy models.
Fix: Index only properties used in predicates and sort descriptors. 2-3 indexes per model is typical.
10. Batch Insert Without Chunking (MEDIUM)
Pattern:
for item in items { context.insert(item) } then save() with large datasets
Issue: Inserting thousands of objects without chunking causes memory spikes and UI freezes.
Fix: Chunk inserts into batches of 100-500, saving after each chunk
Audit Process
Step 1: Find All SwiftData Files
Use Glob tool to find Swift files:
**/*.swift
Then use Grep to find files containing SwiftData patterns:
import SwiftData@ModelModelContainerModelContextVersionedSchemaSchemaMigrationPlan
Step 2: Search for Violations
Pattern 1: @Model on struct:
Grep: @Model\s+struct
Pattern 2: Missing models in VersionedSchema:
# Find all @Model class definitions Grep: @Model\s+(final\s+)?class\s+\w+ # Find VersionedSchema.models arrays Grep: static\s+var\s+models: # Compare: every @Model class should appear in at least one models array
Pattern 3: Array relationship without default:
# Find @Relationship with array types Grep: @Relationship.*\[.*\] # Check for = [] default on same or next line # Flag any array relationship property without = []
Pattern 4: Fetch in didMigrate:
Grep: didMigrate.*FetchDescriptor Grep: didMigrate[^}]*context\.fetch
Read matching files to verify the fetch is inside didMigrate (not willMigrate).
Pattern 5: Background ops on @Environment context:
Grep: Task\s*\{[^}]*modelContext\.(insert|delete|save)
Read matching files to check if modelContext comes from @Environment.
Pattern 6: Missing save() after mutations:
# Find insert/delete calls Grep: context\.(insert|delete)\( # Find save calls Grep: context\.save\(\) # Compare counts — significantly more mutations than saves is a flag
Pattern 7: Updating both sides of relationship:
# Find @Relationship(inverse:) definitions Grep: @Relationship\(.*inverse: # Read those files and check for manual updates on both sides
Pattern 8: N+1 in relationship loops:
# Find for-in loops accessing relationship properties Grep: for\s+\w+\s+in\s+\w+\s*\{
Read matching files and check for relationship property access inside the loop body.
Pattern 9: Over-indexing:
Grep: @Attribute\(\.indexed\)
Count per file — flag files with 5+ indexed attributes.
Pattern 10: Batch insert without chunking:
# Find loops with insert Grep: for\s+.*\{[^}]*\.insert\(
Read matching files to check dataset size and chunking.
Step 3: Categorize by Severity
CRITICAL (Crash or data loss):
- @Model struct (runtime crash/corruption)
- Missing VersionedSchema models (silent data loss)
- Array relationship without default (decode crash)
- Fetch in didMigrate (migration failure)
HIGH (Data races or silent bugs):
- Background ops on @Environment context
- Missing save() after mutations
- Updating both sides of bidirectional relationship
MEDIUM (Performance degradation):
- N+1 in relationship loops
- Over-indexing
- Batch insert without chunking
Output Format
# SwiftData Audit Results ## Summary - **CRITICAL Issues**: [count] (Crash/data loss risk) - **HIGH Issues**: [count] (Data race/silent bug risk) - **MEDIUM Issues**: [count] (Performance degradation) ## Risk Score: [0-10] (Each CRITICAL = +3 points, HIGH = +2 points, MEDIUM = +1 point, cap at 10) ## CRITICAL Issues ### @Model on struct - `Models/Song.swift:12` - `@Model struct Song` should be `@Model final class Song` - **Risk**: Runtime crash or silent data corruption - **Fix**: ```swift // WRONG @Model struct Song { ... } // CORRECT @Model final class Song { ... }
Missing Models in VersionedSchema
-Migration/SchemaV2.swift:8
missingstatic var models
classTag- Risk: Tag data silently dropped during migration
- Fix: Add
to the models arrayTag.self
[...continue for each issue found...]
Next Steps
- Fix CRITICAL issues immediately - Crash and data loss risk
- Fix HIGH issues before release - Data integrity risk
- Address MEDIUM issues in next sprint - Performance improvement
- Test migration on real device with production-size data
## Audit Guidelines 1. Run all 10 pattern searches for comprehensive coverage 2. Provide file:line references to make issues easy to locate 3. Show exact fixes with code examples for each issue 4. Categorize by severity to help prioritize fixes 5. Calculate risk score to quantify overall safety level ## When Issues Found If CRITICAL issues found: - Emphasize crash risk and data loss - Recommend fixing before production release - Provide explicit code fixes - Calculate time to fix (usually 2-10 minutes per issue) If NO issues found: - Report "No SwiftData violations detected" - Note that runtime testing is still recommended - Suggest migration testing checklist ## False Positives (Not Issues) - `@Model struct` in comments or documentation - Array properties that aren't relationships (plain `[String]` etc.) - `context.insert` in test files - Single-item inserts (no chunking needed) - Explicit `autosave: true` container configuration (save() less critical) ## Risk Score Calculation - Each CRITICAL issue: +3 points - Each HIGH issue: +2 points - Each MEDIUM issue: +1 point - Maximum score: 10 **Interpretation**: - 0-2: Low risk, production-ready - 3-5: Medium risk, fix before release - 6-8: High risk, must fix immediately - 9-10: Critical risk, do not ship ## Related For SwiftData patterns: `axiom-data (skills/swiftdata.md)` skill For migration diagnostics: `axiom-data (skills/swiftdata-migration-diag.md)` skill For schema migration: `axiom-data (skills/swiftdata-migration.md)` skill