engsys 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/core/agents/aaron.md +152 -0
  4. package/core/agents/bert.md +115 -0
  5. package/core/agents/isabelle.md +136 -0
  6. package/core/agents/jody.md +150 -0
  7. package/core/agents/leith.md +111 -0
  8. package/core/agents/marcelo.md +282 -0
  9. package/core/agents/melvin.md +101 -0
  10. package/core/agents/nyx.md +152 -0
  11. package/core/agents/otto.md +168 -0
  12. package/core/agents/patricia.md +283 -0
  13. package/core/commands/design-audit-local.md +155 -0
  14. package/core/commands/design-audit.md +235 -0
  15. package/core/commands/design-critique.md +96 -0
  16. package/core/commands/file-issue.md +22 -0
  17. package/core/commands/generate-project.md +45 -0
  18. package/core/commands/implement-issue.md +37 -0
  19. package/core/commands/implement-project.md +40 -0
  20. package/core/commands/naturalize.md +61 -0
  21. package/core/commands/pre-push.md +29 -0
  22. package/core/commands/prep-review-collect.md +130 -0
  23. package/core/commands/prep-review-finalize.md +121 -0
  24. package/core/commands/prep-review-publish.md +113 -0
  25. package/core/commands/prep-review.md +65 -0
  26. package/core/commands/project-closeout.md +25 -0
  27. package/core/skills/agentic-eval/SKILL.md +195 -0
  28. package/core/skills/chrome-devtools/SKILL.md +97 -0
  29. package/core/skills/code-review/SKILL.md +26 -0
  30. package/core/skills/gh-cli/SKILL.md +2202 -0
  31. package/core/skills/git-commit/SKILL.md +124 -0
  32. package/core/skills/git-workflow-agents/SKILL.md +462 -0
  33. package/core/skills/git-workflow-agents/reference.md +220 -0
  34. package/core/skills/github-actions/SKILL.md +190 -0
  35. package/core/skills/github-issues/SKILL.md +154 -0
  36. package/core/skills/llm-structured-outputs/SKILL.md +323 -0
  37. package/core/skills/llm-structured-outputs/references/provider-details.md +392 -0
  38. package/core/skills/pre-push/SKILL.md +115 -0
  39. package/core/skills/refactor/SKILL.md +645 -0
  40. package/core/skills/web-design-reviewer/SKILL.md +371 -0
  41. package/core/skills/webapp-testing/SKILL.md +127 -0
  42. package/core/skills/webapp-testing/test-helper.js +56 -0
  43. package/core/templates/CLAUDE.md.tmpl +98 -0
  44. package/core/templates/adr-template.md +67 -0
  45. package/core/templates/gh-issue-templates/bug.md +39 -0
  46. package/core/templates/gh-issue-templates/content.md +42 -0
  47. package/core/templates/gh-issue-templates/enhancement.md +36 -0
  48. package/core/templates/gh-issue-templates/feature.md +39 -0
  49. package/core/templates/gh-issue-templates/infrastructure.md +41 -0
  50. package/core/templates/post-edit-reminders.sh.tmpl +19 -0
  51. package/core/templates/settings.json.tmpl +90 -0
  52. package/core/templates/settings.local.json.tmpl +3 -0
  53. package/core/workflows/agent-implementation-workflow.md +346 -0
  54. package/core/workflows/generate-project.md +258 -0
  55. package/core/workflows/implement-project-workflow.md +190 -0
  56. package/core/workflows/issue-tracking.md +89 -0
  57. package/core/workflows/project-closeout-ceremony.md +77 -0
  58. package/core/workflows/review-workflow.md +266 -0
  59. package/engsys.config.example.yaml +46 -0
  60. package/install +202 -0
  61. package/lessons-library/README.md +80 -0
  62. package/lessons-library/async-callbacks-verify-liveness.md +15 -0
  63. package/lessons-library/change-isnt-done-until-every-surface-updated.md +15 -0
  64. package/lessons-library/claim-then-act-for-irreversible-ops.md +16 -0
  65. package/lessons-library/co-commit-entangled-work.md +15 -0
  66. package/lessons-library/dependabot-triage-playbook.md +17 -0
  67. package/lessons-library/deploy-by-digest-and-verify-the-running-revision.md +15 -0
  68. package/lessons-library/enforce-your-guarantee-at-your-boundary.md +16 -0
  69. package/lessons-library/gate-changes-on-measurement-not-vibes.md +15 -0
  70. package/lessons-library/iac-first-no-console-changes.md +15 -0
  71. package/lessons-library/independent-objective-review-gate.md +15 -0
  72. package/lessons-library/keep-an-immutable-source-of-truth.md +15 -0
  73. package/lessons-library/long-agent-runs-checkpoint-not-poll.md +15 -0
  74. package/lessons-library/model-identity-with-stable-ids-and-provenance.md +15 -0
  75. package/lessons-library/operator-choices-are-first-class.md +15 -0
  76. package/lessons-library/prefer-tool-enforced-structured-output.md +15 -0
  77. package/lessons-library/prove-causation-before-acting.md +15 -0
  78. package/lessons-library/re-read-state-before-acting.md +14 -0
  79. package/lessons-library/read-layer-tolerates-unbackfilled-rows.md +15 -0
  80. package/lessons-library/shell-safety-pipefail-and-validate-before-teardown.md +14 -0
  81. package/lessons-library/shift-correctness-left-and-distrust-false-greens.md +15 -0
  82. package/lessons-library/stray-control-bytes-hide-changes.md +14 -0
  83. package/lessons-library/tests-can-assert-the-bug.md +15 -0
  84. package/lessons-library/verify-ground-truth-not-reports.md +15 -0
  85. package/lessons-library/worktrees-need-bootstrap-from-origin-main.md +15 -0
  86. package/lib/commands.js +356 -0
  87. package/lib/generate-team-avatars.mjs +251 -0
  88. package/lib/manifest.js +155 -0
  89. package/lib/render.js +135 -0
  90. package/lib/selftest.js +90 -0
  91. package/lib/util.js +89 -0
  92. package/lib/yaml.js +156 -0
  93. package/optional-agents/gary.md +86 -0
  94. package/optional-agents/jos.md +136 -0
  95. package/optional-agents/sandy.md +101 -0
  96. package/optional-agents/steve.md +161 -0
  97. package/package.json +43 -0
  98. package/stacks/cloud/aws/claude.fragment.md +17 -0
  99. package/stacks/cloud/aws/settings.fragment.json +39 -0
  100. package/stacks/cloud/aws/skills/aws-deployment-preflight/SKILL.md +165 -0
  101. package/stacks/cloud/aws/skills/cloud-architecture-aws/SKILL.md +265 -0
  102. package/stacks/cloud/azure/claude.fragment.md +17 -0
  103. package/stacks/cloud/azure/settings.fragment.json +45 -0
  104. package/stacks/cloud/azure/skills/azure-deployment-preflight/SKILL.md +175 -0
  105. package/stacks/cloud/azure/skills/cloud-architecture-azure/SKILL.md +211 -0
  106. package/stacks/cloud/cloudflare/claude.fragment.md +21 -0
  107. package/stacks/cloud/cloudflare/settings.fragment.json +31 -0
  108. package/stacks/cloud/cloudflare/skills/cloud-architecture-cloudflare/SKILL.md +294 -0
  109. package/stacks/cloud/cloudflare/skills/cloudflare-deployment-preflight/SKILL.md +175 -0
  110. package/stacks/cloud/gcp/claude.fragment.md +17 -0
  111. package/stacks/cloud/gcp/settings.fragment.json +40 -0
  112. package/stacks/cloud/gcp/skills/cloud-architecture-gcp/SKILL.md +208 -0
  113. package/stacks/cloud/gcp/skills/gcp-deployment-preflight/SKILL.md +137 -0
  114. package/stacks/db/mongo/skills/mongo-conventions/SKILL.md +96 -0
  115. package/stacks/db/prisma/claude.fragment.md +49 -0
  116. package/stacks/db/prisma/skills/docker-database-package-copy/SKILL.md +44 -0
  117. package/stacks/db/prisma/skills/prisma-conventions/SKILL.md +37 -0
  118. package/stacks/domain/mobile-growth/skills/apple-ads/SKILL.md +184 -0
  119. package/stacks/domain/mobile-growth/skills/apple-ads/references/benchmark-notes.md +47 -0
  120. package/stacks/domain/mobile-growth/skills/apple-ads/references/official-links.md +53 -0
  121. package/stacks/domain/mobile-growth/skills/google-play-growth/SKILL.md +197 -0
  122. package/stacks/domain/mobile-growth/skills/google-play-growth/references/benchmark-notes.md +47 -0
  123. package/stacks/domain/mobile-growth/skills/google-play-growth/references/official-links.md +45 -0
  124. package/stacks/iac/bicep/claude.fragment.md +14 -0
  125. package/stacks/iac/bicep/settings.fragment.json +20 -0
  126. package/stacks/iac/bicep/skills/iac-bicep/SKILL.md +113 -0
  127. package/stacks/iac/cdk/claude.fragment.md +14 -0
  128. package/stacks/iac/cdk/settings.fragment.json +23 -0
  129. package/stacks/iac/cdk/skills/iac-cdk/SKILL.md +104 -0
  130. package/stacks/iac/terraform/claude.fragment.md +13 -0
  131. package/stacks/iac/terraform/settings.fragment.json +25 -0
  132. package/stacks/iac/terraform/skills/iac-terraform/SKILL.md +93 -0
  133. package/stacks/iac/terraform/skills/terraform-conventions/SKILL.md +87 -0
  134. package/stacks/lang/kotlin/skills/android-testing/SKILL.md +263 -0
  135. package/stacks/lang/kotlin/skills/jetpack-compose/SKILL.md +264 -0
  136. package/stacks/lang/kotlin/skills/kotlin-coroutines/SKILL.md +329 -0
  137. package/stacks/lang/python/skills/python-conventions/SKILL.md +61 -0
  138. package/stacks/lang/shell/skills/shell-scripting/SKILL.md +110 -0
  139. package/stacks/lang/swift/skills/swift-concurrency/SKILL.md +423 -0
  140. package/stacks/lang/swift/skills/swift-concurrency/references/approachable-concurrency.md +80 -0
  141. package/stacks/lang/swift/skills/swift-concurrency/references/concurrency-patterns.md +233 -0
  142. package/stacks/lang/swift/skills/swift-concurrency/references/swiftui-concurrency.md +187 -0
  143. package/stacks/lang/swift/skills/swift-concurrency/references/synchronization-primitives.md +341 -0
  144. package/stacks/lang/swift/skills/swift-testing/SKILL.md +497 -0
  145. package/stacks/lang/swift/skills/swift-testing/references/testing-advanced.md +106 -0
  146. package/stacks/lang/swift/skills/swift-testing/references/testing-patterns.md +504 -0
  147. package/stacks/lang/swift/skills/swiftdata/SKILL.md +334 -0
  148. package/stacks/lang/swift/skills/swiftdata/references/core-data-coexistence.md +504 -0
  149. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-advanced.md +975 -0
  150. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-queries.md +675 -0
  151. package/stacks/lang/swift/skills/swiftui-patterns/SKILL.md +371 -0
  152. package/stacks/lang/swift/skills/swiftui-patterns/references/architecture-patterns.md +486 -0
  153. package/stacks/lang/swift/skills/swiftui-patterns/references/deprecated-migration.md +1097 -0
  154. package/stacks/lang/swift/skills/swiftui-patterns/references/design-polish.md +780 -0
  155. package/stacks/lang/swift/skills/swiftui-patterns/references/platform-and-sharing.md +696 -0
  156. package/stacks/lang/typescript/skills/typescript-conventions/SKILL.md +91 -0
  157. package/stacks/platform/android/claude.fragment.md +40 -0
  158. package/stacks/platform/android/hooks/pre-push-gradle.sh +70 -0
  159. package/stacks/platform/android/settings.fragment.json +13 -0
  160. package/stacks/platform/android/skills/android-build-conventions/SKILL.md +247 -0
  161. package/stacks/platform/ios/claude.fragment.md +24 -0
  162. package/stacks/platform/ios/hooks/pre-push-xcodebuild.sh +82 -0
  163. package/stacks/platform/ios/settings.fragment.json +21 -0
  164. package/stacks/platform/ios/skills/xcodebuildmcp-simulator-logs/SKILL.md +76 -0
  165. package/stacks/platform/web/skills/frontend-testing/SKILL.md +246 -0
  166. package/stacks/platform/web/skills/react-conventions/SKILL.md +261 -0
  167. package/stacks/platform/web/skills/web-platform-conventions/SKILL.md +55 -0
  168. package/stacks/tooling/issue-tracker-github/claude.fragment.md +10 -0
  169. package/stacks/tooling/issue-tracker-github/settings.fragment.json +24 -0
  170. package/stacks/tooling/issue-tracker-github/skills/issue-tracker-github/SKILL.md +278 -0
  171. package/stacks/tooling/issue-tracker-linear/claude.fragment.md +17 -0
  172. package/stacks/tooling/issue-tracker-linear/settings.fragment.json +9 -0
  173. package/stacks/tooling/issue-tracker-linear/skills/issue-tracker-linear/SKILL.md +183 -0
