Implementation Plan: Consolidate git merge-base/diff idiom
Branch: fix/merge-base-diff-ssot | Date: 2026-07-09 | Spec: spec.md Input: Feature specification from kitty-specs/merge-base-diff-ssot-01KX44SD/spec.md
Summary
Extract the five independent git merge-base → git diff --name-only copies (a post-plan brownfield squad corrected 4 → 5) onto one canonical surface in src/specify_cli/core/vcs/git.py — two primitives (git_merge_base, git_diff_names, the latter with optional pathspec + diff_filter) plus a HEAD-relative convenience (merge_base_changed_files) — and repoint the five call sites to it with zero behaviour change (NFR-001). The mechanics get direct tests (incl. a non-HEAD branch-target case and a range↔two-arg equivalence assertion); each site keeps a thin integration test. Behaviour-preserving strangler consolidation of a duplicated VCS primitive, per the 3.2.x no-shadow-paths / single-seam discipline.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: standard library only (subprocess, pathlib); no new third-party dependency. Target home src/specify_cli/core/vcs/git.py already exists (GitVCS + module-level git helpers). Storage: N/A (operates on a git working tree via subprocess) Testing: pytest; direct unit tests for the new helper (subprocess mocked / real temp-repo), thin integration tests retained at each of the 4 call sites. Run under the repo's parallel profile (PWHEADLESS=1 pytest tests/ -n auto --dist loadfile). Target Platform: Linux/macOS dev + CI (any platform with git on PATH) Project Type: single (Python CLI package under src/specify_cli/) Performance Goals: unchanged — same two git subprocess calls per invocation as today; no added shell-outs. Constraints: behaviour-preserving (C-001); no expected-value test assertion edits (NFR-001); ruff + mypy zero-issue, complexity ≤15 (NFR-003); helper new-code coverage ≥ repo gate (NFR-004); only incidental hardening allowed is standardizing encoding="utf-8", errors="replace", check=False (C-002). Scale/Scope: 1 new helper surface (~3 functions) + 5 call-site repoints + 1 secondary tidy (FR-007). ~6 files touched in src/, plus their tests. Two touched files are god-modules (tasks_move_task.py ~1848 LOC, acceptance/__init__.py ~1751 LOC) — repoint only, no scope expansion into their other debt.
Charter Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
Charter present (.kittify/charter/charter.md); plan-action context loaded in compact mode (no action-scoped directives returned). Relevant standing orders and how this plan satisfies them:
- Canonical single authority / no shadow paths — this mission is a consolidation onto one authority; it removes duplication rather than adding a parallel path. ✅ (the whole point)
- Campsite / brownfield logical-duplication consolidation — one operation duplicated across 4 sites → one canonical seam. ✅ aligns with the doctrine directly.
- Tests as scaffold, red-first discipline — the helper gets direct tests exercising all failure branches; site tests keep observable-contract assertions (not re-proving git mechanics). Behaviour-preserving, so existing behavioural tests stay green unchanged (NFR-001). ✅
- Architectural gate discipline — full
tests/architectural/sweep + terminology guard at closeout; ruff/mypy clean on new + boy-scout scope. ✅ (recorded in tasks) - Git/workflow discipline — PR-bound; lands via PR to origin/main, never direct push. ✅
No charter violations → Complexity Tracking below is empty.
Project Structure
Documentation (this mission)
kitty-specs/merge-base-diff-ssot-01KX44SD/
├── plan.md # This file
├── spec.md # Committed (cec9b25)
├── research.md # Phase 0 output (this command)
├── data-model.md # Phase 1 output (this command)
├── quickstart.md # Phase 1 output (this command)
├── contracts/ # Phase 1 output (this command)
└── tasks.md # Phase 2 output (/spec-kitty.tasks — NOT created here)
Source Code (repository root)
src/specify_cli/
├── core/vcs/git.py # + git_merge_base, git_diff_names(pathspec,diff_filter), merge_base_changed_files (NEW surface)
├── lanes/stale_check.py # FR-005: replace _git_merge_base/_git_diff_names with helper primitives
├── acceptance/__init__.py # FR-008: _changed_workflow_files -> merge_base_changed_files(pathspec=".github/workflows", diff_filter="AMR") [5th copy]
└── cli/commands/agent/
├── tasks_move_task.py # FR-002: _mt_pre_review_changed_files -> merge_base_changed_files
├── tasks_shared.py # FR-003: first pass -> helper (pathspec=kitty-specs/), keep content re-check
└── tasks_dependency_graph.py # FR-004: upstream check -> git_merge_base + git_diff_names(mb, check_branch) [two-ref, NOT the HEAD convenience]
# FR-007 (secondary): src/specify_cli/review/pre_review_gate.py # ScopeResult.from_override + retire hand-built construction in tasks_move_task FR-004 override tier
tests/
├── core/vcs/ (or tests/specify_cli/core/vcs/) # NEW direct helper test (all 5 FR-006 branches)
├── lanes/test_stale_check*.py # thin integration retained
└── specify_cli/... (move_task / shared / dependency_graph site tests) # patch-target repoint only, no expected-value edits
Structure Decision: single Python package. The canonical surface lives in the existing core/vcs/git.py (already the home of git subprocess helpers), keeping the VCS primitive in the VCS module and out of the four consumer modules.
Complexity Tracking
No Charter Check violations — none.
Implementation Concern Map
> Concerns are NOT work packages. /spec-kitty.tasks translates these into WPs.
IC-01 — Canonical merge-base/diff surface + direct tests
- Purpose: Create the single source of truth for the merge-base→changed-files idiom so the git mechanics are defined and proven exactly once.
- Relevant requirements: FR-001, FR-006 (helper's own branches), NFR-002, NFR-003, NFR-004, C-002.
- Affected surfaces:
src/specify_cli/core/vcs/git.py(addgit_merge_base,git_diff_nameswith optionalpathspec+diff_filter,merge_base_changed_files); new direct unit test module. - Sequencing/depends-on: none (must land before or with the repoints; they import from it).
- Mandatory test fences (from post-plan adversarial squad): FR-006 must include (1) a non-HEAD branch-target case —
git_diff_names(repo, mb, <branch≠HEAD>)— so a lazy swap oftasks_dependency_graphto the HEAD convenience fails red (fences F1, the single most likely silent behaviour change, currently uncaught by any test); (2) a range↔two-arg equivalence assertion (helper two-arg output == rawgit diff --name-only <mb>..HEADon a real temp repo), documenting the silent rewrite three sites undergo; (3) adiff_filtercase for the acceptance 5th copy. - Risks: getting the primitive signatures general enough to serve all 5 variants (2-ref vs HEAD-relative merge-base;
..HEADvs..branchdiff target; optional pathspec; optional--diff-filter) without a god-signature; standardizing subprocess kwargs (C-002) without altering output.
IC-02 — Low-risk HEAD/branch repoints
- Purpose: Route the two straightforward copies through the surface.
- Relevant requirements: FR-002 (
tasks_move_task._mt_pre_review_changed_files→merge_base_changed_files), FR-004 (tasks_dependency_graph→git_merge_base+git_diff_names(mb, check_branch)), NFR-001, C-001. - Affected surfaces:
tasks_move_task.py,tasks_dependency_graph.py. Tests largely survive unedited (global-subprocessordered mocks + symbol-identity batteries; verified by the squad — patch-target edits mostly unnecessary). - Sequencing/depends-on: IC-01.
- Risks: F1 (CONFIRMED) —
tasks_dependency_graphdiffs..check_branch, NOT..HEAD; it MUST use the two-ref primitivegit_diff_names(mb, check_branch), never the HEAD convenience (which would silently invert which commits are inspected). Named acceptance criterion + the IC-01 branch-target test guard this.
IC-02b — Higher-touch repoints (isolate for review)
- Purpose: The two copies with extra behaviour to preserve get their own reviewable unit with named acceptance criteria.
- Relevant requirements: FR-003 (
tasks_sharedtwo-pass), FR-005 (stale_checkprimitive pair, the only site gaining C-002 encoding/errors + the only site that deletes symbols), FR-008 (acceptance._changed_workflow_files, the 5th copy with--diff-filter=AMR+ three-dot), NFR-001, C-001, C-002, C-003. - Affected surfaces:
tasks_shared.py,lanes/stale_check.py,acceptance/__init__.py. - Sequencing/depends-on: IC-01.
- Risks: (a)
tasks_shared— preserve thestartswith("kitty-specs/")filter, dedup, and FR-007/#2274 content re-check (tasks_shared.py:~691-705) verbatim; only the diff call delegates. (b)stale_check— assert real-repo output byte-identical after adoptingencoding/errors(C-002); deleting_git_merge_base/_git_diff_namesmay trip a dead-symbol/architectural guard — update it in-WP. (c)acceptance— three-dot...HEAD→ helper two-arg equivalence is safe only because the merge-base is an ancestor of HEAD; pin it; do NOT expand into the god-module's other debt.
IC-03 — Secondary: ScopeResult.from_override tidy (deferrable)
- Purpose: Retire the hand-built
ScopeResultconstruction intasks_move_task's FR-004 override tier behind afrom_overrideclassmethod. - Relevant requirements: FR-007.
- Affected surfaces:
src/specify_cli/review/pre_review_gate.py,tasks_move_task.pyoverride tier. - Sequencing/depends-on: independent of IC-01/IC-02; may split to a follow-up WP or fast-follow if it widens the diff.
- Risks: scope creep — if
from_overridepulls in gate-policy changes it should be deferred, not expanded here. F7:tasks_move_taskcarries amodule_defs == set(_MOVE_SET)symbol-identity guard (test_tasks_move_task_seam.py:456); adding/removing any module-level symbol here fails it — keep FR-007 in its own WP so this doesn't masquerade as an unrelated breakage in IC-02.