CODERAVEN · PLAN · REVIEW COPY

Silent-Failure Sweep

Nine actions picked from the Jul 27 – Aug 14 dispatch, all one bug class: code that reported success while doing nothing, or did it to the wrong target.

54Tasks done
9Phases
30Files touched
3Commits
The gist

You picked nine actions out of the dispatch. They looked like nine unrelated bugs and were not — every one is the same shape, something reporting success while doing nothing, or doing it to the wrong target. So Phase 0 came first: a script that verifies the built artifact instead of the exit code, because eight more builds were about to be trusted and that trust had already failed twice.

It caught a real state on its very first run. All nine are implemented and pushed across three commits; nothing has been observed at runtime.

The problem

951 Chronicle records rested on two try? calls, and the destructive path was armed — the hot file had crossed the 150-record threshold that runs it. A lost ~100-byte sidecar made the whole archive unreachable; an undecodable archive turned 951 records into an empty array that was then written back atomically and the sidecar rewritten to match, erasing the evidence.

Alongside it: the previous day's blueprint scoping fix had silently broken a caller that read the cache it now clears asynchronously; four directory scanners decoded into types where every field defaults, so they could not reject a foreign file; Chronicle aggregates computed over 169 of 1,120 records while presenting as period summaries; one bridge import fired ~16 @Published publishes, each making every widget re-sort its full array; and ProjectStatus.color returned two colours that were byte-identical to semantic ones, via aliases that shadowed SwiftUI's own members.

Approach

Verify the artifact before trusting any of the other eight

Phase 0 resolves the product from -showBuildSettings, then asserts it exists, that its mtime advanced, and that it postdates every modified .swift file. It stats the newest file anywhere in the bundle — measured on this machine, the .app directory was 1.6 days stale and Info.plist 12 hours stale on a build that was genuinely current, and this is a debug-dylib config so the 58 KB executable is not the witness either.

Prove the check can fail before relying on it

A verification that cannot fail is the bug being fixed. All three paths were exercised: no product (Release, unbuilt), product older than modified source (temporary edit), and compiled-but-not-running — which fired naturally on the first real build.

Positive identity checks, not filename denylists

A denylist catches today's sidecars and nothing else. Where a store writes <id>.json, requiring the decoded id to equal the file stem is invariant by construction and free. Applied to blueprints, journeys, mockup sessions and specs.

Fix the trigger, then the multiplier

Batching the importer's task writes into one transaction cuts the publishes; memoizing the widgets cuts what each publish costs. Doing only one of the two leaves the other dominant.

Decisions made for you

Scope changed

The colour aliases were replaced, not deleted outright

Deleting Color.indigo as literally specified would have repointed ~95 call sites at SwiftUI's system indigo, and replacing them with cmPurple (#9B7BB5) would have repainted most of the Foundation editors from bright indigo to muted lavender. That is a design decision, not a bug fix, and not one I can verify without running the app. Instead a non-shadowing cmIndigoAccent was introduced at the same hex, all sites migrated, then the shadowing aliases deleted. Shadowing is gone, appearance is unchanged, and whether that bright value should join the muted palette is still yours to decide.

Judgment call

Two palette collisions were left open and named

ProjectStatus is now disjoint from the semantic set — which is where the misreading happens — but .planning still shares #D4915C with the gold slot and .development shares #6366f1 with indigo. Closing those needs a project palette guaranteed disjoint from everything else. Documented at the call site rather than papered over.

Judgment call

Two dead blueprint APIs deleted rather than gated

loadCustomSubsystems(for:) and loadScanHistory(for:) assigned shared caches from an arbitrary project's file and had zero callers. A public API that can put one project's data into another's working set is a trap even when unused.

Judgment call

The archival pass now aborts rather than repairing

On an unreadable archive it leaves the hot file untouched and moves nothing, so records stay hot until the archive is fixed. Preferred over any automatic repair: silently rewriting a file you could not read is how the original bug worked.

The work

Phase 0 — Verify the artifact, not the exit code (a30)

9/9 done Built first on purpose — it is what proves the other eight shipped.
  • Create scripts/verify-build-product.sh — resolve the product from xcodebuild -showBuildSettings -json (BUILT_PRODUCTS_DIR + FULL_PRODUCT_NAME), never a hardcoded DerivedData hash
  • Record the newest mtime of ANY file inside the .app before the build — not the .app dir and not Info.plist, both of which go stale on incremental relinks
  • Fail on: nonzero exit; no product on disk; mtime did not advance; product older than any modified tracked .swift file
  • Add a distinct exit code for "compiled but not live" — compare the running process start time (pgrep -x + ps -o lstart=) against the binary mtime
  • Downgrade the unchanged-mtime case to a warning so a clean-tree no-op rebuild does not false-fail
  • Rewrite step 4 of Code Raven/Resources/Commands/cr-complete.md to run the script and set buildSucceeded from its RESULT: line — edit the BUNDLED copy, not ~/.claude/commands/
  • Add an explicit UNVERIFIED state to the terminal summary's Build line (line 234)
  • Point .claude/commands.json's "Verify Build" command at the script
  • Run the script against the current tree and show the output

