Post-Merge Partition Authority
This page is the FR-009 cross-track synthesis for mission
post-merge-partition-integrity-01M2FQ80. It maps the two tracks the mission
delivered onto one question and records the epic-owner notes and the
shared-vs-disjoint verdict the squad reached.
Terminology (per the
primary/mergefootgun canon). "PRIMARY-partition" below is the partition sense — the stable planning artifacts (spec.md,plan.md,tasks/WP*.md,research/,data-model.md) — never the Primary-Branch (main) sense. "Surface" is the read-resolution target (coord branch vs primary), not a git branch instruction. Seedocs/context/orchestration.md#primary-partition.
The one question
After a lane consolidation, what content is authoritative for each partition artifact, and on which surface is that content read? The mission split the question into two halves that are facets of the same authority model:
| Half | Track | Authority decided | Failure it fixed |
|---|---|---|---|
| Write | A (#3942) | Which bytes win per artifact when a merge reconciles divergent kitty-specs/ files |
Older mission-branch planning copy silently clobbered a target-newer copy on squash |
| Read | B (#4090) | Which surface a post-merge reader trusts for WP lane state | A stale diverged coord husk was read as authoritative because the primary-wins guard was dormant |
Both halves are governed by the same partition classifier —
mission_runtime.kind_for_mission_file / is_primary_artifact_kind
(src/mission_runtime/artifacts.py). Planning artifacts are
PRIMARY-partition-authoritative, so lanes must neither overwrite them (write
half) nor be read as their post-merge home (read half).
Track A — the write seam (which bytes win)
Seam: src/specify_cli/lanes/merge.py::_merge_branch_into (the squash step)
plus the pure three-way recency helper
src/specify_cli/merge/planning_recency.py::target_newer_primary_artifacts.
The mission→target squash runs git merge --squash -X theirs <mission_branch>.
-X theirs makes the source (mission branch) win every add/add conflict.
That premise is correct for source-authored code and for the six driver-covered
kitty-specs/** bookkeeping classes (reconciled by _MERGE_DRIVERS), but it is
false for the PRIMARY-partition planning artifacts: those are authored on the
primary/target surface, so the target can legitimately carry a newer copy than
the mission branch (#3942).
A git merge driver cannot fix this. A driver sees only three blobs (base / ours /
theirs) and has no history access, so it cannot decide which side is newer —
recency is a repository-history question. So the fix sits outside the byte-
identical -X theirs block: after the squash,
_preserve_target_newer_planning_artifacts (in lanes/merge.py) calls the pure
helper, restores each target-owned path from the pre-advance target_branch
blob, and amends the squash commit so the preserved content lands in the
single merge commit the ref-advance fast-forwards to. The preservation is
surfaced to the operator, never silent (FR-002).
The three-way rule (D-A3). "Newer" is a base/target/lane comparison, never
mtime: the target wins a path iff the target diverged from
merge-base(target, source) while the lane copy is base-or-ancestor (the target
evolved, the lane is stale). When both sides advanced the same path, the tiebreak
is the last-commit committer-date, with the target winning on a strictly-later
date or a tie (the conservative "do not clobber the target" default). This is
topology-safe by construction: on single_branch/LANES (lane ≠ base,
target = base) the lane correctly wins with no special case.
Note for epic #2907 (classifier-driven conflict taxonomy). D-A1 drives squash
conflict resolution off mission_runtime.kind_for_mission_file — the #2709
"target-newer canonical state is reconciled, not replaced" principle extended
from meta fields (the _TARGET_AUTHORITATIVE_META_FIELDS driver) to
primary-artifact-kind files. This is the #2709 → #3942 lineage. It also
establishes the disposition of the dead module
src/specify_cli/merge/conflict_resolver.py (ConflictType /
classify_conflict / resolve_owned_conflicts): it is a competing conflict
taxonomy, reachable only through the merge/__init__.py re-export (the live
auto-rebase classifier is the different module merge/conflict_classifier.py).
The #3942 fix deliberately did not wire into it. It should be retired as
part of #2907's taxonomy consolidation, so the classifier authority stays single.
Track B — the read seam (which surface a reader trusts)
Seam: the merged_at completion marker, written by
src/specify_cli/merge/baseline.py::record_baseline_merge_commit, and consumed by
src/specify_cli/status/lifecycle.py::is_mission_merged, which the surface
resolver's primary re-anchor (coordination/surface_resolver.py
_primary_mission_is_completed) and the runtime bootstrap short-circuit
(runtime_bridge.py) gate on.
When a mission merges, a diverged coordination husk can survive. The primary-wins
guard is supposed to re-anchor reads to the PRIMARY surface once the mission is
merged — but it gates on meta["merged_at"], whose production writer had been
deleted in #2258 (6f46cf6bb6) and never re-added. The guard was dormant, so
the divergence resurfaced: retrospect and doctor (which now share
resolve_status_surface) could still read the stale husk.
The fix (D-B1) is one coupled change:
- Restore the writer for
meta["merged_at"](a UTC ISO datetime; plusmerged_commitfor provenance) as a sibling of the baseline-commit writer inmerge/baseline.py— the meta-write authority — not indone_bookkeeping.py(which writes nothing tometa.json; a write there would be a boundary leak). The executor is untouched. The driver's_TARGET_AUTHORITATIVE_META_FIELDSalready declaresmerged_atas the marker, so this is canonical reuse, not a new authority. - Make the guard reopen-aware. Because nothing today clears
merged_aton reopen andis_mission_mergedwas presence-only, simply restoring the writer would make a reopened mission read as merged forever across three consumers. Sois_mission_mergedis now event-sourced: a mission is merged iffmerged_atis present AND noMissionReopenedevent postdates it (reusing the adjacent_last_reopen_atmachinery — no meta-mutating clearer of its own). A re-merge re-stamps a freshmerged_atthat postdates the re-open, restoringTrue.
Note for epic #2160 (single canonical post-merge authority). D-B1 chose "override the stale husk on primary" over "freshen the husk". This keeps the single canonical post-merge authority on the PRIMARY surface — the husk is never re-blessed as a second read home; the guard re-anchors past it by data. This is a coord-authority decision for the #2160 owner to ratify.
#4091 disposition (D-B2 — separable, verified-already-fixed). #4090 is fixed
by the marker alone. #4091 is a distinct measure mismatch: event_count is by
definition the count of unique WPStatusChanged transition events, which is
< len(status.events.jsonl) (annotations, lifecycle, and decision events are not
transitions). _project_status_bookkeeping_to_target
(merge/bookkeeping_projection.py) already unions and re-reduces, and its union
branch runs only under a coord husk. WP05 stood up a red-first repro on a coord
fixture and confirmed event_count is already correct on HEAD; #4091 is
therefore verify-and-close, guarded by a characterization test (building a fix
test on already-green code would be the green-regression trap).
Shared-vs-disjoint verdict
Verdict: disjoint code seams, one thematic spine.
The two tracks are file-disjoint and were implemented in parallel:
- Track A writes:
lanes/merge.py+merge/planning_recency.py. - Track B reads:
merge/baseline.py+status/lifecycle.py.
There is no single unifying code seam that both fixes route through — the squad's finding, borne out by implementation. The write half amends a git commit; the read half event-sources a lifecycle predicate. They share no call path.
What they do share is the partition doctrine and its classifier. Both are answers to "what is authoritative post-merge, and on which surface":
- Write half — which bytes win per artifact: PRIMARY-partition planning files are target-authoritative when the target is newer; driver-covered bookkeeping keeps its reconcilers.
- Read half — which surface a reader trusts: once merged, reads re-anchor to the PRIMARY surface; the diverged coord husk is overridden, not trusted.
mission_runtime.kind_for_mission_file / is_primary_artifact_kind is the
candidate shared spine: the same classifier that says a path is
PRIMARY-partition-authoritative is what tells Track A not to overwrite it and
Track B not to read a lane as its post-merge home. The spine is thematic and
doctrinal, not a shared runtime seam. Calling the tracks "the same bug" would
overclaim; calling them unrelated would miss that a single partition authority
governs both. They are genuinely disjoint-but-thematically-shared, unified by
the classifier + partition doctrine rather than by code.
Traceability
| Requirement | Delivered by | Seam |
|---|---|---|
| FR-001/FR-002 (write authority + divergence report) | Track A / WP01–WP02 (#3942) | lanes/merge.py, merge/planning_recency.py |
| FR-003 (preserve driver reconciliation) | Track A (unchanged _MERGE_DRIVERS) |
driver registry / gate artifacts |
| FR-004+ (read authority) | Track B / WP03–WP04 (#4090) | merge/baseline.py, status/lifecycle.py |
| #4091 verify-and-close | WP05 (D-B2) | merge/bookkeeping_projection.py |
| FR-009 (this synthesis) | WP06 | this document |
Epic advances recorded here: #2907 (classifier-driven taxonomy; retire dead
conflict_resolver.py), #2160 (husk-override authority note to ratify).