Naming/Identity SSOT Strangler — Implementer Feasibility (python-pedro lens)
Profile: python-pedro (implementer — feasibility, call-graphs, test scaffolding).
Branch: research/naming-identity-ssot-strangler @ spec-kitty 3.2.0. No commits/switches made.
Date: 2026-06-16.
Directives applied (python-pedro)
- DIR-010 Specification Fidelity — verify every issue claim against real code before scoping; do not carry issue prose that the merged 3.2.0 code already invalidated.
- DIR-024 Locality of Change — each consolidation stays at its own seam; no drive-by widening (#1993 explicitly out-of-scope for #1991).
- DIR-025 Boy Scout — when routing a compose site, tighten the ratchet allow-list in the SAME WP (shrink, don't grow).
- DIR-030 Test+Typecheck Gate — ruff/mypy clean on new code; every new branch/helper gets a focused test in the same WP.
- DIR-034 Test-First — red-green-refactor; the #1993 extraction is a TDD poster child (write the zero-mock test first, then extract).
- Tactic: test-scaffolding-as-design-smell — the 12-mock test in
TestImplementCoordTopologyLanesJson(tests/agent/test_implement_command.py) is the design signal driving #1993; the fix is a pure seam, not more mocks.
HEADLINE FINDING — most of the issue surface is ALREADY SHIPPED in 3.2.0
I grepped every claim. Three of the seven issues are functionally complete on this branch, and a fourth (#1899) is ~85% done. Carrying their issue prose verbatim into the spec would produce phantom scope. Code-grounded status:
| Issue | Issue's premise | Verified code reality | Real residual |
|---|---|---|---|
| #1888 finalize-tasks existence-check | "owned_files never existence-checked" | DONE. validate_glob_matches() (src/specify_cli/ownership/validation.py:319) already classifies literal-vs-glob, hard-errors on literal zero-match with nearest-match suggestion, soft-warns on glob zero-match, suppresses via create_intent. Wired at cli/commands/agent/mission.py:3348-3371 (shipped as #1886, T015-T018, FR-006). |
NONE / verify-only. #1888 is a duplicate of the already-merged #1886. Close as fixed after a confirming test for the exact #1888 typo scenario. |
#1915 _merge_dependency_lane_tips atomicity |
"non-atomic; earlier dep merge survives later conflict" | DONE — provably. Snapshot+reset (option (a)) implemented: _current_head() snapshot at worktree_allocator.py:291, git reset --hard <pre_loop_ref> at lines 336-342 on any conflict. Multi-dep regression tests exist: test_1915_later_dep_conflict_rolls_back_earlier_dep_merge + test_1915_all_clean_deps_still_merge (tests/lanes/test_worktree_allocator_atomicity.py:193,227). |
NONE. Both the fix AND the ≥2-dep regression test the issue demands are present. Close as fixed. |
| #1899 worktree dir-name grammar seam + 4th ratchet | "grammar seam + 4th ratchet still open" | ~85% DONE. worktree_dir_name()/worktree_path()/mission_dir_name() all exist (lanes/branch_naming.py:484,516,532). 4th ratchet tests/architectural/test_no_worktree_name_guess.py exists and is comprehensive (walks /-division chains + name-shape composes, with file:line allow-list + strictness proof). |
Residual = the 3 allow-listed sites only (identical to #2000's list) + the surface_resolver R2 short-circuit dedupe rider. The "seam + ratchet" infrastructure is built; only the migration tail remains. |
#2000 route 3 out-of-scope <slug>-<mid8> composes |
open, accurate | Confirmed: mission_creation.py:321, worktree.py:367, worktree.py:370 are allow-listed in the ratchet (test_no_worktree_name_guess.py:113-115). |
OPEN — this is the real work. Mechanical routing + allow-list shrink. |
#1993 extract _resolve_lanes_dir() pure seam |
open, accurate | Confirmed inline at implement.py:974-982 (_lanes_feature_dir). The cited 12-mock test is TestImplementCoordTopologyLanesJson in tests/agent/test_implement_command.py. |
OPEN — real work. Pure extraction; high test-ROI. |
#1971 3-way locate_project_root split-brain |
"3 defs, divergent authority" | PARTIALLY DONE. project_resolver.locate_project_root now delegates to the authoritative paths.locate_project_root (deferred import, project_resolver.py:23, landed as #1971/commit 8431dd931). The behavior split-brain (lint/helpers ignoring SPECIFY_REPO_ROOT) is already closed. |
Residual = collapse the hops + dedupe. __init__.py:52 → project_resolver → paths is a 2-hop indirection; core/__init__ re-exports the wrapper. True consolidation = point the 4 project_resolver callers + __init__ wrapper at paths and delete the wrapper (verifying no import cycle — the reason it was kept). |
| #1878 umbrella (coord placement/identity strangler) | epic, 8 deferred items | Out of this mission's core SSOT lane; it's the coordination-branch strangler (different seam family). Items 3 (worktree-naming allocator unification) + 5 (AC10 lint expansion) touch the same naming seam and could be co-scoped. | Reference only — pull items 3 & 5 if the mission has headroom; the rest is a separate coordination mission. |
Net: the cohesive 3.2.1 mission is ~2 substantive issues (#2000+#1899-tail, #1993) + 1 cleanup (#1971-tail), NOT seven. #1888 and #1915 should be closed as already-fixed (file confirming-only tests if desired). This is the single biggest planning input — the spec should NOT size for 7.
Per-issue feasibility
#2000 — route 3 <slug>-<mid8> composes through the seam · S · low risk · mechanical
Targets (verified):
core/mission_creation.py:319-321—human_slug = strip_numeric_prefix(mission_slug); mission_slug_formatted = f"{human_slug}-{mid8(mission_id)}"→ the kitty-specs dir name. Replace withmission_dir_name(mission_slug, mid8=mid8(mission_id))(note:mission_dir_namealready does thestrip_numeric_prefix+ idempotent-suffix internally, so the localhuman_slugline is also subsumed → net LOC reduction).core/worktree.py:365-370—human_slug = strip_numeric_prefix(mission_slug); branch_name = f"{human_slug}-{mid8(mission_id)}"; worktree_path = repo_root / WORKTREES_DIR / branch_name. This is the mission worktree (not lane). Replace name compose withmission_dir_name(...); therepo_root / WORKTREES_DIR / namejoin is a legit directory join (leave it, or use amission_worktree_path()helper if one is added — see #1899).- All 3 sites are git-traced to #601/
c16291214, predate the seam, currently allow-listed intest_no_worktree_name_guess.py:113-115.
Test strategy: golden-value parity test — assert mission_dir_name("057-foo", mid8="01KV6510") == "foo-01KV6510" already covered; add a parity assertion that the OLD inline f-string output == mission_dir_name(...) for the legacy AND embedded inputs (byte-identical proof). Then remove the 3 entries from _ALLOWED_SITES and let the ratchet prove no regression (DIR-025).
No-shadow-path risk: mission_dir_name strips the NNN- prefix (canonical grammar). The inline composes ALSO call strip_numeric_prefix first → byte-identical. CONFIRM there is no pre-existing on-disk dir created with the un-stripped slug at these sites (there isn't — both call strip). The hazard is the coordination read path, which composes VERBATIM (coord_mission_dir_name, no strip) — do NOT route these creation sites through the coord variant. mission_creation/worktree are canonical-create → mission_dir_name, correct.
#1899 — worktree dir-name grammar seam + 4th ratchet · S (residual only) · low risk
The seam + ratchet are already built (3.2.0). Residual is exactly:
- The 3 #2000 sites (full overlap — see ownership map).
- Dedupe rider: surface_resolver R2 short-circuit hand-rolls the
.worktrees-segment test instead of calling its own classifier (alphonso Q1 nit). Target:src/specify_cli/coordination/surface_resolver.py— grep the R2 short-circuit for an inline".worktrees"literal and route through the classifier. S, low risk. - Optional: add a
mission_worktree_path()emit-helper soworktree.py:370'srepo_root / WORKTREES_DIR / namejoin also goes through the seam (parallelsworktree_path()for lanes). Verify against the 5th-ratchet-shape note (test_no_worktree_name_guess.py:18-29already anticipates a "no.worktrees/literal" shape).
Test strategy: the ratchet self-tests the allow-list shrink. Add a unit test for the surface_resolver classifier call (zero-mock, tmp_path).
#1993 — extract _resolve_lanes_dir() pure seam · S/M · low risk · design-relieving
Exact inline logic (implement.py:974-982):
_lanes_feature_dir: Path = feature_dir
if not (feature_dir / "meta.json").exists():
from specify_cli.missions._read_path_resolver import primary_feature_dir_for_mission
primary_candidate = primary_feature_dir_for_mission(repo_root, mission_slug)
if (primary_candidate / "meta.json").exists():
feature_dir = primary_candidate
IMPLEMENTER CORRECTION to the issue's proposed code: the issue's snippet uses resolve_feature_dir_for_mission + candidate_feature_dir_for_mission and reassigns _lanes_feature_dir. The actual code keeps _lanes_feature_dir = feature_dir (the coord-aware surface already resolved upstream) and on the meta-missing branch reassigns feature_dir (not _lanes_feature_dir) to primary_feature_dir_for_mission(...). So _lanes_feature_dir deliberately retains the COORD-aware dir while feature_dir falls back to primary for meta reads. This is a two-variable dance, not one — the extraction must preserve BOTH outputs or the seam silently changes which surface require_lanes_json reads (regression risk). Recommend the helper return the lanes-dir AND have the caller separately handle the feature_dir meta-fallback, OR return a small dataclass (lanes_dir, feature_dir).
Module placement: put _resolve_lanes_dir (or the pure resolver) in src/specify_cli/missions/_read_path_resolver.py next to candidate_/primary_feature_dir_for_mission — the blessed KITTY_SPECS path-constructor module (enforced by test_no_raw_mission_spec_paths.py). Do NOT inline a new constructor in implement.py.
Model to mirror: resolve_status_surface_with_anchor (coordination/surface_resolver.py:433) returns a surface object with .read_dir (used at implement.py:1018). The issue cites this as the analog — match its shape for _status_feature_dir.
Test strategy (the payoff): replaces 12 mocks with tmp_path: create a coord-worktree dir with lanes.json but no meta.json, and a primary dir with meta.json; assert the helper returns the coord surface for lanes and the primary for meta. Zero mocks (tactic: test-scaffolding-as-design-smell). Write this test RED first.
#1971 — collapse locate_project_root hops · S/M · low-med risk · cleanup
Verified call-graph (122 references; 3 defs):
paths.py:48(~44 LOC, authoritative: SPECIFY_REPO_ROOT + worktree-pointer + .kittify walk) — the bulk of callers import here.project_resolver.py:8— now delegates to paths (deferred import, line 23). 4 callers:cli/helpers.py:19,cli/commands/lint.py:25,__init__.py:53,compat/planner.py:779.__init__.py:52— wrapper →project_resolver. Caller:__init__.py:125.
The behavior split-brain the issue worries about is ALREADY CLOSED (lint/helpers now inherit env-var+worktree authority via the delegate). Residual is pure DRY: collapse the 2-hop indirection. Risk = the documented import-cycle hazard — project_resolver.py:14-21 explains the deferred import exists precisely to avoid core/__init__ → project_resolver → paths → package-init cycle. Do NOT convert to a module-level import (the docstring calls that out as a regression). Safe move: repoint the 4 project_resolver callers' import sites to from specify_cli.core.paths import locate_project_root (deferred where needed), keep project_resolver.locate_project_root as a thin re-export OR delete it after core/__init__ re-export is redirected.
Test strategy: existing 26+ caller tests are the safety net; add a focused test asserting lint/helpers honor SPECIFY_REPO_ROOT (the original split-brain symptom) — this both documents the fix and guards regression. Run tests/architectural/ for import-cycle/boundary gates.
#1878 — umbrella (reference only)
Coordination-branch placement/identity strangler; different seam family (coord placement vs naming SSOT). Only items #3 (worktree-naming allocator unification) and #5 (AC10 lint expansion to new placement/identity seams) touch the naming seam. Co-scope ONLY if the mission has headroom after the core lanes. The rest (resolver strangler, ref-advance helper, _ensure_branch_checked_out shim, #1827 crash window) is a separate mission — do not absorb.
WP-decomposition sketch (3.2.1 mission) — foundation → routing → enforcement
Mirrors the 3.2.0 mission shape (seam-first → migrate composers → tighten ratchet). Because #1888/#1915 are done, the mission is small (~4 WPs). Recommended:
WP01 Verify-and-close already-shipped issues (#1888, #1915) [foundation/hygiene]
- confirming tests: #1888 exact-typo scenario via validate_glob_matches;
#1915 already covered — assert + close.
- owned: tests/ownership/test_glob_matches_*.py (new), issue-matrix verdicts.
- deps: none. Pure verification; unblocks the spec's scope-truth.
WP02 #1993 — extract _resolve_lanes_dir pure seam [foundation seam]
- extract resolver into missions/_read_path_resolver.py; preserve the
TWO-variable (lanes-dir vs feature_dir) semantics; zero-mock test.
- owned: src/specify_cli/missions/_read_path_resolver.py,
src/specify_cli/cli/commands/implement.py (the 974-982 block only),
tests/missions/test_resolve_lanes_dir.py (new).
- deps: none.
WP03 #1971 — collapse locate_project_root hops [SSOT consolidation]
- repoint 4 project_resolver callers + __init__ wrapper at paths;
keep deferred-import cycle safety; SPECIFY_REPO_ROOT regression test.
- owned: src/specify_cli/core/project_resolver.py,
src/specify_cli/__init__.py (lines 52-55 only),
src/specify_cli/core/__init__.py (re-export),
src/specify_cli/cli/helpers.py, src/specify_cli/cli/commands/lint.py,
src/specify_cli/compat/planner.py, tests/core/test_locate_project_root_authority.py (new).
- deps: none.
WP04 #2000 + #1899-tail — route mission-dir composes + shrink ratchet [routing + enforcement]
- route mission_creation.py:321 + worktree.py:367/370 through
mission_dir_name(); add mission_worktree_path() emit-helper (optional);
surface_resolver R2 classifier dedupe; REMOVE the 3 allow-list entries;
byte-identical golden-parity tests.
- owned: src/specify_cli/core/mission_creation.py,
src/specify_cli/core/worktree.py,
src/specify_cli/lanes/branch_naming.py (only if adding mission_worktree_path),
src/specify_cli/coordination/surface_resolver.py (R2 short-circuit only),
tests/architectural/test_no_worktree_name_guess.py (allow-list shrink),
tests/lanes/test_branch_naming_parity.py (golden parity).
- deps: none on others; LAST because the ratchet shrink is the enforcement
capstone and should land after the routing is proven.
Dependency DAG
WP01 (close #1888/#1915) ─┐
WP02 (#1993 seam) ─┤ all independent foundations
WP03 (#1971 collapse) ─┘
WP04 (#2000+#1899) — independent, but sequence LAST
(enforcement capstone / ratchet shrink)
All four WPs are independent (no hard data deps). WP04 is recommended last for review clarity (ratchet tightening reads cleanest after routing lands), not because of a code dependency.
Ownership-overlap hazard map
| File | WPs touching | Hazard | Mitigation |
|---|---|---|---|
src/specify_cli/lanes/branch_naming.py |
WP04 only (add mission_worktree_path) |
LOW — append-only | Single owner WP04. If WP01 wants a golden-table helper, gate it behind WP04. |
src/specify_cli/core/worktree.py |
WP04 | none (sole owner) | — |
src/specify_cli/core/mission_creation.py |
WP04 | none (sole owner) | — |
src/specify_cli/__init__.py |
WP03 | LOW — only lines 52-55 | Scope owned_files to the wrapper; __init__.py change needs pyproject version bump + CHANGELOG (CLAUDE.md rule). |
src/specify_cli/core/__init__.py |
WP03 | re-export edit | sole owner WP03 |
src/specify_cli/cli/commands/implement.py |
WP02 (974-982) | this file is huge + hot; #1991/#1992 just touched it | Narrow owned-region to the extraction block; rebase risk if other missions touch implement.py concurrently. |
tests/architectural/test_no_worktree_name_guess.py |
WP04 | the ratchet itself — allow-list edit | sole owner WP04; the shrink IS the deliverable. |
Cross-mission rebase hazard: implement.py and branch_naming.py are the hottest files in recent missions (#1908, #1991, #1992, #2000). Land this mission promptly or expect rebases.
No-shadow-path / byte-identical risks (the 3.2.0 lesson)
- #2000/WP04 — canonical vs coord grammar.
mission_dir_nameSTRIPSNNN-;coord_mission_dir_namedoes NOT. The 3 creation sites alreadystrip_numeric_prefix→ route tomission_dir_name(canonical). Routing them to the coord variant would silently change on-disk names. Golden-parity test must cover legacy (057-foo) AND embedded (foo-01KV6510) inputs to prove byte-identity. - #1993/WP02 — two-surface seam. The inline code keeps
_lanes_feature_diron the COORD surface while reassigningfeature_dirto PRIMARY for meta. A naive extraction that returns one path collapses the surfaces →require_lanes_jsonreads the wrong dir (silent C-LANES-1 regression). The seam MUST preserve both outputs. - #1971/WP03 — deferred import is load-bearing.
project_resolver's deferred import prevents a package-init cycle (documented atproject_resolver.py:14-21). Reverting to module-level import is an explicit regression. Keep deferral where the cycle exists. - #1899/WP04 —
.worktrees/join vs name compose. The ratchet distinguishes a name-shape compose (flagged) from a legitdir / ".worktrees" / already_composed_namejoin (allowed).manifest.py:254and 30+ migration sites are legit joins — do NOT sweep them into the seam. Only the<slug>-<mid8>NAME composition routes. - #1888/#1915 — do not "fix" what's fixed. Re-implementing the already-shipped existence-check or atomicity would create a parallel impl (the exact anti-pattern). Verify, test, close.