Context and Problem Statement
PR #3076 shipped the write-side placement seam but deferred the CONSOLIDATED half of it.
ADR 2026-07-23-1
declared TopologySurface.CONSOLIDATED as a member and gave translate_surface a
total arm for it — every TopologySurface member has a translation entry, asserted
by assert_surface_totality. But SurfaceLocations.consolidated (the field that arm
reads) has never been populated in production: translate_surface(CONSOLIDATED, …)
raises ValueError rather than resolving anything, because no caller has ever supplied
the location. resolve_placement_only instead resolves PRIMARY-kind writes to the
mission's Target Ref (target_branch) unconditionally, regardless of lifecycle phase.
That gap is now load-bearing (issue #3033, P0). After a mission is consolidated
(lane-consolidation, "E1") and then published to trunk ("E2"), the Target Ref branch is
deleted — spec-kitty merge's local consolidation only ever keeps lane branches
deleted, but the operator-driven publish-to-trunk step (PR + merge to the Primary Branch)
deletes the Target Ref itself. Post-consolidation evidence writes (an acceptance-matrix
verdict, a tracer finding, an issue-matrix update, a retrospective) still resolve against
that deleted ref, so safe-commit's HEAD-vs-ref guard can never be satisfied and the
write fails or is refused. spec-kitty review --mode post-merge is the operator-visible
symptom; it performs no commit itself, so the fix is not there.
A second, narrower gap sits underneath the first: the codebase's overloaded merge (a
three-sense term already disambiguated by ADR
2026-07-23-2 —
lane consolidation / branch integration / publish-to-origin) has no single-word
canonical replacement for the lane-consolidation sense. Every line of new code or prose
that needs to name that sense precisely has to reach for a multi-word disambiguating
phrase ("local lane consolidation") rather than one word, which is exactly the friction
that lets "merge" creep back in for that sense (mission #3080). This mission
(post-merge-write-authoring-finish-01KYRRM5) lands only the "do not make it worse"
foundation of #3080 — this ADR's terminology decision, a glossary update, and a CI
drift-ratchet guard — sequenced before any of the functional CONSOLIDATED-wiring work.
The full command/code/docs rename (spec-kitty merge, MergeState,
baseline_merge_commit, …) stays #3080; this ADR explicitly does not perform it.
Decision Drivers
- Single write resolver (ADR 2026-06-24-1
C-006) —
resolve_placement_onlyis the one write authority; the fix must not add a parallel/second resolver, and must not thread a phase parameter throughPlacementSeam.write_targetorcommit_for_mission(both must independently re-derive the identical phase from the same durable signal). - Squash-robustness — a squash publish-to-trunk rewrites history, so the E1 consolidation commit is not a commit-ancestor of the Primary Branch tip; any resolution keyed on commit ancestry produces a false refusal on the common publish path.
- Terminology precision, extended — ADR 2026-07-23-2 already applies "always name
the sense" discipline to
merge; this ADR extends that discipline by giving the lane-consolidation sense a canonical single word, so future code and prose can comply cheaply instead of reaching for a phrase every time. - Scope discipline (C-005, C-012) — land the CONSOLIDATED wiring and the terminology foundation without widening into the full #3080 rename or into per-artifact-kind branching inside the resolver (which would reintroduce the call-site kind-conditioning C-006 forbids).
Decision Outcome
Chosen: wire TopologySurface.CONSOLIDATED as the phase-resolved integrated-tree
surface, and adopt consolidate / consolidation as the canonical term for the
lane-consolidation sense. Two decisions, recorded together because the second becomes
load-bearing exactly where the first makes it real: wiring CONSOLIDATED is the point at
which the codebase needs to say "the tree lane consolidation produced" precisely and
repeatedly, in artifact names, log messages, and error text.
Decision 1 — CONSOLIDATED surface wiring (design)
SurfaceLocations.consolidated is populated so translate_surface(CONSOLIDATED, …)
resolves the integrated tree, wherever it currently lives:
- Phase is derived internally, inside
resolve_placement_only. No phase parameter is threaded throughPlacementSeam.write_targetorcommit_for_mission— both re-derive the identical phase from the same durablemeta.jsonsignal (baseline_merge_commit, landed on the Target Ref at E1) plus the Target Ref's existence. This preserves the single-write-resolver invariant: a threaded parameter would let the probe and the materializer disagree (probe says "routable", commit resolves to the deleted Target Ref — a split-brain that reproduces #3073-shaped residue).resolve_placement_onlyis the only resolver that computes phase; it is an input the resolver derives, not a second resolver. - E1 (lane-consolidated, Target Ref still exists):
consolidated= the Target Ref tree — the same tree PRIMARY-kind writes already resolve to today. No behavior change on this leg. - E2 (published-to-trunk, Target Ref deleted):
consolidated= the repository-root checkout, gated by a content-presence predicate — the mission's own committed artifact tree is present atHEAD(e.g. viagit cat-file -e HEAD:kitty-specs/<mission_slug>/meta.json, or an equivalent committed marker) — not commit ancestry. A squash publish-to-trunk rewrites history so the E1 consolidation commit is not a commit-ancestor of the Primary Branch; content-presence succeeds regardless of commit topology (squash, rebase, or merge-commit), and it does not hard-code "main" — it accepts any checkout that carries the consolidated content. - E2 disambiguation — "Target Ref absent" alone does not mean E2: a pre-
consolidation mission whose Target Ref was never materialized also has an absent
branch. E2 requires
baseline_merge_commitpresent and the Target Ref absent and terminal-completion evidence a consolidation produces (all work packagesdonein the status event log and/ormission_numberassigned). The foundation readerget_feature_target_branchstays unrouted — the branch-existence probe lives in the phase reader this wiring adds, not in that reader. - Off-checkout refusal (FR-006). When the phase signal says "consolidated" but the
content-presence check fails on the current checkout, the write refuses with a
recovery instruction rather than forcing a checkout (C-004). The named recovery
branch is derived, not hard-coded and not a SHA: it reads the same
branch-derived default the codebase already uses elsewhere for "the Primary Branch,
whatever it is called" (
git symbolic-ref refs/remotes/origin/HEAD, the pattern already in production atsrc/specify_cli/core/git_ops.py:_origin_head_branchandsrc/specify_cli/git/protection_policy.py) — never a literal"main"string and never a commit SHA, so the message stays correct on a repository whose default branch is notmain. - E2 routing is scoped, not blanket (operator HiC decision, recorded here for
citability). The CONSOLIDATED-surface routing this ADR wires applies to
PRIMARY-kind writes and to the three coord-partition kinds named in scope —
ISSUE_MATRIX,TRACER_FILE,ACCEPTANCE_MATRIX— only.STATUS_STATEandDECISION_LOGare not re-routed to CONSOLIDATED in E2: although both kinds pass through the same sharedresolve_placement_only(per C-005, sharing one resolver is the point, not an exception to it), their pre-mission resolution is left unchanged by this wiring. This is deliberately non-regressive (C-005):DECISION_LOGis in-mission and is not an E2 write target by design, andSTATUS_STATE's existing resolution is out of this mission's scope (emit.pyplacement-routing, #3071, is explicitly excluded). Scoping the CONSOLIDATED destination to the four named kinds, rather than making every shared-resolver caller inherit E2 routing implicitly, avoids the per-kind branching C-006 forbids while still keeping the two untouched kinds verifiably unaffected. - No new field. This is not a ref-swap around CONSOLIDATED and it introduces no new
merge_target_branchfield;translate_surface's existing total arm forCONSOLIDATEDalready exists (ADR 2026-07-23-1) — this decision is its first production wiring.
This decision extends ADR 2026-07-23-2, which named the post-consolidation phase and
the CONSOLIDATED surface and wired only the acceptance-verification side (negative
invariants deferred to a post-consolidation Op); it did not wire the write-routing side.
It re-affirms ADR 2026-06-24-1's C-006 (single write resolver): CONSOLIDATED is populated
inside the existing resolver, not by a parallel one.
Decision 2 — consolidate / consolidation is canonical for the lane-consolidation sense (terminology, #3080 foundation)
consolidate / consolidation becomes the canonical term for merge Sense 1 —
the spec-kitty merge local operation that folds completed lane branches into the mission
branch (glossary: Lane Consolidation).
The other two senses are unaffected and keep their existing canonical names:
merge/ "branch integration" stays canonical forgit merge(Branch Integration / Git Merge).- "publish" stays canonical for the operator-only push-to-origin step (Publish to origin/main).
The bare word "merge" for the lane-consolidation sense becomes a legacy alias:
existing occurrences are grandfathered (they do not need to be rewritten by this
mission), but new code, new symbols, and touched prose must say "consolidate" /
"consolidation" for this sense (boyscouting, C-012). This is the same disambiguation
discipline ADR 2026-07-23-2 already applied when it named CONSOLIDATED instead of
MERGED for the surface member — this ADR gives that same choice a canonical prose word.
Foundation now, full rename in #3080. This ADR and its accompanying glossary update
(FR-015) and CI drift-ratchet guard (FR-016) are the entire scope landed here. They do
not rename spec-kitty merge, MergeState, baseline_merge_commit, or any other
existing public symbol — that full command/code/docs rename is issue #3080's scope.
Landing it here would explode this mission's scope for no P0 benefit; the foundation
exists so the overload cannot get worse while #3080 is pending, not so it is fully
resolved today.
Consequences
Positive
- #3033 (P0) is fixed at its root: post-consolidation evidence writes resolve through a wired CONSOLIDATED surface instead of an unconditionally-resolved, possibly-deleted Target Ref.
- The fix is squash-robust: a squash publish-to-trunk does not produce a false refusal, because E2 resolution never depends on commit ancestry.
STATUS_STATE/DECISION_LOGresolution is unchanged, so the shared-resolver consequence (C-005) is non-regressive by construction rather than by incidental luck.- Future code and prose gain a canonical, single-word way to name the lane-consolidation sense, lowering the cost of complying with the terminology canon and making genuine new drift ("lane merge", "merge the lanes", …) visible to a CI gate (FR-016) instead of silently accumulating.
- No new abort path is introduced into consolidation (consistent with ADR 2026-07-23-2); the off-checkout case (Decision 1 §5) is a refusal at write time, not a consolidation- time gate.
Negative / accepted trade-offs
- The lane-consolidation sense of "merge" remains legally present in existing code and prose until #3080 lands the full rename — this ADR accepts a grandfathered baseline rather than forcing an immediate, scope-exploding rewrite.
- The content-presence predicate (Decision 1 §3) is a git subprocess call, only paid on the E2 leg (Target Ref absent) — NFR-004 requires it add no new subprocess on the pre-consolidation happy path, which the phase-gating in §1/§4 satisfies by construction but which a future change to the predicate must preserve.
Neutral
SurfaceLocations.consolidatedwas already declared (aPath | Nonefield) ahead of this ADR; wiring it does not change its type or its place in the dataclass.
Alternatives considered
- Record a new
merge_target_branch/consolidation_reffield at E1 and swap the Target Ref for it at E2. Rejected: unnecessary scope. The content-presence predicate needs no new durable field beyondbaseline_merge_commit, which already exists; a ref-swap is also the shape C-002 explicitly forbids ("NOT a ref-swap around CONSOLIDATED"). - Commit-ancestry check (
git merge-base --is-ancestor baseline_merge_commit HEAD) for E2. Rejected: breaks under a squash publish-to-trunk, the common publish mode — the exact false-negative this ADR's Decision 1 §3 exists to avoid. git branch --containsto find the checkout. Rejected: the merge-base is an ancestor of every descendant branch, so this is maximally ambiguous and cannot distinguish a valid CONSOLIDATED home from an unrelated descendant.- Route
STATUS_STATE/DECISION_LOGto CONSOLIDATED in E2 for symmetry with the other four kinds. Rejected: out of scope (C-005 excludesemit.pyplacement-routing, #3071) andDECISION_LOGis intentionally in-mission-only; routing them would be an unrequested behavior change smuggled in under "consistency."
- Do the full #3080 rename now, alongside the CONSOLIDATED wiring. Rejected: C-012
is explicit that this mission lands only the tidy-first foundation; renaming
spec-kitty merge/MergeState/baseline_merge_committouches a much larger, independently-scoped surface (command help text, CLI flags, serialized state shapes) that belongs to #3080, not to a P0 write-routing fix.
- Pick "integrate" instead of "consolidate" as the canonical word. Rejected: "branch
integration" is already the canonical name for
mergeSense 2 (thegit mergestep); reusing "integrate" for Sense 1 would create a new overload instead of resolving one.
More Information
Extends: 2026-07-23-2 — Post-Consolidation Deferral and External Enforcement of
Negative Invariants
(named post-consolidation / CONSOLIDATED and wired the acceptance-verification side;
this ADR wires the write-routing side and gives the lane-consolidation sense its
canonical word).
Re-affirms: 2026-06-24-1 — Kind- and Topology-Aware Artifact Placement
C-006 (single write resolver — phase is derived inside resolve_placement_only, never a
second resolver, never a threaded parameter).
Cites: 2026-07-23-1 — Surface Vocabulary: Two Domains and TopologySurface
Rename
(declared TopologySurface.CONSOLIDATED and its total translate_surface arm, wired by
this ADR).
Glossary: Lane Consolidation
canonicalizes consolidate / consolidation for this sense (FR-015); Branch
Integration / Git Merge
and Publish to origin/main are
unchanged.
Issues: #3033 (P0 — post-
consolidation writes fail against a deleted Target Ref; Decision 1),
#3073 (no-residue-on-refusal;
downstream of the CONSOLIDATED wiring, not this ADR's decision),
#3080 (the full consolidate
rename; this ADR lands only its foundation — Decision 2), #2653
(the original primary/merge disambiguation this ADR's terminology decision extends).
Mission: kitty-specs/post-merge-write-authoring-finish-01KYRRM5/ (WP01, FR-014/
FR-015/FR-016; the CONSOLIDATED wiring itself is executed by this mission's later work
packages — IC-03 through IC-07 of plan.md). Research: research.md D1-D5. Contract:
contracts/authoring-commands.md (Terminology foundation section).
Canonical seams: src/mission_runtime/resolution.py
(resolve_placement_only, SurfaceLocations, translate_surface,
resolve_artifact_surface), src/mission_runtime/artifacts.py
(TopologySurface.CONSOLIDATED, MissionArtifactKind), src/specify_cli/coordination/write_seam.py
(post-consolidation write routing), src/specify_cli/core/git_ops.py
(_origin_head_branch — the existing git symbolic-ref refs/remotes/origin/HEAD
pattern the FR-006 recovery message reuses).