This file is for agents working in this repository. Keep it focused on workflow, constraints, and repo-specific pitfalls. Detailed API usage already lives in README.md.
CoreDataEvolution brings SwiftData-style actor isolation to Core Data through macros and a custom serial executor.
- Library target:
CoreDataEvolution - Macro target:
CoreDataEvolutionMacros - Example executable:
CoreDataEvolutionClient - Test target:
CoreDataEvolutionTests
The package uses Swift 6 and ships a macro implementation backed by swift-syntax. swift-syntax is a build-time dependency only.
- Minimum deployment targets are iOS 13, macOS 10.15, tvOS 13, watchOS 6, visionOS 1.
- Use Swift 6 language mode.
- Do not introduce APIs that require iOS 17+ / macOS 14+ unless guarded by explicit availability checks.
- The runtime executor implementation must remain compatible with the minimum deployment targets.
Public root-level user docs:
README.md: primary user-facing overview.AGENTS.md: repository workflow and contributor guidance.
User-facing supplemental docs live under Docs/.
Internal development docs live under Docs/Development/.
Package.swift: package definition, product graph, platform constraints, Swift settings.Sources/CoreDataEvolution: public library code.Sources/CoreDataEvolutionMacros: macro expansion code.Sources/CoreDataEvolutionClient: minimal example executable.Tests/CoreDataEvolutionTests: package tests plus helper Core Data model types..githooks: optional git hooks for formatting staged Swift files before commit..swift-format: formatting rules used by the hook and local formatting runs.
Use these first:
swift build
bash Scripts/run-tests.shrun-tests.sh wraps swift test and always injects com.apple.CoreData.ConcurrencyDebug=1
as a process environment variable via env. macOS UserDefaults reads this from the process
environment, activating Core Data thread-violation detection without requiring -- argument
passing (which current swift test versions do not support for arbitrary keys).
Prefer this script over bare swift test so concurrency violations are caught on every local run,
not only in Xcode via the test plan.
Useful targeted runs:
bash Scripts/run-tests.sh --filter NSModelActorTests
bash Scripts/run-tests.sh --filter WithContextTests
bash Scripts/run-tests.sh --filter IntegrationModelOptions:
--filter <pattern>— forward--filtertoswift test--no-parallel— add--no-paralleltoswift test--sql-debug— also enable-com.apple.CoreData.SQLDebug 1(verbose)
Bare swift test still works but does not carry the Core Data concurrency flag.
Tooling CLI build:
bash Scripts/build-cde-tool.shFormatting, if swift-format is installed:
swift-format format --in-place <path-to-file.swift>Integration model compile + run (for real .momd verification):
bash Scripts/compile-integration-model.sh
bash Scripts/test-integration-model.sh
bash Scripts/test-generated-flow.shPath/toolchain behavior for integration model scripts:
- Do not hardcode toolchain paths.
Scripts/compile-integration-model.shresolvesmomcin this order:CDE_MOMC_BINxcrun --find momcmomcfrom$PATH
- Model source can be overridden by
CDE_INTEGRATION_MODEL_SOURCE. - Output
.momdcan be overridden byCDE_INTEGRATION_MODEL_OUTPUT. - Tests can consume a precompiled model via
CDE_INTEGRATION_MODEL_MOMD. - If
CDE_INTEGRATION_MODEL_MOMDis not set, integration tests compile the model on demand viaScripts/compile-integration-model.sh. Scripts/test-generated-flow.shruns the downstream generated-model fixture:- builds
cde-tool - generates source into
Integration/GeneratedFlowFixture - validates generated output
- builds and runs the external fixture package
- builds
Scripts/test-generated-flow.shdependency modes:- default
pathmode keeps the checked-in fixture pointed at the current workspace via.package(path: "../..") tag/branch/revisionmodes copy the fixture to a temp directory and rewrite only the tempPackage.swiftdependency- use those non-
pathmodes for release smoke checks without editing tracked fixture files - note: the script still builds
cde-toolfrom the current workspace; non-pathmodes only change the external fixture package dependency source
- default
- Release tags use bare semantic versions in the form
x.y.z. - Do not prefix release tags with
v. - Follow the existing repository convention, for example
0.7.4.
This repo includes an optional pre-commit hook.
- Install with
bash .githooks/install.sh, or setgit config core.hooksPath .githooks. - The hook formats staged
.swiftfiles and re-stages them. - If
swift-formatis missing, the hook warns and exits successfully. It does not block commits. swift-formatis resolved from$PATH, then~/.swiftly/bin/swift-format, thenxcrun --find swift-format.
When editing Swift files, keep formatting consistent with .swift-format:
- 2-space indentation
- 100-column line length
- ordered imports
- ASCII identifiers only
Sources/CoreDataEvolution contains:
Macros.swift: public macro declarations.NSModelActor.swift: actor protocol plusunownedExecutor,modelContext, typed subscript, andwithContexthelpers.NSMainModelActor.swift: main-actor class protocol plusmodelContext, typed subscript, andwithContexthelpers.NSModelObjectContextExecutor.swift: serial executor that enqueuesUnownedJobonNSManagedObjectContext.perform.NSPersistentContainer+Testing.swift: isolated SQLite-backed test container helper.module.swift: re-exportsCoreData.
Sources/CoreDataEvolutionMacros is intentionally small:
NSModelActorMacroaddsmodelExecutor,modelContainer, and optionallyinit(container:), then addsNSModelActorconformance.NSMainModelActorMacroaddsmodelContainerand optionallyinit(modelContainer:), then addsNSMainModelActorconformance.Helper.swiftcontains the shared parsing helpers fordisableGenerateInitand access control.
The macros currently mirror public access from the attached type, but otherwise do very little validation. If you add validation or diagnostics, update tests and docs accordingly.
@NSModelActordefault initializer usescontainer.newBackgroundContext().@NSMainModelActorbindsmodelContexttocontainer.viewContext.@NSMainModelActortypes are expected to be@MainActorclasses. The macro does not currently enforce this itself.disableGenerateInit: truemeans the custom initializer must assign the generated stored properties correctly.@PersistentModelrelationship generation keeps setters only for to-one properties; to-many relationships (Set<T>/[T]) are getter-only and must be mutated through generated helper methods.NSModelObjectContextExecutoris@unchecked Sendable; changes here are concurrency-sensitive and need careful review.module.swiftintentionally uses@_exported import CoreData; avoid removing it without checking downstream API impact.
The tests encode several important constraints. Preserve them.
- Use
NSPersistentContainer.makeTest(...)for test stores. - Do not use
/dev/nullas a Core Data store URL. This repo explicitly avoids it because parallel tests can share state and deadlock. makeTestuses an on-disk SQLite store under a temp subdirectory and deletes stale.sqlite,.sqlite-shm, and.sqlite-walfiles before loading.- Treat
makeTestas a one-shot test container by default:testNamefalls back to the call-site identity (#fileID+#function), which is intended for one container per test method. - If one test method needs multiple containers, pass distinct
testNamevalues so they do not reuse the same store path. makeTestintentionally serializesNSPersistentContainercreation andloadPersistentStoreswith a global lock.- Reason: under extreme parallel test execution, Core Data can crash inside
loadPersistentStoreswithEXC_BAD_ACCESSor hang, even when every test uses a unique SQLite store URL. - The motivating real-world case came from
PersistentHistoryTrackingKit: many Core Data-heavy tests running in one process, shared static model/container helpers, and parallel container creation. Without the lock the suite had to run serially; with the lock, the tests could run in parallel again. - Do not remove that lock unless the container initialization path is reworked and revalidated under repeated parallel stress runs.
- The SQLite-backed approach is intentional: it avoids
/dev/nullshared-state issues, exercises a more realistic SQLite + WAL environment, and is typically more reliable for parallel test execution than shared in-memory setups. - Test model definitions should use
static letforNSManagedObjectModel; multiple model instances for the same schema can break store registration. - Test helper files use
@preconcurrency import CoreDatato suppress Swift 6 sendability noise around Core Data types. TestStacksetscontainer.viewContext.automaticallyMergesChangesFromParent = true; keep that in mind when changing tests involving background writes.- Main-thread tests are explicitly marked
@MainActor. - For tests that verify real persistence behavior, prefer suite-local
@NSModelActorhandlers over directly manipulating contexts in test functions. - If a test needs direct context/container access for assertions, use
try await handler.withContext { ... }so operations stay in the actor isolation domain. - Use
@MainActortest suites only when the behavior under test is explicitly main-actor/viewContext specific.
Tests/CoreDataEvolutionTests/CoreDataEvolution-Package.xctestplan enables:
-com.apple.CoreData.ConcurrencyDebug 1
This covers Xcode runs. For CLI runs, use bash Scripts/run-tests.sh which injects the same flag
via env "com.apple.CoreData.ConcurrencyDebug=1" swift test. Bare swift test does not pick up
the test plan argument automatically.
When making code changes:
- Check both the library target and the macro target. Many user-facing changes require edits in both.
- If you change generated members or initializer behavior, update tests first or in the same change.
- If you change public macro semantics, update
README.mdand DocC as well. - Keep main-actor and background-actor behavior aligned where appropriate; the two protocol extensions intentionally expose similar APIs.
- Be conservative around availability, executor behavior, and Core Data threading assumptions.
When editing Sources/CoreDataEvolutionToolingCore/:
- Add succinct comments to public types and service entry points so another developer can quickly understand the role of each file and API.
- Add short internal comments only where a helper encodes non-obvious behavior, ordering, or fallback rules.
- Do not comment every line. Prefer comments that explain:
- what problem a type/function solves
- which inputs or precedence rules matter
- which assumptions or v1 boundaries are intentional
- Keep comments aligned with code and docs. If behavior changes, update the nearby comment in the same change.
There is active WIP for typed path mapping and NSPredicate construction.
- Source location:
Sources/CoreDataEvolution/TypedPath/ - Test location:
Tests/CoreDataEvolutionTests/TypedPath/ - Purpose: support
Keys + path + __cdFieldTableas the shared base for sort and%K-based predicate building.
Current scope:
- Typed sort construction from
Object.KeysandObject.path.* %Kpredicate building from mapped paths (including composition and relationships)- To-many predicate quantifiers:
any/all/none - Composition contracts via
CDCompositionPathProviding+CDCompositionValueCodable(no runtime reflection) @Compositioncurrently generates:__cdCompositionFieldTable__cdDecodeComposition(from:)__cdEncodeComposition
Current boundaries:
- Sort does not support to-many relationship paths.
- Predicate layer currently stays on Foundation
NSPredicate(no separateCDPredicatetype). .noneand.allare expanded usingNOT (ANY ...)forms for compatibility.
When editing this area:
- Keep mapping key space anchored to Swift paths in
__cdFieldTable. - Keep
%Kas the only key interpolation path for predicate format strings. - Update both docs (
Docs/Development/Specification.md,Docs/Development/ImplementationPlan.md,Docs/Development/DesignNotes.md) and tests together.
Before implementing new macros or changing generated members, set up macro tests first to prevent silent expansion drift.
Recommended structure:
Tests/CoreDataEvolutionMacroTests/Tests/CoreDataEvolutionMacroTests/MacroTestSupport.swiftTests/CoreDataEvolutionMacroTests/MacroExpansionSnapshotTests.swiftTests/CoreDataEvolutionMacroTests/MacroDiagnosticTests.swiftTests/CoreDataEvolutionMacroTests/Fixtures/Tests/CoreDataEvolutionMacroTests/__Snapshots__/
Recommended workflow:
- Use snapshot tests for expanded source output.
- Use diagnostic tests for compile-time errors/warnings messages.
- Gate snapshot updates behind
UPDATE_SNAPSHOTS=1; default CI behavior should fail on mismatch.
Implementation notes:
- Prefer using the same
SwiftSyntaxexpansion pipeline pattern asObservableDefaultsMacroTests(SwiftParser+SwiftSyntaxMacroExpansion+BasicMacroExpansionContext). - Keep one shared macro registry in
MacroTestSupportfor all test files. - When macro output changes intentionally, update snapshots and docs in the same change.
Package.swiftSources/CoreDataEvolution/Macros.swiftSources/CoreDataEvolution/NSModelActor.swiftSources/CoreDataEvolution/NSMainModelActor.swiftSources/CoreDataEvolution/NSModelObjectContextExecutor.swiftSources/CoreDataEvolution/NSPersistentContainer+Testing.swiftSources/CoreDataEvolutionMacros/NSModelActorMacro.swiftSources/CoreDataEvolutionMacros/NSMainModelActorMacro.swiftTests/CoreDataEvolutionTests/NSModelActorTests.swiftTests/CoreDataEvolutionTests/WithContextTests.swiftTests/CoreDataEvolutionTests/Helper/Container.swiftSources/CoreDataEvolution/TypedPath/Tests/CoreDataEvolutionTests/TypedPath/
Keep AGENTS.md focused on repository workflow and constraints.
- Put end-user API explanations in
README.md. - Put package reference material in DocC.
- Put only the minimum necessary API reminders here when they affect safe code changes or test behavior.