Implementation Plan: Silent-Write Hardening Residuals
Branch: fix/silent-write-hardening-residuals | Date: 2026-09-23 | Spec: spec.md Input: Feature specification from kitty-specs/silent-write-hardening-residuals-01M37QN4/spec.md
Summary
Close the three residual silent-write weaknesses that PR #4938's landing-pass squad confirmed still-live on current main, and close #4993 (epic #2720):
- A (P1) — Invert
doctor mission-state --fix's non-lane preservation from a registry allowlist to preserve-by-default + explicit denylist, so an unregistered future authoritativeevent_type(which the durable reader already preserves) is never silently quarantined. Extend the_registry_authoritative_quarantine_violationsfail-closed guard to cover non-registry authoritative rows. Ship behind an ADR because it changes what--fixprunes. - B (P2) — Consolidate the two divergent
charter.yamlcatalog.missionreaders (ruamel round-trip vsYAML(typ="safe")) onto one shared accessor delegating to the canonical charter loader. - C (P3) — Harden the traces merge driver's 3-way base-aware stale-drop path: recognize
~~~fences equivalently to backtick fences, and give blocks a non-colliding key so duplicate headings do not collide. No broad contract refactor.
Technical approach = collapse the reader/repair contract into one rule (A), one canonical read path (B), and two bounded correctness edges (C). All three are independent seams (no shared guard — see C-001).
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: ruamel.yaml (already present — canonical charter loader), Typer/Rich (CLI, unchanged); no new dependencies (spec C-004) Storage: append-only status.events.jsonl (authoritative mission event log); charter.yaml (project governance config); traces/<category>.md (mission trace artifacts) Testing: pytest (+ existing red-first regression suites under tests/status/, tests//test_mission_state_repair.py, tests//merge_driver*, charter reader tests) Target Platform: Linux/macOS/Windows CLI (spec-kitty) Project Type: single project (src/ + tests/) Performance Goals: no perf regression; repair/merge-driver are batch/interactive, not hot-path Constraints: fail-closed toward retention (spec C-003); byte-faithful traces merge (NFR-004); no asset_preservation routing (C-001); no traces-contract refactor (C-002); keep the #4938-folded duplicate-event_id guard fix intact (SC-005) Scale/Scope: 3 findings, ~6 source files, 1 new ADR, 1 shared accessor; bounded surgical change set
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
- Single canonical authority — Finding A collapses the reader's permissive contract and the repair's preserve contract into ONE rule powered by the existing
status/lifecycle_events.pyauthority; Finding B collapses twocatalog.missionreaders into ONE accessor. Both move toward canonical sources (DIRECTIVE_001). ✅ - Decision documentation (DIRECTIVE_003) — Finding A's behavioral change to
--fixpruning is recorded in a new Accepted ADR underdocs/adr/3.x/(FR-003). ✅ - DDD + tiered rigour — status event-log repair and merge semantics are high-rigour (data-loss adjacent) → red-first tests + fail-closed guard; charter reader and traces edges are moderate. ✅
- ATDD-first / red-first — each finding ships with a test that fails against the pre-fix code and passes after (spec Independent Tests; SC-001/SC-003). ✅
- Locality of change (DIRECTIVE_024) — three bounded seams; no cross-cutting refactor (C-002 fences the traces driver). ✅
- Campsite (DIRECTIVE_025) — folds in the adjacent B + C hardening the squad surfaced while the context is loaded; corrects #4993's reader count. ✅
- Terminology canon — Mission vocabulary only (C-005). ✅
No violations to justify → Complexity Tracking empty.
Project Structure
Documentation (this mission)
kitty-specs/silent-write-hardening-residuals-01M37QN4/
├── plan.md # This file
├── research.md # Phase 0 output (denylist determination, canonical loader, seam recipes)
├── data-model.md # Phase 1 output (event classification model, accessor contract)
├── quickstart.md # Phase 1 output (how to verify each finding)
├── contracts/ # Phase 1 output (repair-preservation contract, accessor contract, traces-merge contract)
└── tasks.md # Phase 2 output (/spec-kitty.tasks — NOT created here)
Source Code (repository root)
src/specify_cli/
├── status/
│ ├── store.py # is_non_lane_event (reader) — reference contract for A (read; likely unchanged)
│ └── lifecycle_events.py # AUTHORITATIVE_NON_LANE_EVENT_TYPES registry + predicate (A: guard authority)
├── migration/
│ └── mission_state.py # _is_preserved_non_lane_row, _canonicalize_status_row,
│ # _registry_authoritative_quarantine_violations (A: invert + extend guard)
├── cli/commands/
│ ├── charter/generate.py # _read_catalog_mission_from_charter_yaml (B: delegate to shared accessor)
│ └── merge_driver.py # _TRACE_FENCE_MARKER, _split_trace_blocks, _trace_block_key,
│ # _drop_stale_theirs_trace_blocks (C: tilde fence + block-key)
└── charter_runtime/preflight/
└── references_refresh.py # _read_catalog_mission_and_template_set (B: delegate to shared accessor)
# New shared accessor home: TBD in Phase 0 (Q2) — a module reachable from both cli.commands.charter
# and charter_runtime without a layer violation; likely alongside the canonical charter loader.
docs/adr/3.x/
└── 2026-09-23-N-mission-state-repair-preserve-by-default.md # A: ADR (FR-003)
tests/
├── status/ # non-lane registry + repair tests (A)
├── (mission_state repair test) # extend red-first (A)
├── (charter reader tests) # accessor parity (B)
└── (merge_driver traces tests) # tilde fence + duplicate heading (C)
Structure Decision: single-project layout; changes are surgical edits to the seams above plus one shared accessor module and one ADR. No new top-level packages.
Complexity Tracking
No Constitution Check violations — section intentionally empty.
Parallel Work Analysis
The three findings are independent seams that touch disjoint files, so they parallelize cleanly after a shared research base.
Dependency Graph
Phase 0 research (denylist + accessor home + seam recipes)
│
├── Finding A: invert repair + extend guard + ADR (status/, migration/) [largest]
├── Finding B: single catalog.mission accessor (charter/, charter_runtime/)
└── Finding C: traces-driver hardening (merge_driver.py)
│
└── Tracker close-out: #4993 close + reader-count correction (docs/changelog)
Work Distribution
- Sequential (shared base): Phase 0 research settles the denylist (A) and the accessor home (B) before implementation lanes open.
- Parallel streams: A, B, C touch disjoint files → three lanes with no file contention.
- Lane ownership: A ⇒
status/lifecycle_events.py,migration/mission_state.py(+status/store.pyread-only reference), ADR; B ⇒charter/generate.py,charter_runtime/preflight/references_refresh.py, new accessor module; C ⇒cli/commands/merge_driver.py.
Coordination Points
- Shared surfaces (landing-time regen, not lane contention):
docs/changelog/CHANGELOG.md,docs/api/cli-commands.md(only if a help string changes),src/specify_cli/_completion_manifest.json— regenerate against current main at finalize/landing. - Concurrent PR #4950 alignment: verified disjoint source + zero semantic coupling; if both edit
docs/*/manifest, resolve as an ordinary rebase merge (per operator: nodoctor.pyfence needed). - Integration test: full non-lane preservation + duplicate-
event_iddedup suite green (SC-005), proving A changed the default without regressing preserved classes.