Code Raven · Diagnostic

Blueprint cross-project leak

13 August 2026 · triggered by “Colonym is showing SnapSparrow’s blueprints, grayed out”
Verdict

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.

2
independent write paths leaking
6
projects holding foreign data
31
misplaced blueprint files
30
foreign subsystem definitions

1. What you actually saw

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.

2. Root cause

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?

Path A — leaked subsystem definitions (your grayed-out rows)

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.

Path B — leaked blueprint content

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:

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.

Why projectId and loadedProjectId diverge

3. Current contamination

ProjectForeign blueprint filesForeign subsystem defsOther
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—
Hoppernone 12 from CodeRaven (its own 4 follow)—
ToWatchnone 4 from CodeRaven — all it has—
LochLustranone 5 from CodeRaven — all it has—
Keep Sparrownoneclean 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.

4. Recommended fix

Important caveat

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:

  1. 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.
  2. 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.
  3. applyStatusChanges and the staleness detector — operate on blueprints loaded for projectId, and bail early when loadedProjectId != projectId rather than falling back to projects.first.
  4. 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.

5. Resolution — applied 13 Aug 2026

Caught live

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.

Code — 4 files, 8 sites

Every site that receives an explicit projectId now resolves its data from that project instead of the singleton caches:

New disk-backed companions to knownSubsystems(for:): storedBlueprint(subsystemId:projectId:), storedBlueprints(for:), storedScanHistory(for:). Build passes.

Deliberately not done

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.

Data — visible layer cleaned

Backup taken first; the store is also under git in Application Support, so every state is recoverable.

RemovedCountDetail
Foreign live blueprints25 CodeRaven 10, NoteSparrow 9, Colonym 4, PressRaven 2
Their history dirs25archived versions of the same files
Foreign subsystem definitions30 Colonym −9, Hopper −12, ToWatch −4, LochLustra −5
Misattributed scan histories2Colonym, 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.

Review

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.