Research: Fail-closed acceptance-matrix merge driver (#4880)
Phase 0 for acceptance-matrix-merge-fail-closed-01M34HG8. The bulk of this "research" is a completed 3-lens investigation (code grounding, architectural alignment, related tickets) run 2026-09-22 against skupstream/main @ 793e555224.
Decision: fail closed at the shared _merge_field mechanism
merge driver raises RowMatrixMergeError (→ typer.Exit(1) → git leaves the conflict → _merge_branch_into aborts) instead of returning a _field_conflict_marker(...) string. _field_conflict_marker is deleted.
(RowMatrixMergeError → Exit(1), used for malformed JSON and the intra-side duplicate-key guard); the field-conflict path is the one deviation. Raising in the shared mechanism closes the class by construction (DIRECTIVE_043) for both the acceptance and issue matrix drivers, and covers the "lesser variant" (prose-only divergence still embedded markers today).
- Decision: On a genuine both-sides-diverged keyed-row field, the matrix
- Rationale: The fail-closed spine already exists in the module
Alternatives considered
reconcile_acceptance_matrix_documents.** Rejected: smaller diff but leaves the lesser variant (markers embedded in description/notes) silently committed — the architecture review flagged this as whack-a-field.
fires downstream of the corrupting write and today from_dict succeeds on a marker string (a valid str), silently recomputing to fail. Kept as defense-in-depth (FR-004), not the load-bearing fix.
wider blast radius; belongs behind the ADR as a follow-up, not this hotfix.
- **Guard only the verdict field (
pass_fail/result) in - Read-side rejection only (
from_dict). Rejected as the primary fix: it - Widen
overall_verdictrecompute to raise on unknown tokens. Deferred:
Recorded-decision reversal (load-bearing)
Fail-closed reverses decisions that are currently contract- and test-pinned; all amendments land in this mission:
qualify its "no consolidation abort" rule to exclude gate/verdict artifacts (authoritative → fail closed; non-authoritative prose, e.g. review-cycle .md, → best-effort embed).
the line that calls marker-embed "fail-closed (no consolidation abort)".
negative control* — asserts the pass_fail-conflict→fail flip is correct*; amend to expect refusal.
variant) and tests/specify_cli/cli/commands/test_row_aware_merge_driver.py (pass_fail marker embed). Keep test_a4 (authored out-of-domain value → fail is a different, correct behavior).
- ADR
docs/adr/3.x/2026-07-23-2-post-consolidation-deferral-and-external-enforcement.md— - Contract
kitty-specs/write-side-seam-matrix-tracer-01KYP3MH/contracts/merge-driver-algorithm.md:29— - **Completed mission
gate-artifact-merge-driver-unit-gate-01KZPG7V, FR-004 - Test pins:
tests/merge/test_gate_artifact_merge_drivers_2804.py(lesser
Symmetric precedent: the review-cycle driver was deliberately downgraded to non-aborting because it became non-authoritative prose; the identical reasoning justifies upgrading the verdict-bearing acceptance fields to fail-closed.
History note
#1424 recorded the opposite historical default (unknown token → fail-open to pass); the recompute has since inverted to fail (matrix.py:311-335). Not changed here, but noted so the direction is understood.
Supply-chain security
N/A — this mission adds, upgrades, and removes no dependencies in any ecosystem. No registry-authenticity / lifecycle-script / LTS decision to record.
Adversarial evidence
The three review lenses (python-pedro code grounding, paula-patterns arch alignment, planner-priti related tickets) served as the pre-plan adversarial pass. Contested-finding dispositions:
per the whack-a-field argument (operator-confirmed).
scope (FR-006).
the coherent rule (operator-confirmed).
distinct vector, own follow-up issue, must consume canonical mission-state.
- Altitude (verdict-field-only vs mechanism) — changed to mechanism-level
- Direction reverses recorded decisions — accepted; amendments folded into
- Issue-matrix also fails closed under the shared mechanism — accepted as
- Closed-mission mutation guard (#4880 fix #2) — deferred_with_rationale: