A static analysis CLI for Swift actor isolation and data-race risk — a whole-project isolation map, not a single runtime trace.
Latest release: 0.3.0.
Status: working. The full pipeline is implemented and tested — project/scheme resolution, index-store discovery and staleness detection, a hybrid
libIndexStore+SwiftSyntaxinference engine, and an external-isolation oracle that resolves compiled-dependency symbols (CocoaPods, XCFrameworks, SDK frameworks) via bulksymbolgraph-extractand, as a fallback, livesourcekitdqueries, including real Swift 6 escape-hatch detection (@unchecked Sendable,nonisolated(unsafe),@preconcurrency) with a genuine SE-0337 severity downgrade. 645 tests passing (swift test), continuously validated against several real, independent projects (one private, ~2200+ source files across dozens of CocoaPods + SPM dependencies; several public, including a 5500+-file, 497-target one), not just fixtures.mermaid/dot/jsonoutput all ship. Not yet built: thev0.4items below (diffsubcommand, GitHub Action, migration-debt map, packaged distribution).
- macOS 15+.
- A full Xcode install — not just the Command Line Tools. This tool
dlopens two toolchain dylibs directly at runtime —libIndexStore.dylib(the semantic call graph) andsourcekitdInProc(the external-isolation oracle) — and, for.xcodeproj/.xcworkspaceprojects, also shells out toxcodebuild. Neither dylib ships with the standalone Command Line Tools package, so ifxcode-selectis currently pointed at/Library/Developer/CommandLineTools, point it at a full Xcode install instead:sudo xcode-select -s /Applications/Xcode.app. - The
swifttoolchain — forPackage.swift(SPM) targets, checked in addition to the above.
All of this is checked automatically at startup: if anything's missing, swift-isolation-map fails
fast with a specific diagnostic (which binary or dylib, where it looked, how to fix it) instead of
failing deep inside the pipeline with a cryptic error.
No packaged distribution yet (Homebrew/SPM-plugin are v0.4+, see Roadmap below) — build from
source:
git clone https://github.com/btctcn/swift-isolation-map.git
cd swift-isolation-map
swift build -c release
.build/release/swift-isolation-map --help
See Usage below for the full flag reference, or
CONTRIBUTING.md for running the test suite and opening the package in Xcode.
Swift 6's strict concurrency checking turned actor isolation violations into hard compiler errors. Developers migrating legacy codebases hit opaque errors like Sending value risks data race with no architectural context for why the boundary is unsafe, and no tool that shows the isolation structure of the whole project at once.
Xcode Instruments visualizes actor hops, but only as a runtime trace of one specific run — it doesn't cover the whole codebase statically, and it requires exercising the relevant code path first. Compiler diagnostics are bare, with no architectural framing. swift-isolation-map fills that gap:
- Static analysis of the whole project, not a runtime trace of one run
- An architectural map (the full isolation-domain graph), not a single-run timeline
- Explains why an isolation boundary is risky, in architectural context — not just what happened
- CI-suitable — can be embedded as a pipeline gate, which a runtime trace cannot
- A migration-debt map, trackable over time via git history
It's easy to assert that Sending value risks data races is uninformative;
here's what actually happens when you compile three progressively-realistic
reproductions under Swift 6.3 strict concurrency. The diagnostic is precise
when the unsafe access is in the same function as the send — it names the
exact conflicting line. The moment the send crosses a function boundary
(a helper call, a Task { } closure), the diagnostic still correctly fires,
but stops pointing at the access it actually conflicts with, and can't rank
which of several candidate mutations is the real one. See
docs/motivation.md for the full reproductions and
unedited compiler output.
A hybrid of libIndexStore (semantic call graph, resolved through protocols/generics/witnesses --
read directly via its raw C API, not the higher-level IndexStoreDB wrapper; see
docs/task-raw-indexstore-spike.md) and SwiftSyntax (lexical isolation attributes), combined by
a version-aware inference engine.
Where a call reaches into a compiled dependency the index alone can't explain (a CocoaPod, an
XCFramework, an SDK framework), an external-isolation oracle backfills the missing fact — a bulk
swift symbolgraph-extract cache first, a live, sequential sourcekitd query only for what the
cache misses. See docs/README.md for the full pipeline and oracle diagrams,
docs/architecture.md for the original project specification, and
docs/research/ for the real research/review trail behind the oracle's
design.
swift-isolation-map <path> --scheme <scheme> [OPTIONS]
ARGUMENTS:
<path> Path to a .xcodeproj, .xcworkspace, or Package.swift
OPTIONS:
--auto-build If the index store is missing or stale, build the project
without an interactive prompt.
--index-store-module-filter
Re-enable index-store module-name/is_system_unit scoping of the raw
index-store scan. Off by default -- this tool's own private, per-
(project, scheme, destination) index store (see "Where the index
store lives" below) never accumulates unrelated targets' units in
the first place. A defensive fallback for the one remaining
scenario that isn't fully ruled out (e.g. Xcode's own "Custom
Derived Data Location" preference happening to point at this same
private path).
--force-reindex Forces a rebuild, ignoring any existing (even fresh) index store.
--oracle-workers <N> Parallelize the external-oracle live-query phase across N worker
processes (default: 1, sequential). Real speedup on a large project:
~1.8x at N=4, ~3.3x at N=8 -- see docs/task-process-tree-optimization.md.
--out-file <path> Where to write the result (default: stdout)
--output <format> mermaid | dot | json (default: mermaid)
--platform <name> For a scheme offering more than one simultaneous Simulator
destination (e.g. visionOS added alongside iOS on the same
scheme, not a separate one): iOS | tvOS | watchOS | visionOS.
Matched case-insensitively. Fails with a clear error (listing
what's actually available) rather than silently picking a
platform you didn't ask for. Default: whichever Simulator
destination xcodebuild lists first for the scheme.
--scheme <scheme> Build scheme (Xcode) or product/target (SPM). Required.
--severity <level> Only include edges at or above this risk level in the output:
low | medium | high (default: no filtering, everything is
included). An edge with unresolved/unknown isolation on either
side is always included regardless of this setting -- filtering
to a stricter severity never hides genuine uncertainty.
--skip-macro-validation Pass -skipMacroValidation to every internal xcodebuild invocation
(live-fallback/cursor-info compiler-args resolution, --auto-build's
rebuild). Needed for projects using SPM macro plugins (e.g.
swift-case-paths) -- without it, those internal builds fail with
"Macro ... must be enabled before it can be used", silently
starving the live-oracle phase. Off by default: this bypasses a
real Xcode security gate (arbitrary code execution during macro
expansion), so only enable it for a project you trust.
--sort <key> Sort edges in the output: file (by location.file, then line) |
severity (high first). Default: whatever order the analysis
produced them in. Purely a presentation choice -- never changes
which edges are included or the exit code.
--verbose What was searched, where the index store was found, how many
types were processed.
Example:
swift-isolation-map ./MyApp.xcworkspace --scheme MyApp --auto-build --output json --out-file report.json
For .xcodeproj/.xcworkspace projects, this tool never reads from or builds into Xcode's own
shared ~/Library/Developer/Xcode/DerivedData — every real xcodebuild invocation it makes (the
compiler-argument-resolution build SyntaxAnalysis/the live-fallback/the external-isolation oracle
need, and the index-store-populating build itself) targets its own private, per-run -derivedDataPath
instead:
~/Library/Caches/swift-isolation-map/DerivedData/<project>-<hash>/<scheme>/<destination>/<configuration>/
<project>-<hash> identifies the exact real checkout (two different checkouts of the same repo —
branches, worktrees — get different, non-colliding directories); <scheme>/<destination> keep two
different schemes or platforms from ever landing in the same store. This exists because Xcode's own
Index.noindex/DataStore is shared and accumulated across every build anything has ever run
against that DerivedData (Xcode GUI, CI, a different tool invocation) — a real, confirmed source of
stale/foreign data in the index a scheme-scoped analysis never asked for; see
docs/task-index-store-module-scoping.md and docs/task-private-derived-data-hypothesis.md for the
full investigation. The directory is not deleted between runs (repeat analyses of the same
project/scheme/destination get normal Xcode incremental-build speed, not a full rebuild every time)
but lives under ~/Library/Caches, so it's always safe to delete by hand if you want the space back
or want to force a clean rebuild.
For Package.swift (SPM) targets, the equivalent has been true since v0.1: this tool has always
used its own private index store (.build/swift-isolation-map-index-store), never SwiftPM's shared
default.
Exit codes: 0 — no high-risk boundaries found; 1 — high-risk boundaries found (fail a CI
gate on this); a thrown error otherwise (bad scheme, unreachable index store, etc.).
Take this real, tiny package (a background-sync helper that has to reach both an actor and a
@MainActor view controller to do its job):
actor SessionStore {
static let shared = SessionStore()
private(set) var currentUserName: String = "Guest"
func signIn(as name: String) {
currentUserName = name
}
}
@MainActor
final class ProfileViewController {
var displayName: String = ""
func render(name: String) {
displayName = name
}
}
// Runs off the main actor (a background sync task) -- has to hop onto both
// SessionStore's actor and ProfileViewController's MainActor to do its job.
func backgroundSync(controller: ProfileViewController) async {
let name = await SessionStore.shared.currentUserName
await controller.render(name: name)
}swift-isolation-map ./Package.swift --scheme ReadmeExample --force-reindex
produces (real output, unedited):
flowchart LR
n0["SessionStore<br/>actor(SessionStore)"]
n1["currentUserName<br/>actor(SessionStore)"]
n2["shared<br/>nonisolated"]
n3["signIn<br/>actor(SessionStore)"]
n4["backgroundSync<br/>nonisolated"]
n5["ProfileViewController<br/>globalActor(MainActor)"]
n6["displayName<br/>globalActor(MainActor)"]
n7["render<br/>globalActor(MainActor)"]
n4 --> n7
n4 --> n1
linkStyle 0 stroke:#e53935,stroke-width:2px
linkStyle 1 stroke:#e53935,stroke-width:2px
Reading it: every node is a declaration, labeled with the isolation domain it actually
belongs to (an actor, a @MainActor/custom global actor, or nonisolated) — this is the part a
compiler error never shows you in one place: the whole project's isolation domains, not just
the one symbol currently under your cursor. Every edge is a real call the index found from a
project-local function into isolated state; its color is the risk classification. Here both edges
are red (high) — backgroundSync is itself nonisolated, and it reaches into both
SessionStore's actor state and ProfileViewController's @MainActor state.
--output json gives the same facts machine-readably (trimmed here to the two interesting nodes
and the two edges — the real output also lists every other analyzed declaration):
{
"schemaVersion": "1.0",
"swiftVersion": "6.3",
"ruleSetUsed": "Swift63RuleSet",
"summary": {
"typesAnalyzed": 3,
"actors": 1,
"mainActorTypes": 1,
"crossActorBoundaries": 2,
"highRiskBoundaries": 2,
"unspecifiedIsolation": 0
},
"nodes": [
{ "usr": "s:...SessionStoreC", "name": "SessionStore", "isolation": "actor(SessionStore)", "location": {"file": "Sources/.../Sync.swift", "line": 3} },
{ "usr": "s:...ProfileViewControllerC", "name": "ProfileViewController", "isolation": "globalActor(MainActor)", "location": {"file": "Sources/.../Sync.swift", "line": 13} }
],
"edges": [
{
"callerUSR": "s:...backgroundSync...", "calleeUSR": "s:...ProfileViewControllerC6render...",
"callerIsolation": "nonisolated", "calleeIsolation": "globalActor(MainActor)",
"risk": "high", "isUnknown": false,
"explanation": "nonisolated code reaches globalActor(MainActor)-isolated state -- no static isolation check protects this boundary",
"location": {"file": "Sources/.../Sync.swift", "line": 25}
}
]
}nodes— every analyzed declaration, keyed by USR (stable across runs/revisions — the identifier a futurediffsubcommand or your own tooling should match on, nevername, which isn't unique under overloading).edges— every cross-isolation call the index found, with both sides' isolation named explicitly (not just a boolean), arisklevel, and a plain-Englishexplanationstring — the "why," not just the "where," which is the whole point (see Compiler diagnostics: a concrete look above).isUnknown— set when one side's isolation genuinely couldn't be determined (a compiled dependency the oracle failed to resolve). Whentrue,riskis still present but must not be read as a confirmed finding — it reflectsunspecifiedisolation, not a proven-unsafe boundary. The tool would rather tell you "I don't know" than guess.isAwaited—truewhen this exact call site is syntactically inside a realawait <expr>expression. Purely informational, and deliberately never changesrisk— see the caveat below for why.structuralRisk/severityRationale— present on an edge only when a real Swift 6 escape hatch actually softens the compiler's own diagnostic at that boundary (SE-0337@preconcurrency, on the callee's own declaration, an ancestor up its class hierarchy, or the callee's module being@preconcurrency import-ed by the caller's file):structuralRiskkeeps the un-softened value (high), whileriskitself reports the real, softened one (medium), andseverityRationalenames which mechanism fired and why. Never fires for.medium/.lowedges — only a structuralnonisolated → isolatedboundary has a real compiler error to soften in the first place (see the caveat below).escapeHatches— every declaration in the project carrying an explicit, real Swift concurrency-checking escape hatch (@unchecked Sendable,nonisolated(unsafe),@preconcurrencyon a declaration/conformance,@preconcurrency import) — visible on its own even where it never triggers astructuralRiskdowngrade, so a team can see quietly-accumulating@unchecked Sendable/nonisolated(unsafe)usage directly..uncheckedSendable's ownisMutableis three-valued:true/falsewhen the conforming type's real stored-property state is known (including inherited from a same-project superclass),nilwhen it depends on an external/unresolved ancestor this tool has no member data for.summary— the numbers you'd put in a PR description or a migration-tracking spreadsheet today, by hand (highRiskBoundariesis the CI-gate number — see exit codes above).
Today's risk classification is structural, not syntactic: high means "a nonisolated
declaration has a call edge into actor/globalActor-isolated state," full stop — it does not
distinguish a call that already has a correct await protecting it from one that doesn't (or from
an @unchecked Sendable/nonisolated(unsafe) escape hatch). A perfectly correct, properly-awaited
hop like the example above and a genuine missing-await bug both show up as high, on purpose:
by the time your project compiles under Swift 6, every one of these edges is already await-ed or
explicitly unsafe somehow, and high exists to track migration debt — every place a nonisolated
context still reaches into isolated state — not just the subset that happens to be unguarded today.
Confirmed directly against this project's own real fixture matrix (docs/task-await-aware-risk- classification.md): downgrading an already-await-ed edge to low was tried and reverted, because
it stopped surfacing exactly the boundaries a migration effort most wants visible. high findings
are best read as "every place migration debt lives," not "every place there's an active
bug." The isAwaited field above gives you the await-presence signal directly, without the tool
making an incorrect claim about which shapes are risk-free. Distinguishing a real
@preconcurrency/@unchecked Sendable escape hatch specifically now ships (escapeHatches,
structuralRisk, severityRationale above, docs/task-escape-hatch-and-preconcurrency-severity.md)
-- @preconcurrency softens the compiler's own diagnostic (and this tool's risk) for a real
subset of high edges; @unchecked Sendable/nonisolated(unsafe) are surfaced as their own
findings but deliberately never soften risk itself, since neither one is a compiler-verified
safety guarantee the way @preconcurrency's SE-0337 downgrade is.
In practice, await-protected crossings are a small minority of high findings, not the bulk of
them — confirmed against a real, independent corpus (WordPress-iOS, WordPress scheme, 3208
Swift files): of 1486 high boundaries, only 42 (2.8%) have isAwaited: true. The other 97.2% are
exactly the unguarded crossings a migration effort needs to see first. If your own project's split
looks very different — await-protected crossings dominating the high count — that's a signal
worth acting on with isAwaited-based filtering downstream (a spreadsheet pivot, a jq query against
the JSON output), not a reason to distrust the classification itself.
The compiler already enforces every one of these boundaries correctly, one error at a time, as you touch each file. What it doesn't give you:
- The whole map, before you start. Instead of discovering isolation debt one compile error at a time during a migration, see every cross-actor boundary in the project at once, including ones in code you haven't touched yet.
- Third-party isolation, resolved for you. A huge fraction of real-world isolation facts live
in compiled dependencies you don't control — CocoaPods, XCFrameworks, SDK frameworks
(
UITableViewCell,NSView,SwiftUI.View...). Figuring out by hand which of your own types inherit@MainActorfrom a third-party superclass, across dozens of pods, is exactly the kind of bookkeeping this tool exists to do once, automatically, correctly (seedocs/research/for how much real work went into making that resolution trustworthy). - A CI gate, not just an editor squiggle. Exit code
1on any high-risk boundary means this slots into a pipeline today; adiff-based gate (v0.4) will let it fail a PR only on new risk, not the whole existing backlog. - A trackable migration-debt number, not a vague sense of "we should really finish this
someday" —
highRiskBoundariesin the JSON summary is one number you can put in a dashboard and watch move.
- CI gate. Fail the build on any high-risk boundary — exit code
1already does this on its own; add--severityif a stricter or looser threshold fits your pipeline better than the default (see Usage above). - A one-time audit before a Swift 6 migration. Run it once against the whole project to get
every
nonisolated→actor/@MainActorboundary at once, instead of discovering them one compile error at a time — including boundaries that only exist through a compiled dependency (CocoaPods, an XCFramework, an SDK type) that nobody would trace by hand. - Tracking migration debt over time. Run with
--output json --out-file report.jsonperiodically (a scheduled CI job, or once per release), commit the report, and watchsummary.highRiskBoundariesmove release to release — a real number for a dashboard or spreadsheet instead of "we should really finish this someday."
Compiler-synthesized declarations (default init(), deinit, rawValue/allCases accessors,
...) are structurally invisible to this tool's extraction pass. Declaration extraction is built
on SwiftSyntax, a lossless parse of exactly the source text in a file — nothing more, nothing
less. A declaration the compiler generates because none was hand-written (a memberwise initializer,
a default deinit, an enum's rawValue accessor) has no corresponding node anywhere in the parse
tree, so there is nothing for the extraction pass to visit in the first place. This is a structural
limitation, not a bug scoped to one code path — a real fix would mean independently
re-deriving the compiler's own synthesis-eligibility rules (which members get synthesized, and
under exactly which conditions) inside this tool, which risks introducing new, harder-to-verify
false positives/negatives for a class of declaration that isn't where undiscovered isolation risk
tends to hide in practice. See
docs/task-implicit-synthesized-declarations.md
(issue #55) for the full investigation, including a measured real-world scope (84% of one large
project's remaining unresolved declarations are this shape) and why call sites into these
declarations degrade safely to unspecified isolation rather than a false high.
A tool that gives an incorrect concurrency-safety result is worse than no tool at all. Every isolation-inference rule ships with an explicit test referencing the exact compiler behavior it verifies (tracked in docs/isolation-rules.md), and the tool refuses to run rather than silently produce a result it isn't confident in (stale index store, unrecognized Swift version, an oracle query that failed outright reports unknown, never a guessed nonisolated).
This tool reports actor isolation as the code actually compiles today — using each module's own real -swift-version from its real build arguments, never a hardcoded or assumed language mode. This matters because isolation semantics genuinely differ between language modes for some constructs (e.g. whether a class's synthesized zero-argument init() inherits its type's global-actor isolation depends on the language mode in effect — see SE-0411 — confirmed empirically, not assumed, against a real project's own build). Analysis results describe the project as it is built right now; they are not a prediction of what a future migration to a newer Swift language mode would report. If your build mixes modules on different -swift-version settings, each module's isolation is computed in its own mode, matching how the real build itself behaves.
The isolation-inference rules this tool implements are sourced directly from Swift Evolution
proposals (primary source of intent), cross-checked against the compiler's own source when a
proposal's text is ambiguous, and validated empirically against a real swiftc (see
docs/architecture.md §1.5.1 and docs/isolation-rules.md
for the full sourcing discipline and rule-by-rule citations). This is the list of proposals
actually checked and cited throughout this codebase — not a general reading list. No Apple
Technical Notes are cited anywhere in this project; none were found relevant to actor isolation
inference specifically.
Proposals that define or change the isolation model this tool implements:
| Proposal | Title | Swift version |
|---|---|---|
| SE-0306 | Actors | 5.5 |
| SE-0316 | Global actors | 5.5 |
| SE-0401 | Remove Actor Isolation Inference caused by Property Wrappers | 5.9 |
| SE-0411 | Isolated default value expressions | 5.10 |
| SE-0420 | Inheritance of actor isolation | 6.0 |
| SE-0449 | Allow nonisolated to prevent global actor inference |
6.1 |
| SE-0466 | Control default actor isolation inference | 6.2 |
| SE-0478 | File-level defaults | accepted, not yet shipped — confirmed empirically that its default: syntax doesn't compile on the local Swift 6.3 toolchain; review once it lands |
| SE-0518 | ~Sendable for explicitly marking non-Sendable types |
6.4 — not yet reviewed, see the runbook in docs/isolation-rules.md |
SE-0411 is worth calling out specifically: whether a class's synthesized zero-argument init()
inherits its type's global-actor isolation depends on the language mode in effect under this
proposal — the exact behavior this project's language-mode contract
exists to report correctly, confirmed the hard way against a real project's own build (see
docs/hypothesis-0-file-sorted-oracle-queries.md).
Proposals reviewed per Swift version and confirmed not to change isolation inference (kept as
the record of what was checked, not assumed — see docs/isolation-rules.md, "Rule set version
boundaries"): SE-0337, SE-0338, SE-0414, SE-0423, SE-0430, SE-0431, SE-0434, SE-0481.
See docs/glossary.md for every acronym, tool name, and project-specific term
used across this README and docs/ explained in one place, alphabetically — external, pre-existing
terms link to a real external reference; terms this project coined (like "the oracle") are marked
as such instead.
- v0.1 — shipped. Project/scheme resolution, index-store discovery and staleness detection, the hybrid inference engine, the external-isolation oracle (bulk + live),
mermaid/dot/jsonoutput, a file-sorted query-ordering optimization (~33% faster oracle phase on a real ~2200-file project, zero semantic change). - v0.2 — shipped. A direct
swift-build/SWBBuildServiceAPI path promoted to the default compiler-argument resolution for Xcode projects — faster and more correct than thexcodebuild -verbosepath it replaces, byte-for-byte edge-parity verified against two real 2000+-file corpora (thexcodebuild -verbosepath was kept for a while afterward as an unreachable-from-the-CLI fallback, then removed from the tree entirely once confirmed genuinely dead). Closure-level isolation attribution completed end to end: a real@globalActordeclared in a compiled dependency is now recognized, and the full de-isolating mirror direction (Task.detached, non-mainDispatchQueues,@concurrent) is implemented and real-corpus-verified. A real declaration-extraction bug fixed — locallet/var/nestedfuncs inside a function or closure body were being misattributed as phantom members of the enclosing type (22% of all declarations on one real ~2200-file corpus). Dozens of further real declaration/USR-matching correctness fixes across Objective-C/Swift interop edge cases (bridged extern constants, protocol witnesses, subscripts, multi-target declaration aliasing, and more), plus index-store scoping and DerivedData isolation hardening for shared/multi-run environments. A--sort=file|severityflag was added afterward (0.2.1) to order the output's edges by location or by risk instead of leaving them in whatever order the analysis happened to produce them. - v0.3 — shipped. Real Swift 6 escape hatches (
@unchecked Sendable,nonisolated(unsafe),@preconcurrencyon a declaration/conformance/import) are now surfaced as their ownescapeHatchesfindings, and a real SE-0337 diagnostic softening (@preconcurrencyon a callee's own declaration, an ancestor up its class hierarchy, or its module being@preconcurrency import-ed) downgrades that edge'sriskfor real, withstructuralRisk/severityRationalekeeping the "why" traceable — including.uncheckedSendable's own real mutable-stored-property detection, walking a type's members and superclass chain. A new--platform <name>flag picks among several simultaneously-valid Simulator destinations on one modern multiplatform scheme (iOS + visionOS on the same scheme, confirmed against a real app); compiler-argument resolution for Xcode projects generalized beyond iOS Simulator to the other three real Simulator SDK families (tvOS/watchOS/visionOS), verified against four independent real corpora.#if <name>custom build conditions are now resolved from the real, active compiler arguments instead of a hardcodedtrue, and a real#if targetEnvironment(macCatalyst)misclassification (backwards-answered) is fixed. Totalsourcekitdunavailability is now a hard failure instead of a silent, incomplete report; the SwiftPM compiler-argument path's own run-to-run non-determinism (an incrementalswift build -vsilently omitting compile lines) is fixed with a real-file-count-scaled retry. Several further real declaration/USR-matching and stderr-noise correctness fixes shipped along the way (anNSDictionary/NSMutableDictionaryNSCopying-keyed subscript,MKCoordinateRegion.center's typealias-wrapped setter, two distinct compiler-argument stderr-noise root causes, aDeclarationLinkermerge tie-break for stray duplicate source files). - v0.4 — not started.
diffsubcommand, a GitHub Action that comments on PRs when a new cross-actor boundary appears, a migration-debt map, packaged distribution (Homebrew, possibly an SPM build-tool plugin). (Per-call-site suppression comments were designed —docs/task-suppression-comments.md— and then decided against; not planned.) - v0.5 — not started. Revisit staleness-detection strategy, deeper cross-module accuracy, possibly rewrite suggestions.
See CONTRIBUTING.md for building from source, running the test suite, using
Xcode, and where to start reading in docs/.
MIT — see LICENSE.