This is a live bug, not leftover damage from before the July cleanup. Fresh contamination was
written on 8 August and 10 August 2026 — after the 26 July cleanup and after the
5cbf9ef scoping fix. The July fix addressed only one of the paths.
The grayed-out SnapSparrow rows in Colonym are not blueprints. They are subsystem definitions — entries in
Colonym’s custom-subsystems.json — and they render gray because Colonym has no blueprint content for them.
BlueprintsView.swift:607 renders any definition without a blueprint as “Not generated” in
.cmTextTertiary. So the confusing gray state is the UI correctly reporting an incorrect list.
Colonym’s definition file holds 18 entries. The first nine are byte-identical to SnapSparrow’s entire list, in the same order, followed by Colonym’s own nine game subsystems:
SnapSparrow : design-system, data-model, layout-engine, export-pipeline, annotations,
compose-workspace, capture-input, app-shell, prototyping-docs
Colonym : design-system, data-model, layout-engine, export-pipeline, annotations,
compose-workspace, capture-input, app-shell, prototyping-docs, ← SnapSparrow's, verbatim
game-core, rendering-engine, sprite-pipeline, player-hud,
crafting-building, world-generation, npc-ai-dialogue,
progression-onboarding, dev-tooling-runtime ← Colonym's own
That foreign list as an exact prefix, own entries appended after shape is the fingerprint of an in-memory array that was never cleared when the project changed. It repeats across the portfolio: Hopper’s first twelve entries are CodeRaven’s twelve, exactly; ToWatch and LochLustra contain nothing but CodeRaven subsystems.
BlueprintStore is a singleton whose caches are global, not keyed by project, while every
write API takes projectId as an independent parameter. Nothing checks that the two agree, so the
store cheerfully flushes whichever project’s content is in memory into whichever directory it is handed.
// BlueprintStore.swift:46-49
@Published var blueprints: [String: ArchitectureBlueprint] = [:] // ← not keyed by project
@Published var customSubsystems: [SubsystemDefinition] = [] // ← not keyed by project
@Published private(set) var loadedProjectId: UUID?
persistCustomSubsystems encodes the entire in-memory array into the target project’s file:
// BlueprintStore.swift:374-404
func saveCustomSubsystem(_ definition: SubsystemDefinition, projectId: UUID) {
...
customSubsystems.append(definition) // ← mutates the GLOBAL array
persistCustomSubsystems(projectId: projectId)
}
private func persistCustomSubsystems(projectId: UUID) {
let data = try encoder.encode(customSubsystems) // ← whole global array
try data.write(to: customSubsystemsURL(for: projectId), options: .atomic) // ← foreign dir
}
When discovery runs for Colonym while the array still holds SnapSparrow’s entries, each discovered subsystem is
appended and the whole contaminated array is written to Colonym’s file. persistScanHistory has the same
shape — which is why Colonym’s and Keep Sparrow’s scan histories both list PressRaven’s candidate directories.
The maintenance agent reads the global cache and writes it under a caller-supplied project id:
// BlueprintMaintenanceAgent.swift:397-409
private func applyStatusChanges(_ results: [BlueprintStalenessResult], projectId: UUID) {
for result in results {
guard result.recommendedStatus != result.currentStatus else { continue }
if var bp = store.blueprint(for: result.subsystemId) { // ← whatever project is LOADED
bp.status = result.recommendedStatus
store.saveBlueprint(bp, projectId: projectId) // ← a DIFFERENT project's directory
}
}
}
Against a foreign projectPath the code fingerprint can never match, so the detector marks everything
stale, and the status flip fires the write. I verified this signature on disk rather than inferring it:
game-core.json share one blueprint UUID — D4DCA25D-33AF-445A-8791-887B230A40CD
— one generatedAt, version: 1, generationSource: aiGenerated.history/game-core/v1.json differs from its live copy by
exactly one line: "status": "current" → "stale".A regeneration would have produced a new UUID, a new timestamp, version: 2, and
generationSource: agentUpdated. It didn’t. This is the status-flip path copying one project’s blueprint
into another’s folder.
projectId and loadedProjectId divergeloadBlueprints sets loadedProjectId synchronously (:135)
but assigns the caches only when the detached load returns (:193). For the duration of
every load the store claims project B while holding project A’s data.currentProject ?? projects.first — an arbitrary fallback unrelated to what
the store has loaded.FeatureClusterGenerator re-points the shared singleton at other projects on a debounced refresh.| Project | Foreign blueprint files | Foreign subsystem defs | Other |
|---|---|---|---|
| Colonym | 4 CodeRaven blueprints — cli-bridge (v43), ideation-studio (v24),
testing-sessions (v22), mockup-studio |
9 from SnapSparrow your report | scan history from PressRaven |
| CodeRaven | 9 Colonym game blueprints + archived v1s | clean | — |
| NoteSparrow | 9 Colonym game blueprints + archived v1s | clean | — |
| Hopper | none | 12 from CodeRaven (its own 4 follow) | — |
| ToWatch | none | 4 from CodeRaven — all it has | — |
| LochLustra | none | 5 from CodeRaven — all it has | — |
| Keep Sparrow | none | clean | scan history from PressRaven |
The leak is bidirectional — Colonym both received CodeRaven’s blueprints and donated its own to CodeRaven and NoteSparrow — which rules out any theory involving one “poisoned” project.
The obvious fix — assert projectId == loadedProjectId inside saveBlueprint — would
break BlueprintBridgeImporter, which by design iterates every project and writes to all
of them while the store is loaded on just one. A blanket guard would silently drop legitimate imports. The fix has to
remove the global-cache reads from the write paths, not gate the writes.
Four changes, all at the same seam — stop reading singleton state inside functions that take an explicit
projectId:
saveCustomSubsystem — read knownSubsystems(for: projectId) from disk,
merge the one definition, write that back. Never touch the global array except when
projectId == loadedProjectId. Same for deleteCustomSubsystem and
persistScanHistory.saveBlueprint — read the existing blueprint to archive from
projectId’s directory on disk (:216), not from
blueprints[...]; update the in-memory dict only when the ids match.applyStatusChanges and the staleness detector — operate on blueprints loaded for
projectId, and bail early when loadedProjectId != projectId rather than falling back to
projects.first.loadBlueprints — clear the caches on the abandoned-load branch
(:192) instead of returning with stale data in place.The structural version is to key blueprints and customSubsystems by project id, the way
knownSubsystems(for:) already does. That is the durable answer; the four targeted changes above close every
observed leak without a broad refactor.
While this report was being written, the running build rewrote Colonym’s scan-history.json at
02:37 with Keep Sparrow’s directories — the same file had held PressRaven’s earlier
the same evening. Direct observation of the leak firing, independent of the forensic argument above.
Every site that receives an explicit projectId now resolves its data from that project instead
of the singleton caches:
saveBlueprint archives the previous version read from the target project’s directory; the cache is
updated only when it is the loaded project. deleteBlueprint likewise.saveCustomSubsystem / deleteCustomSubsystem read the target’s list from disk, merge, and
write it back; persistCustomSubsystems now takes the list explicitly rather than flushing the global array.recordScan builds the record from its arguments; persistScanHistory takes it explicitly.detectAllStaleness reads the named project’s subsystems and blueprints from disk.applyStatusChanges reads via storedBlueprint(subsystemId:projectId:); the shared
stalenessDetails cache is written only when the project coincides.detectUncoveredAreas takes a projectId and records the scan against the project it
actually scanned — previously it scanned one project’s path and recorded under loadedProjectId.BlueprintBridgeImporter takes the version number and preserved feedback from the project being imported.loadBlueprints clears the caches synchronously on project switch, closing the window between
assigning loadedProjectId and the background load returning.New disk-backed companions to knownSubsystems(for:): storedBlueprint(subsystemId:projectId:),
storedBlueprints(for:), storedScanHistory(for:). Build passes.
No projectId == loadedProjectId assertion was added to saveBlueprint. The bridge importer
legitimately writes to every project while the store is loaded on one, so that guard would have silently dropped valid
imports. The leak is closed by removing the cache reads, not by gating the writes.
Backup taken first; the store is also under git in Application Support, so every state is recoverable.
| Removed | Count | Detail |
|---|---|---|
| Foreign live blueprints | 25 | CodeRaven 10, NoteSparrow 9, Colonym 4, PressRaven 2 |
| Their history dirs | 25 | archived versions of the same files |
| Foreign subsystem definitions | 30 | Colonym −9, Hopper −12, ToWatch −4, LochLustra −5 |
| Misattributed scan histories | 2 | Colonym, Keep Sparrow |
Post-cleanup audit: 0 foreign live blueprints, 0 duplicated blueprint files across projects, and every project’s subsystem list now contains only its own. verified
Two follow-ups. ToWatch and LochLustra now have empty subsystem lists — everything they held was CodeRaven’s — so both need a discovery run before they will generate anything. And the 241 dormant foreign history directories were left in place by choice; they are only read when opening a blueprint’s version history, and the inventory in §3 stands if you want them purged later.
What is certain: the leak is live; both write paths are identified with file and line references; the contamination inventory was derived from the files themselves, and the status-flip signature was verified by diff rather than inferred.
What is not: I have not reproduced the leak in a running app — the evidence is forensic
(file contents, timestamps, git history of the store). The exact interleaving that made loadedProjectId
diverge on 8 August is reconstructed, not observed. That affects the story, not the defect: the write paths
read singleton state under a foreign project id regardless of how the divergence arises.
Where this could be inadequate: if some other component also flushes store state, fixing these four sites would reduce but not eliminate the leak. The audit is repeatable, so a post-fix re-run settles it.
Confidence: 92 / 100 — high on the diagnosis and the contamination inventory, which are backed by on-disk proof; the deduction is capped short of certainty only by the absence of a live reproduction.