Implementation Plan: Post-merge partition integrity (#3942 + #4090)
Branch: issue-3942-merge-surface-authority | Date: 2026-09-14 | Spec: ./spec.md Input: Feature specification from kitty-specs/post-merge-partition-integrity-01M2FQ80/spec.md
Summary
Two-track fix on the shared post-merge partition surface, verified live red-first by a 3-lens pre-spec squad on HEAD ecbf036328.
- Track A (#3942, write half): squash consolidation clobbers target-newer
kitty-specs/planning files with older lane copies. Real seam:lanes/merge.py::integrate_mission_into_target(SQUASH)→_merge_branch_into→git merge --squash -X theirs(lanes/merge.py:635), with_MERGE_DRIVERS(lanes/merge.py:59-121) reconciling only an allowlist of 6 artifact classes. Everything else (planning docs) falls to blanket-X theirs= lane wins. No recency/kind guard. - Track B (#4090 + folded #4091, read half): retrospect and doctor now share
resolve_status_surface, but the primary-wins guard for a surviving diverged coord husk (surface_resolver.py:745_primary_mission_is_completed→is_mission_merged→_last_merge_marker_at→meta.get("merged_at")) gates onmeta["merged_at"], whose production writer was deleted in #2258 (6f46cf6bb6) and never re-added. The guard is dormant; divergence resurfaces.
Shared root-cause hypothesis (FR-009): both are "what content/state is authoritative post-merge, and on which surface." Track A = which bytes win per artifact; Track B = which surface a reader trusts. The classifier mission_runtime.kind_for_mission_file / is_primary_artifact_kind (the single-authority seam extended by the #3867/#2181 work) is the candidate shared spine: planning artifacts are PRIMARY-partition-authoritative, so lanes must neither overwrite them (A) nor be read as their post-merge home (B).
Technical Context
Language/Version: Python 3.11 Primary Dependencies: internal — specify_cli.lanes.merge, specify_cli.merge.executor, specify_cli.coordination.surface_resolver, mission_runtime kind classifier, specify_cli.status reducer Storage: git (merge worktrees), kitty-specs/ files, status.events.jsonl, meta.json Testing: pytest — targeted only (PYTHONPATH=$(pwd)/src .venv/bin/python -m pytest <targeted> -q -p no:cacheprovider), never full suite, never uv run Target Platform: Linux dev/CI Project Type: single (CLI toolkit) Constraints: complexity ≤15; single canonical authority (extend, don't add); name overloaded primary/merge senses; mission minted single_branch (no coord dogfooding); ATDD red-first per track Scale/Scope**: two disjoint code seams, ~4-6 WPs, both P1
Constitution Check
GATE: passes. C-001 single canonical authority — Track A extends the merge-driver/restore + kind_for_mission_file authority; Track B re-activates the existing _primary_mission_is_completed guard rather than adding a second reader. C-002 terminology — plan names write/read halves and primary/coord senses explicitly. ATDD-first — each track's WP01-equivalent is a red-first repro that must fail on HEAD before a fix lands. No new authority introduced.
Locked design decisions (post-brownfield-squad, 3-lens convergent)
Validated by a 3-lens post-plan brownfield squad (architect-alphonso seam, paula-patterns reuse, python-pedro feasibility). All three converged; two decisions were RE-LOCKED from the proposals. Citations verified against HEAD ecbf036328.
Track A — #3942 (write authority)
- D-A1 (LOCKED): Resolve
kitty-specs/conflicts during squash by artifact kind. Files whereis_primary_artifact_kind(kind_for_mission_file(path))is true resolve target-authoritative when the target copy is newer, with the divergence reported (FR-002); driver-covered classes keep their existing reconcilers (FR-003). pedro ran the classifier: spec.md→SPEC, plan.md→FINALIZED_EXECUTION_PLAN, tasks/WP*.md→WORK_PACKAGE_TASK, research/data-model → allis_primary_artifact_kind=True; driver-covered artifacts False. The predicate (src/mission_runtime/artifacts.py:158/402/415) is the pre-built single authority — advances #2907 by driving resolution off it. - D-A2 (LOCKED = (a); (b) REJECTED as infeasible): Extend the post-squash restore
_restore_regressed_gate_artifacts(merge/executor.py:206, #2804 pattern). Option (b) — a_MERGE_DRIVERSplanning-file driver — is structurally incapable of the required decision: a git merge driver receives only three blobs (%O %A %B) with no ref/history access, so it cannot compute recency; it could only refuse-on-collision (over-fires on every legitimate lane-carries-finalized-state merge). Recency lives in the executor, which has full git access. - D-A3 (RE-LOCKED → three-way divergence): "Newer" is a three-way base/target/lane comparison: target wins iff target diverged from the merge-base while the lane copy is base-or-ancestor (target evolved, lane is stale). Committer-date is a tiebreak only; never mtime. This mirrors the #2709 meta-field driver and is topology-safe by construction — on
single_branch/LANES(lane≠base, target=base) the lane correctly wins with no special-case. Reuse the merge-base + per-side-diff shape atlanes/stale_check.py:39-67; no ready-made helper exists, so WP02 builds a small pure one. - D-A4 (new): Amend the
lanes/merge.py:631-633"mission branch is authoritative" comment — its premise is false for PRIMARY-partition planning kinds (authored on primary, not the lane).
Track B — #4090 + #4091 (read authority)
- D-B1 (RE-LOCKED → restore writer AND make guard reopen-aware; one coupled change): Restore a production writer for
meta["merged_at"](datetime; +merged_commitfor provenance) as a sibling ofrecord_baseline_merge_commitin the executor's_phase_capture_and_baseline(executor.py:951, meta-write authoritymerge/baseline.py) — notdone_bookkeeping.py(which writes nothing to meta.json; a meta write there is a boundary leak). The driver's_TARGET_AUTHORITATIVE_META_FIELDS(merge_driver.py:84-95) already declaresmerged_atas the marker, so this is the canonical reuse, not a new authority. Do NOT re-point the guard tobaseline_merge_commit(modern-mission-only; not cleared on reopen; a SHA not a datetime) ormission_number(permanent identity; never cleared on reopen) — both would make a reopened mission read as merged forever. Mandatory addendum: because nothing today actually clearsmerged_aton reopen andis_mission_merged(lifecycle.py:302) is presence-only, restoring the writer would make a reopened mission wrongly read as merged across three consumers (surface_resolver.py:745,runtime_bridge.py:1600,is_mission_completed). So WP04 must also makeis_mission_mergedreopen-aware via the adjacent_last_reopen_atmachinery (merged iffmerged_atpresent AND no laterMissionReopened) — event-sourced, no meta-clearer. Writer + reopen-awareness are ONE WP. - D-B2 (LOCKED = separable; #4091 contingent): #4090 is fixed by the marker alone. #4091 is a distinct measure mismatch:
event_count = len(unique transition events)by definition < rawstatus.events.jsonlline count (annotations, lifecycle, decision events are not transitions)._project_status_bookkeeping_to_target(bookkeeping_projection.py:282) already unions + re-reduces, and its union branch runs only under a coord husk (:315is_under_worktrees_segment) — thissingle_branchmission never hits it. So WP05 is CONTINGENT: it must first stand up a red-first repro on a coord fixture; ifevent_countis already correct on HEAD, #4091 is verify-and-close (building a fix-test first = green-regression trap). - D-B3 (LOCKED): Keep
resolve_status_surfaceas the single read resolver; the fix only makes the merged-state signal it consumes actually written + reopen-correct — it does not fork the reader.
Do-not-touch / hazards (all three lenses)
merge/conflict_resolver.py(ConflictType/classify_conflict/resolve_owned_conflicts) is DEAD (onlymerge/__init__.pyre-exports it; the live auto-rebase classifier is the different modulemerge/conflict_classifier.py). WP02 must not wire the fix into it. Boy-scout: file a dead-code ticket advancing #2907.- Keep green (FR-003 / SC-004): the #2709/#2804 suites —
test_squash_target_newer_provenance_regression_2709.py,test_squash_reconcilers_2709.py,test_gate_artifact_merge_drivers_2804.py,test_issue_2804_merge_resets_gate_artifacts.py,test_issue_2709_projection_union.py. _restore_regressed_gate_artifactsis ~complexity 4 today; WP02 must extract the recency decision into its own helper to hold ≤15 (NFR-003).- WP03 must drive the real
doctor mission-stateread leg (it passes throughenforce_primary_write_ownership,_mission_state_doctor.py:429), not infer agreement from the retrospect side alone. - Epic notes for WP06 synthesis: D-A1 advances #2907 (classifier-driven taxonomy); D-B1 chooses "override the husk on primary" over "freshen the husk" — a #2160 coord-authority decision to record for the epic owner.
Project Structure
Documentation (this mission)
kitty-specs/post-merge-partition-integrity-01M2FQ80/
├── spec.md # committed
├── plan.md # this file
├── analysis-report.md # /analyze output (later)
├── synthesis.md # FR-009 cross-track root-cause synthesis (WP06)
└── tasks/ # WP prompt files (/tasks output)
Source Code (repository root) — seams by track
Track A (write):
src/specify_cli/lanes/merge.py # integrate_mission_into_target, _merge_branch_into, _MERGE_DRIVERS
src/specify_cli/merge/executor.py # _phase_mission_to_target, _restore_regressed_gate_artifacts
src/mission_runtime/ (kind_for_mission_file / is_primary_artifact_kind) # classifier authority (read/extend)
tests/integration/ tests/lanes/ # red-first + regression
Track B (read):
src/specify_cli/coordination/surface_resolver.py # _primary_mission_is_completed, is_mission_merged (guard)
src/specify_cli/merge/done_bookkeeping.py # merge completion — write merged_at
src/specify_cli/status/ (lifecycle.py, reducer) # merged_at read; event_count (#4091)
src/specify_cli/cli/commands/agent_retrospect.py # reader (verify, likely untouched)
tests/ (retrospect + doctor + status) # red-first + regression
Structure Decision: single project; two disjoint seam clusters kept in separate WP tracks for reviewability but synthesized in WP06.
Parallel Work Analysis
Dependency Graph
Track A: WP01 (red-first clobber repro) ──▶ WP02 (three-way-recency restore + kind guard + report + comment amend)
Track B: WP03 (red-first retro/doctor disagreement) ──▶ WP04 (restore merged_at writer + reopen-aware is_mission_merged)
──▶ WP05 (#4091 CONTINGENT: coord-fixture repro → reconcile-or-verify-close)
Both: WP02, WP04, WP05 ──▶ WP06 (cross-track synthesis + NFR census + green consolidation)
Tracks A and B are file-disjoint and run in parallel. WP06 is the only cross-track join.
Work Distribution
- Sequential within track: red-first repro must land (and be RED on HEAD) before its fix WP.
- Parallel streams: Track A (
merge/executor.py+ new recency helper) and Track B (merge/baseline.py/executor writer +status/lifecycle.py) touch disjoint files — safe to parallelize. - Ownership:
- WP01 owns
tests/merge/(ortests/lanes/) Track-A red-first (realintegrate_mission_into_target(SQUASH)harness, fromrepro_3942.py). - WP02 owns
merge/executor.py(_capture_pre_target_planning_artifacts, extended restore) + a net-new pure three-way recency helper + thelanes/merge.py:631-633comment amendment; consumes themission_runtimeclassifier read-only. Must NOT touchmerge/conflict_resolver.py(dead) or the_MERGE_DRIVERSregistry. - WP03 owns the Track-B red-first test driving the real retrospect AND real doctor readers to disagreement (constructed post-merge state; NO live coord merge — C-003).
- WP04 owns the
merged_atwriter besidemerge/baseline.py's pattern (call siteexecutor.py:951) and the reopen-aware change instatus/lifecycle.py::is_mission_merged— one coupled change; re-armssurface_resolver.py:745+runtime_bridge.py:1600by data. - WP05 owns the #4091 coord-fixture repro in
merge/bookkeeping_projection.py/ status reducer — existence gated on the repro being RED on HEAD. - No
owned_filesoverlap;mission_metadata._MERGE_FIELDSstays untouched.
Coordination Points
- Integration: WP06 runs the combined regression (both red-first tests green) + the preserved #2709/#2804/#3981 suites, writes the FR-009 synthesis (record the #2709→#3942 target-newer lineage, the dead-
conflict_resolver.pydisposition for #2907, and the D-B1 husk-override authority note for #2160), and runs the NFR-003 complexity census. - Post-plan brownfield squad: DONE — 3-lens convergent; decisions above are the re-locked result.