Implementation Plan: Squash Merge Target Safety

Architectural intent

Keep one branch-integration authority in src/specify_cli/lanes/merge.py and make the isolated squash operation reusable in two modes:

merge --dry-run ─┐
                 ├─> isolated squash simulation ─> conflict result
real merge ──────┘              │
                                ├─ custom drivers for governed artifacts
                                └─ planning-recency resolver for eligible paths

real merge + clean result  ─> commit temporary tree ─> advance target ref
dry-run or conflict        ─> discard temporary tree ─> refs unchanged

The simulation uses normal git merge --squash <mission> semantics. It never passes a global favour-mission option. A failed Git merge is inspected for unmerged paths. Only paths explicitly owned by the existing target-newer planning policy may be reconciled; every remaining path becomes a blocker.

Components

1. Squash simulation and conflict result

Owner: src/specify_cli/lanes/merge.py

_merge_branch_into into a narrow helper that runs inside the existing temporary worktree and ephemeral merge-driver activation.

diagnostic. Do not expose temporary paths.

target_newer_primary_artifacts and three-way target-favouring helper.

before commit and advance_branch_ref.

  • Extract the squash command and conflict inspection from
  • Add a typed, immutable result carrying sorted unresolved paths and a concise
  • Resolve eligible target-newer planning conflicts with the existing
  • On unresolved conflict, abort/discard the temporary merge and raise/return
  • Preserve no-op squash and retry behavior.

2. Read-only forecast seam

Owners: src/specify_cli/lanes/merge.py, src/specify_cli/merge/forecast.py

preview mode and always discards the worktree.

local refs exist. The existing missing-mission-branch preview remains compatible.

and exit 1. Human output names branches, paths, and rebase/manual-resolution remediation.

  • Add a public forecast helper that runs the same isolated squash primitive in
  • Invoke it from run_dry_run_forecast only when strategy is squash and both
  • On conflict, emit a stable TARGET_BRANCH_CONTENT_CONFLICT blocked payload
  • Leave the clean payload key set unchanged.

3. Regression coverage

Owners: tests/lanes/test_merge.py, tests/merge/test_forecast_seam.py, and one CLI-level regression module if the public CLI assertions need a fuller fixture.

ATDD order:

1. Add a same-hunk source-conflict test that currently fails because target content is silently replaced. 2. Add a dry-run conflict test that currently returns a ready payload. 3. Add disjoint same-file and target-byte/ref invariants. 4. Re-run existing merge-driver, #3942 planning-recency, no-op/resume, golden payload, and non-squash strategy tests.

4. Operator documentation

Owners: docs/guides/how-to/recovery/troubleshoot-merge.md, docs/changelog/CHANGELOG.md

  • Explain the fail-closed default squash behavior and remediation.
  • Record the RC5 bug fix and reference #4892.

Data flow

1. Resolve persisted mission and target branches from lanes.json. 2. Create a detached temporary worktree at the target tip. 3. Activate registered merge drivers for that operation. 4. Run normal squash integration of the mission ref. 5. If Git reports conflicts, enumerate unmerged repo-relative paths. 6. Reconcile only target-newer planning paths; inspect unmerged paths again. 7. If any remain, return a deterministic blocker and discard the worktree. 8. In preview mode, discard even a clean simulation and report readiness. 9. In execute mode, commit the staged squash result and atomically advance the target ref through the existing advance_branch_ref seam.

Error and recovery contract

branches, worktrees, and retention state.

resolve the named files, re-run dry-run, then run merge.

  • Diagnostic code: TARGET_BRANCH_CONTENT_CONFLICT.
  • State preserved: source ref, target ref, primary checkout, WP lifecycle,
  • Suggested recovery: update/rebase the mission against the current target,
  • No automatic ordinary-source side selection is offered.

Validation matrix

SurfaceRequired evidence
Same-hunk ordinary sourceFailure; target SHA and bytes unchanged
Disjoint same-file editsSuccess; both edits present
Dry-run conflictExit 1; deterministic JSON/human blocker; refs unchanged
Dry-run cleanExisting exact success keys and exit 0
Custom-driver artifactExisting reconciliation tests green
Target-newer planningExisting #3942 tests green
Merge/rebase strategiesExisting behavior green
QualityFocused pytest, Ruff, mypy, project fast and architectural gates

Risks and mitigations

current history-aware planning authority and retain its regression suite.

information-attributes leak tests.

temporary-directory paths in public diagnostics.

squash when both refs exist; correctness takes precedence at this destructive boundary.

  • Planning conflict becomes a regression: resolve only paths selected by the
  • Preview drifts from execution: both modes call the same simulation helper.
  • Temporary activation leaks: retain the existing ExitStack teardown and
  • Error details become unstable: sort and normalize conflict paths; avoid raw
  • Preview becomes expensive: it performs one local isolated merge only for

Rollback

The implementation is localized to the merge simulation and forecast call site. If rollback is required, revert the mission commit; no data migration, schema change, or persisted-state transformation is introduced.