@@ -0,0 +1,504 @@
1
+ # Core Data Coexistence
2
+
3
+ Standalone Core Data patterns for projects not yet on SwiftData, strategies
4
+ for running Core Data and SwiftData side by side against the same store, and
5
+ migration guidance for transitioning from Core Data to SwiftData.
6
+
7
+ ## Contents
8
+
9
+ - [Standalone Core Data Stack](#standalone-core-data-stack)
10
+ - [Core Data + SwiftData Coexistence](#core-data--swiftdata-coexistence)
11
+ - [Migration from Core Data to SwiftData](#migration-from-core-data-to-swiftdata)
12
+
13
+ ## Standalone Core Data Stack
14
+
15
+ For teams that haven't adopted SwiftData, Core Data remains a fully supported
16
+ persistence framework.
17
+
18
+ Docs: [NSPersistentContainer](https://sosumi.ai/documentation/coredata/nspersistentcontainer),
19
+ [Setting up a Core Data stack](https://sosumi.ai/documentation/coredata/setting-up-a-core-data-stack)
20
+
21
+ ### NSPersistentContainer Setup
22
+
23
+ `NSPersistentContainer` encapsulates the Core Data stack: the managed object
24
+ model, the persistent store coordinator, and the managed object context.
25
+
26
+ ```swift
27
+ import CoreData
28
+
29
+ final class CoreDataStack: @unchecked Sendable {
30
+ static let shared = CoreDataStack()
31
+
32
+ let container: NSPersistentContainer
33
+
34
+ private init() {
35
+ // Name must match the .xcdatamodeld file name
36
+ container = NSPersistentContainer(name: "MyAppModel")
37
+
38
+ container.loadPersistentStores { description, error in
39
+ if let error {
40
+ fatalError("Core Data store failed to load: \(error)")
41
+ }
42
+ }
43
+
44
+ // Automatically merge changes from background contexts
45
+ container.viewContext.automaticallyMergesChangesFromParent = true
46
+ container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
47
+ }
48
+
49
+ /// The main-thread context for UI reads
50
+ var viewContext: NSManagedObjectContext {
51
+ container.viewContext
52
+ }
53
+
54
+ /// A new background context for writes
55
+ func newBackgroundContext() -> NSManagedObjectContext {
56
+ container.newBackgroundContext()
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### NSManagedObjectContext Usage
62
+
63
+ The `viewContext` is bound to the main queue; use it for reads and UI. Use
64
+ background contexts for writes and batch operations.
65
+
66
+ ```swift
67
+ // Reading on the main context
68
+ func fetchTrips() throws -> [CDTrip] {
69
+ let context = CoreDataStack.shared.viewContext
70
+ let request = CDTrip.fetchRequest()
71
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: true)]
72
+ return try context.fetch(request)
73
+ }
74
+
75
+ // Writing on a background context
76
+ func createTrip(name: String, destination: String) async throws {
77
+ let context = CoreDataStack.shared.newBackgroundContext()
78
+ try await context.perform {
79
+ let trip = CDTrip(context: context)
80
+ trip.name = name
81
+ trip.destination = destination
82
+ trip.startDate = Date.now
83
+ trip.id = UUID()
84
+ try context.save()
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### NSFetchRequest with NSPredicate and NSSortDescriptor
90
+
91
+ ```swift
92
+ import CoreData
93
+
94
+ func fetchUpcomingTrips(destination: String) throws -> [CDTrip] {
95
+ let context = CoreDataStack.shared.viewContext
96
+ let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
97
+
98
+ // Predicate: filter by destination and future start date
99
+ request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
100
+ NSPredicate(format: "destination ==[cd] %@", destination),
101
+ NSPredicate(format: "startDate > %@", Date.now as NSDate)
102
+ ])
103
+
104
+ // Sort: newest first
105
+ request.sortDescriptors = [
106
+ NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: false)
107
+ ]
108
+
109
+ // Performance: limit results and prefetch relationships
110
+ request.fetchLimit = 20
111
+ request.relationshipKeyPathsForPrefetching = ["accommodation"]
112
+
113
+ return try context.fetch(request)
114
+ }
115
+
116
+ // Counting without loading objects
117
+ func countFavoriteTrips() throws -> Int {
118
+ let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
119
+ request.predicate = NSPredicate(format: "isFavorite == YES")
120
+ return try CoreDataStack.shared.viewContext.count(for: request)
121
+ }
122
+ ```
123
+
124
+ ### Saving Context and Error Handling
125
+
126
+ ```swift
127
+ func saveContext(_ context: NSManagedObjectContext) throws {
128
+ guard context.hasChanges else { return }
129
+
130
+ do {
131
+ try context.save()
132
+ } catch {
133
+ // Roll back unsaved changes to prevent inconsistent state
134
+ context.rollback()
135
+
136
+ if let nsError = error as NSError? {
137
+ // Check for common Core Data errors
138
+ switch nsError.code {
139
+ case NSManagedObjectConstraintMergeError:
140
+ // Unique constraint violation -- handle merge conflict
141
+ throw PersistenceError.constraintViolation(nsError)
142
+ case NSValidationMissingMandatoryPropertyError:
143
+ // Required property is nil
144
+ throw PersistenceError.validationFailed(nsError)
145
+ default:
146
+ throw PersistenceError.saveFailed(nsError)
147
+ }
148
+ }
149
+ throw error
150
+ }
151
+ }
152
+
153
+ enum PersistenceError: Error {
154
+ case constraintViolation(NSError)
155
+ case validationFailed(NSError)
156
+ case saveFailed(NSError)
157
+ }
158
+ ```
159
+
160
+ ### Background Processing Pattern
161
+
162
+ ```swift
163
+ func importTrips(_ records: [TripRecord]) async throws {
164
+ let context = CoreDataStack.shared.newBackgroundContext()
165
+ try await context.perform {
166
+ // Batch insert for performance (iOS 13+)
167
+ let batchInsert = NSBatchInsertRequest(
168
+ entity: CDTrip.entity(),
169
+ objects: records.map { record in
170
+ [
171
+ "id": record.id,
172
+ "name": record.name,
173
+ "destination": record.destination,
174
+ "startDate": record.startDate
175
+ ] as [String: Any]
176
+ }
177
+ )
178
+ batchInsert.resultType = .count
179
+ let result = try context.execute(batchInsert) as? NSBatchInsertResult
180
+ print("Inserted \(result?.result as? Int ?? 0) trips")
181
+
182
+ // Merge changes into viewContext
183
+ NSManagedObjectContext.mergeChanges(
184
+ fromRemoteContextSave: [NSInsertedObjectsKey: []],
185
+ into: [CoreDataStack.shared.viewContext]
186
+ )
187
+ }
188
+ }
189
+ ```
190
+
191
+ ## Core Data + SwiftData Coexistence
192
+
193
+ Core Data and SwiftData can share the same underlying SQLite store. This
194
+ enables gradual migration: keep existing Core Data code running while
195
+ introducing SwiftData for new features.
196
+
197
+ Docs: [Adopting SwiftData for a Core Data app](https://sosumi.ai/documentation/coredata/adopting_swiftdata_for_a_core_data_app)
198
+
199
+ ### Using the Same Underlying Store
200
+
201
+ Both stacks must point to the same SQLite file and agree on the schema. The
202
+ Core Data `.xcdatamodeld` and SwiftData `@Model` classes must describe the
203
+ same entities and properties.
204
+
205
+ ```swift
206
+ import SwiftData
207
+ import CoreData
208
+
209
+ // 1. Determine the store URL that Core Data already uses
210
+ let storeURL = NSPersistentContainer.defaultDirectoryURL()
211
+ .appendingPathComponent("MyAppModel.sqlite")
212
+
213
+ // 2. Point SwiftData at the same store
214
+ let config = ModelConfiguration(
215
+ "MyAppModel",
216
+ url: storeURL
217
+ )
218
+
219
+ let container = try ModelContainer(
220
+ for: Trip.self,
221
+ configurations: config
222
+ )
223
+ ```
224
+
225
+ ### ModelConfiguration Pointing to Existing Core Data Store
226
+
227
+ Key rules for coexistence:
228
+
229
+ 1. The `@Model` class name must match the Core Data entity name.
230
+ 2. Property names and types must match exactly.
231
+ 3. Use `@Attribute(originalName:)` if you renamed properties.
232
+ 4. Both stacks should use the same store file.
233
+
234
+ ```swift
235
+ // Core Data entity: CDTrip (entity name "Trip" in .xcdatamodeld)
236
+ // Attributes: name (String), destination (String), startDate (Date),
237
+ // isFavorite (Boolean), imageData (Binary Data)
238
+
239
+ // Matching SwiftData model
240
+ @Model
241
+ class Trip {
242
+ var name: String
243
+ var destination: String
244
+ var startDate: Date
245
+ var isFavorite: Bool = false
246
+ @Attribute(.externalStorage) var imageData: Data?
247
+
248
+ init(name: String, destination: String, startDate: Date) {
249
+ self.name = name
250
+ self.destination = destination
251
+ self.startDate = startDate
252
+ }
253
+ }
254
+ ```
255
+
256
+ ### Gradual Coexistence Strategy
257
+
258
+ ```swift
259
+ // Phase 1: Core Data stack still handles writes;
260
+ // SwiftData reads the same store for new UI
261
+ @main
262
+ struct MyApp: App {
263
+ let coreDataStack = CoreDataStack.shared // Existing Core Data
264
+
265
+ var body: some Scene {
266
+ WindowGroup {
267
+ ContentView()
268
+ }
269
+ // SwiftData reads from the same store
270
+ .modelContainer(for: Trip.self, configurations:
271
+ ModelConfiguration(url: coreDataStack.storeURL)
272
+ )
273
+ }
274
+ }
275
+
276
+ // Phase 2: New features use SwiftData for both reads and writes
277
+ // Phase 3: Migrate remaining Core Data code to SwiftData
278
+ // Phase 4: Remove Core Data stack and .xcdatamodeld
279
+ ```
280
+
281
+ ### Important Coexistence Rules
282
+
283
+ - **Do not write to the same entity from both stacks simultaneously.** Pick
284
+ one stack per entity for writes to avoid conflicts.
285
+ - Core Data's `automaticallyMergesChangesFromParent` and
286
+ `NSPersistentStoreRemoteChangeNotification` help detect changes from the
287
+ other stack.
288
+ - Test thoroughly -- schema mismatches between the `.xcdatamodeld` and
289
+ `@Model` cause crashes.
290
+
291
+ ## Migration from Core Data to SwiftData
292
+
293
+ ### Step 1: Map Core Data Entities to @Model Classes
294
+
295
+ Create a `@Model` class for each Core Data entity. Property names and types
296
+ must align with the `.xcdatamodeld` definition.
297
+
298
+ ```swift
299
+ // Core Data entity "Article"
300
+ // Attributes: id (UUID), title (String), body (String),
301
+ // createdAt (Date), isDraft (Boolean)
302
+ // Relationships: author (to-one → Author), tags (to-many → Tag)
303
+
304
+ @Model
305
+ class Article {
306
+ @Attribute(.unique) var id: UUID
307
+ var title: String
308
+ var body: String
309
+ var createdAt: Date
310
+ var isDraft: Bool = true
311
+
312
+ @Relationship(deleteRule: .nullify, inverse: \Author.articles)
313
+ var author: Author?
314
+
315
+ @Relationship(deleteRule: .nullify, inverse: \Tag.articles)
316
+ var tags: [Tag] = []
317
+
318
+ init(id: UUID = UUID(), title: String, body: String, createdAt: Date = .now) {
319
+ self.id = id
320
+ self.title = title
321
+ self.body = body
322
+ self.createdAt = createdAt
323
+ }
324
+ }
325
+
326
+ @Model
327
+ class Author {
328
+ @Attribute(.unique) var id: UUID
329
+ var name: String
330
+ var articles: [Article] = []
331
+
332
+ init(id: UUID = UUID(), name: String) {
333
+ self.id = id
334
+ self.name = name
335
+ }
336
+ }
337
+
338
+ @Model
339
+ class Tag {
340
+ @Attribute(.unique) var id: UUID
341
+ var name: String
342
+ var articles: [Article] = []
343
+
344
+ init(id: UUID = UUID(), name: String) {
345
+ self.id = id
346
+ self.name = name
347
+ }
348
+ }
349
+ ```
350
+
351
+ ### Type Mapping Reference
352
+
353
+ | Core Data Type | SwiftData Type |
354
+ |---|---|
355
+ | String | String |
356
+ | Boolean | Bool |
357
+ | Integer 16/32/64 | Int |
358
+ | Float / Double | Float / Double |
359
+ | Date | Date |
360
+ | Binary Data | Data |
361
+ | UUID | UUID |
362
+ | URI | URL |
363
+ | Decimal | Decimal |
364
+ | Transformable | Codable struct (composite, iOS 18+) |
365
+ | To-one relationship | Optional reference to @Model |
366
+ | To-many relationship | Array of @Model |
367
+
368
+ ### Step 2: Schema Versioning Considerations
369
+
370
+ If the Core Data store has existing data, SwiftData must be able to open it.
371
+ Use `VersionedSchema` and `SchemaMigrationPlan` for non-trivial changes.
372
+
373
+ ```swift
374
+ // If the SwiftData model exactly matches the Core Data schema,
375
+ // no migration is needed -- SwiftData opens the store directly.
376
+
377
+ // For schema differences, define versioned schemas:
378
+ enum SchemaV1: VersionedSchema {
379
+ static var versionIdentifier = Schema.Version(1, 0, 0)
380
+ static var models: [any PersistentModel.Type] { [Article.self, Author.self] }
381
+
382
+ @Model class Article {
383
+ var id: UUID
384
+ var title: String
385
+ var body: String
386
+ var createdAt: Date
387
+ init(id: UUID, title: String, body: String, createdAt: Date) {
388
+ self.id = id; self.title = title
389
+ self.body = body; self.createdAt = createdAt
390
+ }
391
+ }
392
+
393
+ @Model class Author {
394
+ var id: UUID
395
+ var name: String
396
+ init(id: UUID, name: String) { self.id = id; self.name = name }
397
+ }
398
+ }
399
+
400
+ enum SchemaV2: VersionedSchema {
401
+ static var versionIdentifier = Schema.Version(2, 0, 0)
402
+ static var models: [any PersistentModel.Type] { [Article.self, Author.self, Tag.self] }
403
+
404
+ @Model class Article {
405
+ var id: UUID
406
+ var title: String
407
+ var body: String
408
+ var createdAt: Date
409
+ var isDraft: Bool = true // New property
410
+ init(id: UUID, title: String, body: String, createdAt: Date) {
411
+ self.id = id; self.title = title
412
+ self.body = body; self.createdAt = createdAt
413
+ }
414
+ }
415
+
416
+ @Model class Author {
417
+ var id: UUID
418
+ var name: String
419
+ init(id: UUID, name: String) { self.id = id; self.name = name }
420
+ }
421
+
422
+ @Model class Tag {
423
+ var id: UUID
424
+ var name: String
425
+ init(id: UUID, name: String) { self.id = id; self.name = name }
426
+ }
427
+ }
428
+
429
+ enum ArticleMigrationPlan: SchemaMigrationPlan {
430
+ static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] }
431
+ static var stages: [MigrationStage] {
432
+ [MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)]
433
+ }
434
+ }
435
+ ```
436
+
437
+ ### Step 3: Testing Migration Paths
438
+
439
+ Always test migration with real production data copies before shipping.
440
+
441
+ ```swift
442
+ import XCTest
443
+ import SwiftData
444
+
445
+ final class MigrationTests: XCTestCase {
446
+
447
+ func testCoreDataToSwiftDataMigration() throws {
448
+ // 1. Copy a known Core Data store into the test bundle
449
+ let sourceURL = Bundle(for: type(of: self))
450
+ .url(forResource: "TestStore", withExtension: "sqlite")!
451
+
452
+ let tempDir = FileManager.default.temporaryDirectory
453
+ .appendingPathComponent(UUID().uuidString)
454
+ try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
455
+
456
+ let destURL = tempDir.appendingPathComponent("TestStore.sqlite")
457
+ try FileManager.default.copyItem(at: sourceURL, to: destURL)
458
+
459
+ // Copy WAL and SHM if they exist
460
+ for ext in ["-wal", "-shm"] {
461
+ let src = sourceURL.deletingLastPathComponent()
462
+ .appendingPathComponent("TestStore.sqlite\(ext)")
463
+ if FileManager.default.fileExists(atPath: src.path) {
464
+ try FileManager.default.copyItem(
465
+ at: src,
466
+ to: tempDir.appendingPathComponent("TestStore.sqlite\(ext)")
467
+ )
468
+ }
469
+ }
470
+
471
+ // 2. Open with SwiftData
472
+ let config = ModelConfiguration(url: destURL)
473
+ let container = try ModelContainer(
474
+ for: SchemaV2.Article.self,
475
+ migrationPlan: ArticleMigrationPlan.self,
476
+ configurations: config
477
+ )
478
+
479
+ // 3. Verify data survived migration
480
+ let context = ModelContext(container)
481
+ let articles = try context.fetch(FetchDescriptor<SchemaV2.Article>())
482
+ XCTAssertFalse(articles.isEmpty, "Migration should preserve existing articles")
483
+
484
+ // 4. Verify new properties have defaults
485
+ for article in articles {
486
+ XCTAssertTrue(article.isDraft, "New isDraft property should default to true")
487
+ }
488
+
489
+ // Cleanup
490
+ try FileManager.default.removeItem(at: tempDir)
491
+ }
492
+ }
493
+ ```
494
+
495
+ ### Migration Checklist
496
+
497
+ - [ ] Every Core Data entity has a matching `@Model` class with identical property names and types
498
+ - [ ] Relationship inverse properties are specified in both directions
499
+ - [ ] `VersionedSchema` and `SchemaMigrationPlan` defined for non-trivial schema changes
500
+ - [ ] `ModelConfiguration` points to the existing Core Data SQLite file
501
+ - [ ] Tested migration with a copy of production data
502
+ - [ ] Only one stack writes to each entity during coexistence
503
+ - [ ] `automaticallyMergesChangesFromParent` enabled on Core Data's `viewContext`
504
+ - [ ] `.xcdatamodeld` removed only after full migration is verified