Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
287
.claude/skills/axiom-audit-swiftdata/SKILL.md
Normal file
287
.claude/skills/axiom-audit-swiftdata/SKILL.md
Normal file
@@ -0,0 +1,287 @@
|
||||
---
|
||||
name: axiom-audit-swiftdata
|
||||
description: Use when the user mentions SwiftData review, @Model issues, SwiftData migration safety, or SwiftData performance checking.
|
||||
license: MIT
|
||||
disable-model-invocation: true
|
||||
---
|
||||
# SwiftData 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`
|
||||
- `@Model`
|
||||
- `ModelContainer`
|
||||
- `ModelContext`
|
||||
- `VersionedSchema`
|
||||
- `SchemaMigrationPlan`
|
||||
|
||||
### 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
|
||||
|
||||
```markdown
|
||||
# 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` - `static var models` missing `Tag` class
|
||||
- **Risk**: Tag data silently dropped during migration
|
||||
- **Fix**: Add `Tag.self` to the models array
|
||||
|
||||
[...continue for each issue found...]
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Fix CRITICAL issues immediately** - Crash and data loss risk
|
||||
2. **Fix HIGH issues before release** - Data integrity risk
|
||||
3. **Address MEDIUM issues in next sprint** - Performance improvement
|
||||
4. **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-swiftdata` skill
|
||||
For migration diagnostics: `axiom-swiftdata-migration-diag` skill
|
||||
For schema migration: `axiom-swiftdata-migration` skill
|
||||
Reference in New Issue
Block a user