Implementation Plan: Merge squash provenance + rollback/resume coherence (P0 #2709/#2711)
Branch: kitty/mission-merge-squash-provenance-and-rollback-coherence-01KXRRB7 | Date: 2026-07-17 | Spec: spec.md Input: Feature specification from kitty-specs/merge-squash-provenance-and-rollback-coherence-01KXRRB7/spec.md
Summary
Two related P0 defects in the mission-merge core, both surfaced in the #2658 merge, both symptoms of one architectural class — the merge core writes to the committed target/coord branch without reconciling against the durable event-log authority:
acceptance/VCS fields and traces/*.md; separately, _project_status_bookkeeping_to_target blind-overwrites the target event log. Fix by per-artifact-class reconciliation (planning artifacts stay mission-authoritative; meta.json field-merged; traces unioned; event-log projection routed through the canonical merge_event_payloads).
coord-branch done commit standing over reverted working status; --resume derives progress from a byte-reverted MergeState and re-emits duplicate done. Fix by Option A (revert the coord done commit on rollback → restores committed==working coherence) plus durable-log resume derivation.
- #2709 (happy path):
git merge --squash -X theirsclobbers target-newermeta.json - #2711 (failure path): rollback reverts only working-tree bytes, leaving the committed
Delivered ATDD-first: a red-first reproduction for each defect lands before its fix, on two decoupled chains under one mission so the release-blocking #2709 can land independently of the deeper #2711 surgery.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: git (custom .gitattributes merge drivers), typer, ruamel.yaml; internal specify_cli.status.event_log_merge, specify_cli.merge., specify_cli.lanes.merge, specify_cli.coordination.status_transition, specify_cli.acceptance Storage: git-tracked mission artifacts — status.events.jsonl (append-only event authority), meta.json, traces/.md; .kittify/merge-state.json Testing: pytest; real-git coord harness (tests/specify_cli/cli/commands/test_merge_coord_topology_1772.py), regression suite convention (tests/regression/test_issue_<n>.py with pytestmark), INV ratchets (tests/architectural/test_merge_pipeline_ratchets.py) Target Platform: Linux/macOS/Windows CLI Project Type: single project (CLI library) Performance Goals: no merge-path latency regression; NFR-001 byte-identical no-divergence squash Constraints: mypy --strict, ruff (complexity ≤ 15), no new-code Sonar regressions; INV-5 (#1827) ratchet must stay green; -X theirs planning-artifact authority (#1732) preserved Scale/Scope: ~4–5 coordinated files for the .gitattributes driver wiring; localized executor/rollback edits; two new regression tests + invariant guards
Charter Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
commit failing reproductions (RED-for-the-right-reason per SC-001) before any fix; reviewer verifies RED-on-base → GREEN-on-final.
merge_event_payloads; the new meta.json field reconciler is a fit-for-purpose reconciler for a different artifact, not a duplicate authority (C-001 wording clarified in spec).
non-vacuous and cover BOTH loss mechanisms — a no-blind-copy lint over merge/ projection (the write_bytes vector) plus a driver-registry-completeness lint (the -X theirs/no-driver vector in lanes/merge.py, blind to the projection lint — paula HIGH). Not a re-run of WP01. Split per chain (WP04a/WP04b) + one cross-cutting lint.
entry point and reuse the canonical coord harness; no reconstructed paths.
scope; rebase-first (C-003) since the merge core was freshly refactored (#2057/#2173/#2632/#2675).
harness extensions get standard rigour.
- ATDD-first / red-first (C-011, DIRECTIVE_041/034): PASS by construction — WP01a/WP01b
- Single canonical authority (DIRECTIVE_044): PASS — event-log reconciliation reuses
- Architectural gate discipline (DIRECTIVE_043): the class-closing guard (FR-008) must be
- Canonical sources (DIRECTIVE_044): PASS — repros drive the real
_run_lane_based_merge - Terminology canon (C-005):
Missionnotfeature;--missionflags. - Git/workflow discipline (DIRECTIVE_045): PRs only, operator merges; no version numbers in
- Tiered rigour: core merge/rollback logic gets maximum rigour (it is core domain); test
Phase-1 re-check RESOLVED (architect, code-verified): Option A does NOT violate the INV-5 #1827 ordering ratchet — rollback runs on the failure path before the RECORD→commit→ASSERT sequence and reorders nothing. Anchor correction: the INV-5 #1827 ratchet lives in tests/merge/test_executor_phase_boundary.py + tests/specify_cli/merge/test_1827_baseline_regression.py, NOT tests/architectural/test_merge_pipeline_ratchets.py (which locks #1826/#1736 — AC-B3 no-raw-update-ref, AC-F1 _make_merge_env env authority). Those AC-B3/AC-F1 lints do bind IC-04's coord-done-revert mechanism (it moves a branch ref) — use git/ref_advance.py::advance_branch_ref or coord-worktree git revert, never raw update-ref.
Project Structure
Documentation (this mission)
kitty-specs/merge-squash-provenance-and-rollback-coherence-01KXRRB7/
├── plan.md # This file
├── spec.md # Mission spec (squad-folded)
├── research/ # Pre-spec squad findings (lens-a..d)
├── tasks.md # Phase 2 output (/spec-kitty.tasks)
└── tasks/ # WP prompt files
Source Code (repository root)
src/specify_cli/
├── lanes/merge.py # _merge_branch_into squash (-X theirs), driver self-heal, _make_merge_env (#2709)
├── merge/
│ ├── executor.py # _phase_bake_and_pre_target_done, _phase_mission_to_target, rollback (#2711)
│ ├── bookkeeping_projection.py # _project_status_bookkeeping_to_target (blind write_bytes), snapshot restore (#2709 FR-005, #2711)
│ ├── done_bookkeeping.py # pre-target done emit, resume reconcile (#2711)
│ ├── state.py # MergeState.completed_wps → advisory hint (#2711 FR-007)
│ └── baseline.py # record_baseline_merge_commit (idempotency surface) (#2709 edge)
├── coordination/status_transition.py # worktree-first read contract (do NOT re-route — FR-007)
├── status/event_log_merge.py # merge_event_payloads (canonical reconciler — reuse) (#2709 FR-005)
├── acceptance/__init__.py # canonical acceptance field shapes (#2709 FR-004)
└── cli/commands/
├── merge.py # _run_lane_based_merge (canonical entry point)
├── merge_driver.py # existing event-log driver command (sibling for new drivers) (#2709 FR-003/004)
└── __init__.py # driver registration
tests/
├── regression/test_issue_2709_squash_provenance.py # WP01a
├── regression/test_issue_2711_merge_rollback_resume_coherence.py # WP01b
├── specify_cli/cli/commands/test_merge_coord_topology_1772.py # harness to extend
├── merge/test_merge_target_resolution.py # coord-worktree materialization pattern
├── merge/test_executor_phase_boundary.py # REAL INV-5 #1827 ratchet (must stay green)
├── specify_cli/merge/test_1827_baseline_regression.py # REAL INV-5 #1827 baseline (must stay green)
└── architectural/test_merge_pipeline_ratchets.py # AC-B3/AC-F1 (#1826/#1736) lints; FR-008 lint home (NOT INV-5)
Also relevant: src/specify_cli/git/ref_advance.py::advance_branch_ref (sanctioned ref-move for IC-04's coord-done-revert) and src/specify_cli/merge/_constants.py (_STATUS_FILENAME).
Structure Decision: Single-project CLI. Changes are confined to src/specify_cli/{lanes,merge,coordination,status,acceptance,cli} with new regression tests under tests/regression/ and the class guard under tests/architectural/. No new top-level packages.
Complexity Tracking
| Decision | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
New meta.json field-level merge driver (FR-004) | No reconciler exists for meta.json; -X theirs clobbers acceptance provenance | "Reuse merge_event_payloads" rejected — it is keyed event-union, structurally wrong for a keyed JSON object; a post-squash reconcile pass is the fallback if the driver proves unfit |
| Isolate the FR-004 driver spike into its own WP (architect MEDIUM) | The meta.json driver is the top unknown; bundling it with replicable wiring stalls the whole #2709 chain on a spike failure | Keeping it in WP02 rejected — a spike failure would block the release-blocking chain's low-risk work |
Option A (revert coord done commit) over Option B (defer commit) | Restores committed==working coherence without touching the read-contract SSOT; verified not to violate INV-5 #1827 | Option B rejected — it inverts done_marked_before_target machinery and fights the INV-5 #1827 phase-ordering ratchet |
Helper extractions (_revert_coord_done_commit, _union_event_logs, _rematerialize_status_snapshot) | _restore_pre_target_if_at_baseline / projection near the 15-CC ceiling; FR-005's status.json re-reduce adds a pass | Inlining rejected — would breach complexity ceiling and lack focused test coverage |
| IC-04 co-top-risk with FR-004 (Ivan/architect) | Option A reaches across the executor↔coordination boundary (coord-ref resolve + revert) that does not exist today | Treating IC-04 as low-risk plumbing rejected — the coord-branch-ref capture/revert is net-new cross-boundary work |
Implementation Concern Map
> Concerns are architectural areas, not WPs. > > Old→final WP numbering (post-tasks renumber): the prose below and the spec use the > original draft IDs WP01a/WP01b/WP02/WP03/WP04a/WP04b. The finalized WPs are: > WP01a→WP01 (#2709 red), WP01b→WP02 (#2711 red), WP02→WP03 (#2709 fix), > WP03→WP04 (#2711 fix), WP04a→WP05 (#2709 guard), WP04b→WP06 (#2711 guard). > Chains: WP01→WP03→WP05 (#2709) ∥ WP02→WP04→WP06 (#2711). See tasks.md for the authoritative graph.
IC-01 — Red-first reproduction harness
- Purpose: Witness both defects against live code before any fix, RED-for-the-right-reason.
- Relevant requirements: FR-001, FR-002; SC-001.
- Affected surfaces:
tests/regression/test_issue_2709_.py,tests/regression/test_issue_2711_.py; harness extensions intest_merge_coord_topology_1772.py(per-branch accept helpers, coord-worktree materialization,real_baseline_recordingreal-done seam,review_status: approvedseed). - Sequencing/depends-on: none (root of both chains); the two repros are independent.
- Risks: green-on-base traps — #2709 needs both-sides
meta.jsondivergence; #2711 needsdone_marked_before_target=True+ committed-doneprecondition asserted. Canonical failure-injection target isspecify_cli.lanes.merge.integrate_mission_into_target(NOTspecify_cli.merge.executor.*).
IC-02 — Squash artifact reconciliation (#2709 content)
- Purpose: Reconcile mission artifacts with target-newer canonical state instead of wholesale
-X theirsreplacement. - Relevant requirements: FR-003 (per-class + traces union contract), FR-004 (meta.json field-merge — spike-flagged top risk), C-002 (#1732 preserved), C-006 (.gitattributes multi-surface wiring).
- Affected surfaces:
lanes/merge.py(incl. generalizing_ensure_event_log_merge_driver_config— parametrize name/command, do NOT clone it, DIRECTIVE_044),cli/commands/merge_driver.py(+ new driver),cli/commands/__init__.py, root.gitattributes,init.py, a driver-config migration sibling,acceptance/__init__.py(field shapes). - Sequencing/depends-on: IC-01 (the #2709 repro). Split the FR-004 meta.json-driver spike into its own WP (architect MEDIUM) so a spike failure fails fast without stalling the replicable traces/wiring work.
- Risks: FR-004 meta.json driver is the top spike risk; traces have no natural dedup key → union contract must be concrete or descoped to a post-merge pass; driver-under-
--squash -X theirsis empirically confirmed to fire;_make_merge_envPATH pin already auto-covers new drivers; idempotency vs the post-merge baselinemeta.jsonwrite.
IC-03 — Event-log projection union + snapshot rematerialization (#2709 event log)
- Purpose: Stop the coord→target projection from blind-overwriting the target event log and the derived
status.jsonsnapshot. - Relevant requirements: FR-005; C-001.
- Affected surfaces:
merge/bookkeeping_projection.py::_project_status_bookkeeping_to_target— TWO blind writes::306status.events.jsonl(→ unionsource ∪ original, both byte-sets already captured at:302-303, viamerge_event_payloads) and:308status.json(→ rematerialize from the unioned events viareduce(), NOT blind-copy; it is a reduced snapshot,_STATUS_FILENAME). *Write both back through the already-resolvedtrusted_target paths** returned by_target_bookkeeping_status_paths(seam-resolved viaprimary_feature_dir_for_mission) — compose no new path. Assertsnapshot == reduce(union). - Sequencing/depends-on: IC-01 (needs its own witnessing RED — US2-S4, incl. a target-newer
status.jsonfield). - Risks: NOT a ~5-line swap (both-side JSONL parse/serialize + a reduce pass); ensure the union is order-stable for NFR-001; a
_rematerialize_status_snapshothelper (CC).
IC-04 — Rollback/resume coherence (#2711 failure path) — co-top-risk with FR-004
- Purpose: Keep committed and working mission state coherent on rollback, and make
--resumederive progress from durable events without duplicating transitions. - Relevant requirements: FR-006 (Option A coord-
done-revert), FR-007 (durable-log resume,completed_wps→hint); NFR-002 idempotency. - Affected surfaces:
merge/executor.py(_restore_pre_target_if_at_baseline; capture coord ref SHA before the pre-target done emit — not captured today,pre_target_bookkeeping_snapshotsholds working bytes only),merge/done_bookkeeping.py(resume reconcile),merge/state.py,git/ref_advance.py::advance_branch_ref(sanctioned ref-move for the revert). Source the ref-to-revert fromresolve_placement_only(repo_root, slug, kind=STATUS_STATE).ref(the canonical write-target thedonecommit used — NOT an inlinemeta.get("coordination_branch"), which reintroduces the retired D-2 CWD-divergence class). FR-007 resume derivation consumesread_event_log(EventLogReadContract.coordination_branch_ref(...))+wp_lane_actor_from_events()(coordination/status_service.py) — do NOT author a new reducer, and do NOT re-route thestatus_transition.pyread contract. Revert mechanism must beadvance_branch_refor coord-worktreegit revert— never rawupdate-ref(AC-B3), env via_make_merge_env(AC-F1). Note: the surface-resolution seam is path-only (surface_path/primary_anchor) and deliberately does not surface refs — do not seek the ref from_with_anchor. - Sequencing/depends-on: IC-01 (the #2711 repro); rebase-first (C-003) + re-resolve anchors by symbol.
- Risks: co-equal top risk — Option A is net-new executor↔coordination cross-boundary work; coord ref SHA not currently captured; helper extraction near CC ceiling; real INV-5 #1827 ratchet homes (
test_executor_phase_boundary.py,test_1827_baseline_regression.py) must stay green. WP03 open question (resolve first): confirm the coorddonecommit is made viagit commitvsupdate-refinsideBookkeepingTransaction, and that_read_contract_from_transaction_target(status_transition.py:680-693) reads the worktree leg — governs whether AC-B3 binds the revert and whether Option A restores dedup "for free".
IC-05 — Class-closing invariant guards (TWO mechanisms)
- Purpose: Prevent recurrence of the "merge silently overwrites target-newer canonical/durable state" class — covering BOTH the
write_bytesprojection vector AND the-X theirs/no-driver vector. - Relevant requirements: FR-008; SC-005.
- Affected surfaces:
tests/architectural/— (1) no-blind-copy AST lint overmerge/projection paths; (2) driver-registry-completeness lint: every both-sides-divergentkitty-specs/**canonical glob has a registered merge driver (today root.gitattributesregisters onlystatus.events.jsonl); plus a property assertionmerge_output ⊇ target-newer canonical fields. Precedent:test_merge_pipeline_ratchets.pyalready runs AST per-call-site lints. - Sequencing/depends-on: co-delivered per chain — WP04a with the #2709 fix (IC-02/03), WP04b with the #2711 fix (IC-04); plus one thin cross-cutting lint.
- Risks: must be non-vacuous (a self-mutation check), not a re-run of IC-01 tests; the driver-registry lint is the piece that closes the class against future artifacts (paula HIGH).
Cross-chain ownership note (Ivan MEDIUM)
The two "decoupled" chains are not file-disjoint: merge/bookkeeping_projection.py is edited by both IC-03/WP02 (projection union) and IC-04/WP03 (rollback machinery — _restore_final_bookkeeping_snapshots, _capture_bookkeeping_snapshots live here too). /spec-kitty.tasks MUST publish a function-level ownership map for this module (or serialize the two edits) to avoid a merge conflict when the chains land. Campsite (DIRECTIVE_025) while in these files: the stale _project_status_bookkeeping_to_target shadow re-export in cli/commands/merge.py.__all__, and the tracked temp artifact tests/architectural/tmp_ratchet_baseline.txt.