Phase 1 — Close the two Chronicle archival holes (a1)

6/6 done Highest consequence. The destructive path was armed when this started.
  • Hole 1a: archivedRecordCount(for:) (:177-182) must fall back to a real count when archive-meta.json is missing but archive.json exists on disk
  • Hole 1b: ensureArchiveLoaded (:507-517) must NOT insert into archiveLoadedProjects when the decode failed, and must surface the failure
  • Hole 2a: runArchivalMaintenance (:552-590) must distinguish "file absent" from "present but undecodable" and ABORT the pass in the latter case
  • Hole 2b: refuse to write when existing.count == 0 but archiveMeta reports a non-zero count
  • Write a .bak of archive.json before the first overwrite of each run (the existing backup at :555-558 covers the HOT file only and never refreshes)
  • Build and verify the artifact with the Phase 0 script

Phase 2 — Repair the caller the blueprint fix broke (a3)

3/3 done A regression introduced by the previous day's fix.
  • FeatureClusterGenerator.loadBlueprints(for:) (:455-461) → use store.storedBlueprints(for:), and stop touching loadedProjectId
  • Sweep every other caller that invokes loadBlueprints(for:) then reads blueprints/customSubsystems/scanHistory in the same synchronous scope; report findings even where no change is made
  • Build and verify the artifact

Phase 3 — Close the three remaining blueprint leak paths (a4)

7/7 done Same subsystem as Phase 2; done in one sitting.
  • markAsVerified(subsystemId:projectId:) (:343-349) → resolve via storedBlueprint(subsystemId:projectId:)
  • loadCustomSubsystems(for:) (:426) and loadScanHistory(for:) (:547) — delete, or gate the cache assignment on loadedProjectId == projectId
  • loadVersionHistory(projectId:subsystemId:) (:383-389) — add the sidecar filter and empty-id rejection the other two loaders got
  • BlueprintBridgeImporter.processFile(_:project:) (:217-313) — delete the dead duplicate, or fix it identically
  • Make BlueprintSubsystemMigration (:54-55) reference BlueprintStore.sidecarFilenames instead of its own hand-copied denylist
  • Replace the denylist guard with a positive identity check: decoded subsystemId must equal the file stem
  • Build and verify the artifact

Phase 4 — Give five directory scanners a way to say no (a5)

6/6 done Four of five hardened. The fifth was a deliberate no — see Out of scope.
  • CreativeJourneyStore.swift:589 — CreativeJourney defaults everything including id
  • MockupSessionPersistence.swift:94 — MockupSession defaults id and projectId
  • ImplementationSpecStore.swift:67 — files are <sessionId>-spec.json but the stem is never checked
  • CLIUsageBridgeImporter.swift:61 — every field defaults; guard bridge.version == 1 at :120 is vacuous
  • FlowMapScanner.swift:128 — accepts any .html in the scanned folder
  • Build and verify the artifact

Phase 5 — Make Chronicle rollups archive-aware (a6)

7/7 done Aggregates were reporting 15% of history as if it were all of it.
  • generateRollup (:295-302) — add includeArchived, thread fullHistoryLoaded from ChronicleView.swift:658-661
  • availableClusters (:285-290) — archive-aware, so a cluster present only in archived records is selectable
  • ChronicleView.swift:830-843 — allProjectRecords / archivedCount / uniqueGroupCount, and the mismatched subtitle at :846
  • Decide and handle lastPatternAnalysisDate (:979-984) and LivingGuideEvaluator.swift:84-88
  • Leave DispatchDigestGate.swift:82-84 hot-only — it is correct and documented as such
  • Where loading the archive is too expensive, change the LABEL rather than report a partial figure silently
  • Build and verify the artifact

Phase 6 — Stop the post-import render storm (a7)

7/7 done The trigger and the multiplier, fixed together.
  • Add batch-write APIs so each importer assigns once: PlanTaskStore, ChronicleManager.addRecord, PlanSnapshotManager.updateSnapshot, ScratchpadManager.createNote, ToDoStore.upsertFromBridge, QuickTaskManager.createTask
  • Extend beginBatchCapture()/endBatchCapture() (:52-80) to cover the @Published assignment, or add a parallel mechanism — today it defers disk writes only and no importer calls it
  • Convert CheckinBridgeImporter (:198-250), CompletionBridgeImporter (:278, :395), SummaryBridgeImporter (:241, :347, :354, :365)
  • Memoize NotesWidget (:206-230) — four uncached properties over ~310 notes, ~16 passes per render
  • Same treatment for ToDosWidget (:31-39) and TasksWidget (:259-267)
  • Memoize ChronicleView.baseFilteredRecords (:855-872) — currently ~15-20 full search passes per render, ~9× worse after Load full history
  • Build and verify the artifact

Phase 7 — Make To-Do and Quick Task writes atomic (a8)

4/4 done The only correctness item in the performance list.
  • ToDoStore — async load, Task.detached + options: .atomic on write (:218, :236, :255)
  • QuickTaskManager — same (:1080, :1099, :1125)
  • Make a decode failure on load distinguishable from an empty file in both stores
  • Build and verify the artifact

Phase 8 — Delete the shadowing color aliases (a24)

5/5 done Scope changed on contact — see Decisions.
  • Make ProjectStatus.color (Project.swift:152-162) token-based throughout
  • Delete Color.teal and Color.indigo from Colors.swift (:288, :294) and fix every call site
  • Fix the two-greens-side-by-side cases: ProjectIntelligenceDashboard.swift:1512, ProgressMetricsSection.swift:441, ActiveAgentsSection.swift:122
  • Report on azure500 → cmOcean (equals cmInfo and agentRunning) and let Sarah decide
  • Build and verify the artifact

Manual testing

4 tasks None of this was observed running — GUI verification is blocked from the terminal.
  • Relaunch and confirm Chronicle still lists records and "Load full history" still appearsyou
  • Generate a blueprint in a non-selected project and confirm feature clusters carry real subsystem contextyou
  • Run a /cr-checkin and confirm the overview no longer hitches while the import landsyou
  • Look at the sidebar and the project cards after the color change — Maintenance and Beta chips especiallyyou

Risks & edge cases

Needs your eyes

Nothing here was observed running

Every phase is compile-verified via the new script. GUI verification is blocked from the terminal on this machine, so the four manual checks above are the only real proof.

Highest risk

The Chronicle timeline memoization can go stale

It now recomputes on explicit triggers rather than on every render. Every input has one and the comment says so — but the failure mode of a missed input is a timeline that quietly stops tracking a filter. Worth exercising each filter once.

Behaviour change

Stores now refuse to save after a failed load

ToDoStore and QuickTaskManager skip writes for a project whose file failed to decode. Correct — it prevents overwriting data you could not read — but it means edits in that state are silently not persisted until the app restarts. It logs, and nothing surfaces in the UI.

Edge case

CLIUsageBridge now rejects files missing version or type

All 933 files on disk carry both, and every writer emits them, so this should be inert. A hand-written usage bridge would now be refused rather than silently defaulted.

Out of scope

FlowMapScanner was deliberately left permissive

It has the same shape as the phantom-record bug — scan a directory, accept what is in it — but not the same consequence, and the obvious hardening makes things worse. There is no identity to verify; hand-authored guides are explicitly supported; and I checked the real artifacts: requiring a <title> would drop four legitimate mockups, and requiring a generated marker would drop every guide. The failure mode is a stray file rendering as a gallery row with its filename as the title — visible and self-correcting. The reasoning is recorded at the call site.

azure500 → cmOcean reported, not changed

It is byte-identical to both cmInfo and agentRunning, so an azure project's session bar reads like a running-agent badge. Left for you: unlike the status-enum collisions this one is a palette decision.

Final review

What this does well. Phase 0 earned its place immediately — it reported NOT LIVE on the first real build, which is exactly the 14 August failure caught in seconds instead of a day. The archival fixes close a live data-loss path rather than a theoretical one. And the sweep in Phase 2 was worth more than the fix it came from: looking for other callers of the same shape turned up two more, one of them a computed property re-triggering a load on every render.

Where it is weakest. The Chronicle memoization is the piece I would most want to see running — it is the largest behavioural change in the batch and its failure mode is quiet. The colour migration touched 25 files mechanically; the hexes are unchanged so it should be invisible, but "should be" is doing work there. And Phase 6's batching changed the shape of two importers that write task state, which is the data you most rely on being right.

What I would adjust. If the memoization proves fiddly, the honest fallback is to keep the batching and revert the ChronicleView cache alone — the trigger fix carries most of the win and none of the staleness risk.

82/100

High on the data-integrity work — those are small, well-understood changes closing paths whose failure I could reproduce. Lower on the two big mechanical passes: the colour migration and the Chronicle memoization are both correct as written and both would fail quietly if they are not. The score is capped by the fact that nothing was seen running.