Research — Silent-Write Hardening Residuals
Grounding: PR #4938 landing-pass second-opinion squad (correctness, design/authority, SSOT/whack-a-field), verified still-live on main @ 138d25dd0f. Prior mission grounding: kitty-specs/silent-destructive-write-hardening-01M355VK/research.md. No dependency changes → Supply-Chain Security / Adversarial Evidence section: N/A (no added/upgraded/removed dependency; ruamel.yaml is already present).
Operator Decision Moment DM-01M37QQ62T11G9JPZQJE2GYSYH: ship the full Finding-A inversion this mission, gated by an ADR.
Finding A — mission-state repair: preserve-by-default (denylist = EMPTY)
- Decision: Invert
migration/mission_state.py::_is_preserved_non_lane_rowfrom a registry allowlist to preserve-by-default: preserve any row the durable readerstatus/store.py::is_non_lane_eventtreats as non-lane (annotation branch, retrospectiveevent_name, retrospective type-envelope, and — the new behavior — any row carrying anevent_type). Prune only an explicit denylist of known-prunable mirrorevent_types. That denylist is EMPTY. - Rationale: An empirical writer census of
status.events.jsonlshows everyevent_typepresent is either (a) a lane transition — modern flat rows carry noevent_type; the legacy typedWPStatusChangedrow is owned by the lane reducer — or (b) an authoritative canonical record (lifecycle / DecisionPoint / retrospective) whose sole per-mission home is this log. Derived folds (decisions/index.json) are separate files rebuilt FROM the log (decisions/index_fold.py::fold_events), never rows inside it;review_result(#3541) is a nested field on a laneStatusEvent, not a standalone row. So noevent_type-bearing row is a prunable mirror — the safe rule is "preserve everything the reader preserves", which collapses the reader's permissive contract and the repair's preserve contract into one rule and closes the whack-a-field class (#2376 → #3066 → #3541 → #4897 → the unregistered-future case) structurally. - Guardrail (must not regress #3066): Keep the
_is_legacy_typed_lane_transitionpassthrough (WPStatusChanged+ top-levelwp_id/from_lane/to_lane) FIRST, before preserve-by-default, so a legacy typed lane row is canonicalized intostatus.json(event_type stripped) rather than preserved verbatim.WPStatusChangedis a lane row, not a prunable mirror; the denylist does not name it because it never reaches the non-lane path. - Quarantine after the inversion: only rows with no
event_typethat are genuinely non-status. The existing"event_name" in rowquarantine branch stays (a non-retrospectiveevent_name-only row is not non-lane to the reader either — it flows toStatusEvent.from_dictand fails loud per FR-010). These are "genuinely non-status", not a mirror denylist. - Guard extension (FR-002): strengthen
_registry_authoritative_quarantine_violationsfrom "no registry-authoritative row is quarantined" to the full reader==repair invariant: noevent_type-bearing row is quarantined at all. This makes the guard catch the unregistered-future case it structurally could not before (it was powered by the same registry). Preserve the #4938-folded carve-out: a benign duplicate-event_iddrop whose survivor stays incanonical_rowsmust NOT trip the guard. - Alternatives considered: (a) add the next missing type to the registry — REJECTED, the whack-a-field patch this mission exists to end; (b) a non-empty denylist naming DecisionPoint/lifecycle as "mirrors" — REJECTED, the census proves they are canonical, not mirrors; (c) conservative interim (guard-only, keep pruning) — REJECTED by operator decision (ship the inversion).
Finding B — single catalog.mission accessor
- Decision: Add a shared accessor (e.g.
read_catalog_field(repo_root, field)/ a thinread_catalog_mission(repo_root)) tosrc/charter/activation/charter_yaml_io.py, alongside the canonicalload_charter_yaml. Collapse bothcli/commands/charter/generate.py::_read_catalog_mission_from_charter_yamlandcharter_runtime/preflight/references_refresh.py::_read_catalog_mission_and_template_setonto it. Optionally (campsite) refactorlanguage_scope.py::_read_compiled_languagesto become a client of the same accessor. - Rationale:
load_charter_yamlis the canonical loader (ruamel round-trip,preserve_quotes, empty→CommentedMap(), missing→FileNotFoundError).generate.pyalready imports it;references_refresh.pyhand-rolls a divergentYAML(typ="safe")reader (:122) — the exact drift.language_scope._read_compiled_languagesalready models the target shape (load_charter_yaml → document.get("catalog") → catalog.get(field)with a(YAMLError, OSError, UnicodeDecodeError)guard). Both callers already reachcharter.activation.*lazily elsewhere, so no import-layer/boundary violation. - Correction to #4993: the issue says "three
catalog.missionreaders" — the true count is two. The claimed third (language_scope._read_compiled_languages) readscatalog.languages, a different field. The mission corrects this in the #4993 close-out. - Alternatives considered: (a) point
references_refreshatload_charter_yamlinline without a named accessor — leaves two copies of the catalog-walk shape; the named accessor is the SSOT; (b) a new module — unnecessary,charter_yaml_ioowns the loader.
Finding C — traces merge-driver hardening (bounded)
- Decision: In
cli/commands/merge_driver.py: (1) widen_TRACE_FENCE_MARKERfromre.compile(r"^`")tore.compile(r"^(?:`|~~~)")(optionallyr"^(?:{3,}|~{3,})") — the single fence-toggle site;in_fencethen propagates through_split_trace_blocks. (2) Replace the colliding_trace_block_key(block[0]) +setdefaultmaps in_drop_stale_theirs_trace_blockswith a non-colliding base-comparison key: prefer the<!-- section:ID -->id when the block opens with one; else fall back to(block[0], occurrence_ordinal)` so duplicate headings get distinct keys. - Rationale:
~~~is valid CommonMark fencing; a heading-like line inside a tilde block is currently misread as a section boundary.setdefault(block[0], …)is first-occurrence-wins, so two sections sharing a heading collide in the base/ours maps. The<!-- section:... -->delimiter is the author-supplied stable id the driver's own boundary regex already recognizes. - Must NOT change (C-002 / NFR-004): the whole-block byte-identical dedup in
union_trace_texts(full-tupleseen) stays as-is — it is correct record-granularity dedup. Do not use a full-block hash for the base-comparison key: base-awareness must detect "same section, body changed" (theirs_unchangedvsours_diverged); a body-sensitive key defeats the 3-way match. The two keys are intentionally different. - Alternatives considered: (a) fail-closed on repeated headings like the acceptance/issue-matrix drivers — REJECTED (aborts real keyless append-union merges, per the prior mission's C-003); (b) broad parser refactor — REJECTED by C-002.
Cross-cutting
- These are three independent seams with no shared guard (prior mission's cross-cutting finding). Do NOT route any through the
asset_preservationguard (filesystem-path granularity; category error — spec C-001). - Only Finding A is a recurring whack-a-field class warranting the shared-rule collapse; B and C are first-order SSOT/correctness fixes.
- Concurrent PR #4950 (coordination-doctor branch-safety): verified disjoint source + zero semantic coupling; shared surfaces (
_completion_manifest.json,docs/api/cli-commands.md,CHANGELOG.md) are landing-time regen, not lane contention.