Implementation Plan: Single Mission-Surface Resolver
Branch: feat/single-mission-surface-resolver | Date: 2026-06-19 | Spec: spec.md Input: Mission specification from kitty-specs/single-mission-surface-resolver-01KVGCE8/spec.md (source: GitHub #2040)
Summary
Strangle the coord-vs-primary mission-surface selection decision — today owned by 4+1 parallel resolvers — down to one canonical resolver (coordination/surface_resolver.resolve_status_surface_with_anchor), proven safe to collapse by a cross-resolver differential equivalence test (FR-002, the deletion gate) and locked by a load-bearing architectural guard (FR-004). Sequencing follows the spec's Tidy-First Inputs: unify the divergent primitives and extract a shared delegator FIRST (so the equivalence matrix asserts over a clean surface), land the two cheap behavioral slices (FR-008 silent-glob removal, FR-005 typed-error pass-through), then gate the collapse + shim retirement + #1900 allowlist drain on equivalence-green.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: stdlib pathlib; existing seams — coordination/surface_resolver.py (canonical owner), missions/_read_path_resolver.py, status/aggregate.py, mission_runtime/resolution.py, coordination/status_transition.py (#1900 predicates); reused 01KVFTFV scaffolding (tests/architectural/untrusted_path_audit/audit.py AST walker + test_untrusted_path_containment.py guard pattern, C-001) Storage: filesystem — kitty-specs/<slug>[-mid8]/ on the primary checkout vs .worktrees/<slug>-coord/ (the divergent surfaces); git worktree registry Testing: pytest — FR-002 differential equivalence test over the input-class matrix (no-coord / coord-fresh / coord-behind / coord-empty / ambiguous-mid8 / bare-slug-vs-<slug>-<mid8>-handle); mutation-killing tests per guard/fix; architectural guard in tests/architectural/ Target Platform: cross-platform CLI (Linux, macOS, Windows 10+) Project Type: single (CLI library: src/specify_cli + src/mission_runtime) Performance Goals: no regression — resolution is metadata/path composition (no I/O added beyond existing resolve()/registry reads); no benchmark gate Constraints: ruff + mypy --strict 0 on changed code (no new # noqa/# type: ignore); migrate, don't wrap — no new parallel resolver (C-002); FR-002 equivalence-green BEFORE deleting any duplicate (C-004); no version prescription (C-003) Scale/Scope: ~5 resolver sites + the mission_runtime caller boundary + 30+ feature_dir_resolver shim import sites (T6 bulk-edit slice)
Charter Check
GATE: passes before Phase 0; re-checked post-design.
- Tests for new functionality / ATDD: satisfied — FR-002 equivalence test + per-fix mutation tests + FR-004 guard.
- Code Quality (ruff/mypy --strict 0, complexity ≤ 15): satisfied — NFR-001.
- Canonical seams (#1868): directly advanced — bind surface-selection authority to one owner + a guard.
- Terminology Canon: Mission;
feature_dir/feature_slugreferenced as existing field/var names only. - Migrate-don't-wrap / no shadow path (#1993): C-002 binding — the collapse removes resolvers, never adds one.
- Bulk-edit note: only the T6 shim-retirement slice (migrate 30+
missions.feature_dir_resolverimport sites) is an import-path bulk edit. The mission is NOT globallychange_mode: bulk_edit(that would wrongly gate the non-bulk WPs); the T6 WP MUST produce a scopedoccurrence_map.yaml(import_paths category) via the occurrence-classification guardrail at its implement time — after the IC-02 audit fixes the exact site set. Flagged for /tasks. - No charter violations → no Complexity Tracking entries.
Project Structure
src/specify_cli/
├── coordination/
│ ├── surface_resolver.py # CANONICAL owner (resolve_status_surface_with_anchor) — IC-06
│ └── status_transition.py # #1900 coord predicates → migrate to canonical (IC-06)
├── missions/
│ ├── _read_path_resolver.py # unify primary_feature_dir (T1/FR-009), shared delegator (T4) — IC-01
│ └── feature_dir_resolver.py # C-004 shim → retire (T6) — IC-06
├── status/
│ └── aggregate.py # _find_meta_path glob (T2/FR-008) + _resolve_read_dir re-gate (T3) — IC-03/IC-06
├── mission_read_path.py # opportunistic shim retirement (T7)
└── (mission_runtime/resolution.py)# typed-error pass-through (FR-005) — IC-04
tests/
├── architectural/
│ ├── <surface-resolution audit> # repointed AST walker (FR-003) — IC-02
│ └── <single-resolver guard> # load-bearing guard (FR-004) — IC-07
└── <differential equivalence test># FR-002 deletion safety gate — IC-05
Structure Decision: single-project CLI. Work concentrates in coordination/ (canonical owner), missions/ (primitives + shim), status/aggregate.py, mission_runtime/resolution.py, and tests/architectural/. Refactor mission → WP ownership overlap on the resolver files is expected; the IC map linearizes the shared surfaces (per the refactor-overlap practice).
Complexity Tracking
No charter violations — none.
Implementation Concern Map
> Concerns are NOT work packages. /spec-kitty.tasks translates these into WPs. Sequencing encodes the tidy-first → equivalence-gate → collapse flow.
IC-01 — Tidy-BEFORE: unify the resolver primitives
- Purpose: Remove the hidden divergence so the equivalence test asserts over a clean surface. Disambiguate the two
primary_feature_dir_for_mission(T1/FR-009): keep the raw-slug topology-blind_read_path_resolver.py:410as canonical, make thefeature_dir_resolver.py:23mid8-composing twin re-export it (NOT a merge onto mid8). Confirm_compose_mission_diris the single grammar (T5) for the topology-aware/resolved path; extract ONE sharedresolve-dir-or-typed-errordelegator (T4) from the duplicated wrappers inaggregate._resolve_read_dirandmission_runtime/resolution.py. - Relevant requirements: FR-009, (enables FR-001/FR-002), C-002
- Affected surfaces:
missions/_read_path_resolver.py,missions/feature_dir_resolver.py(the divergent copy),mission_runtime/resolution.py,status/aggregate.py - Sequencing/depends-on: none — FIRST.
- Risks (squad-corrected 2026-06-19): the canonical primary anchor is raw-slug by design (topology-blind; 01KTRC04 FR-003;
_mid8_from_primary_metacalls it to derive mid8 — composing mid8 there is circular). Do NOT merge onto the mid8-composing form — that would reintroduce the split-brain. Pin the topology-blind contract with a regression on the existing raw-slug callers (_mid8_from_primary_meta, finalize-tasks). The T4 delegator's fallback-target + exception-set difference must be reconciled (a recorded decision).
IC-02 — Surface-resolution audit (read-only inventory)
- Purpose: Repoint the 01KVFTFV AST walker to enumerate every mission-surface-resolution callsite, classified
routed-through-resolver/topology-blind-by-design/raw-bypass. The map that scopes the guard (IC-07) and the shim migration (IC-06/T6). - Relevant requirements: FR-003
- Affected surfaces:
tests/architectural/(new audit module, reuseuntrusted_path_audit/audit.pymachinery); read-only oversrc/specify_cli+src/mission_runtime - Sequencing/depends-on: none — parallel with IC-01.
- Risks: distinguish
topology-blind-by-design(legitimate primary-only reads, e.g. meta.json) fromraw-bypass; do NOT flag theWorktreeTopology/classify_worktree_topologymachinery (correct authority, out of scope).
IC-03 — FR-008 / T2: single mid8 disambiguation
- Purpose: Eliminate
aggregate._find_meta_path's silent-first-matchglob("{slug}-*/meta.json"); route mid8 handles through the one canonical handle resolver so--mission <mid8>resolves identically everywhere (or raisesMISSION_AMBIGUOUS_SELECTOR). Closes the S8 selection ambiguity. - Relevant requirements: FR-008
- Affected surfaces:
status/aggregate.py(_find_meta_path), the canonical handle resolver - Sequencing/depends-on: after IC-01 (single composition grammar).
- Risks: behavior change (silent-pick → typed error) — carry the mutation-killing test; it is the intended FR-008 fix.
IC-04 — FR-005 / typed-error pass-through (cheapest behavioral slice)
- Purpose: Translate the un-caught
MISSION_AMBIGUOUS_SELECTORthrough themission_runtimeboundary (add theexcept MissionSelectorAmbiguousarm inresolution.pymirroring the existingStatusReadPathNotFoundtranslation at:185-190). No resolver change. - Relevant requirements: FR-005
- Affected surfaces:
mission_runtime/resolution.py(the:183try); the error types already exist - Sequencing/depends-on: depends on IC-01 (shared delegator); independent of the collapse.
- Risks (squad-corrected 2026-06-19): the original "flatten to
MISSION_NOT_FOUND" premise was FALSE —resolution.py:185-190already preservesStatusReadPathNotFoundandruntime_bridge.py:3163is already guarded. The residual is the un-caught ambiguous-selector. Use a LIVE ambiguous-handle repro (red onmain); reject a born-green test.
IC-05 — FR-002 / differential equivalence test (the deletion safety gate)
- Purpose: Feed the same
(slug, mid8, topology)matrix to every resolution entry point; assert identical dir OR identical typed error. MUST be green before any duplicate resolver is deleted (C-004). - Relevant requirements: FR-002, NFR-003
- Affected surfaces: new test; exercises all resolvers
- Sequencing/depends-on: after IC-01 + IC-02 + IC-03 (clean surface + inventory + mid8 fixed).
- Risks: must cover the FR-009 mid8-handle class or T1 hides a false-green; the matrix is the gate, not a smoke test.
IC-06 — FR-001/FR-007 / collapse to one resolver (gated on IC-05 green)
- Purpose: Make
resolve_status_surface_with_anchorthe sole authority.aggregate._resolve_read_dir→ thin adapter (drop the unmaterialized-coord re-gate, T3); migratestatus_transition.pycoord predicates to the canonical resolver and drain its C-002 topology-ratchet allowlist (#1900, the SC-005 proof); retire thefeature_dir_resolver.pyC-004 shim (T6, 30+ import sites → scopedoccurrence_map.yaml). - Relevant requirements: FR-001, FR-007, #1900
- Affected surfaces:
coordination/surface_resolver.py,status/aggregate.py,coordination/status_transition.py,missions/feature_dir_resolver.py(+ 30+ callers),tests/architectural/test_topology_resolution_boundary.py(allowlist) - Sequencing/depends-on: IC-05 (equivalence green) — hard gate (C-004); IC-01.
- Risks: the bulk import migration (T6) and the allowlist drain are the highest-overlap surfaces — linearize; do NOT delete a resolver until the equivalence test covers its input classes.
IC-07 — FR-004 / load-bearing architectural guard
- Purpose: Clone the 01KVFTFV guard — a
raw-bypassjoin outside the blessed resolver/delegator set fails CI; proven load-bearing (real-code mutation + non-empty coverage assertion anchored on the IC-02 inventory). - Relevant requirements: FR-004, SC-002
- Affected surfaces:
tests/architectural/<new guard> - Sequencing/depends-on: after IC-02 (inventory) + IC-06 (asserts the collapsed state).
- Risks: anchor strictly on the inventory (low false-positive); self-test must bite.
IC-08 — FR-006 / coord-empty hard-fail policy + ADR
- Purpose: A materialized-but-empty coord worktree hard-fails with
STATUS_READ_PATH_NOT_FOUNDwhose message names BOTH recovery paths (collapse/flatten OR recreate/populate the coord branch) — never a silent primary fallback. Record the decision in an ADR; bind it to the single resolver. (Distinct from the no-coord create→first-write window, where primary is authoritative.) - Relevant requirements: FR-006, NFR-004, #1716
- Affected surfaces:
coordination/surface_resolver.py(the policy),architecture/**/adr/(the ADR), tests - Sequencing/depends-on: with/after IC-06 (bound to the canonical resolver).
- Risks: must not regress the legitimate no-coord primary path; the message must be actionable (NFR-004).
Opportunistic (not load-bearing): T7 (mission_read_path.py shim retirement), T8 (CoordinationBranchDeleted error-ownership relocation when FR-005 lands). Fold only if cheap; skip T7 if it costs a bulk-edit.
Post-Planning Brownfield Checks
(recorded per standing practice; the adjacent-issues + boy-scout squads performed the bulk of this before plan)
- Foldable-issue search — DONE (adjacent-issues squad): exactly one genuine fold, #1900 (the
status_transition.py5th selection site), now in the issue-matrix + IC-06. Everything else in the coord/worktree neighborhood (#1357 races, #1887 merge-staging, #1829/#1890/#1891, #2017 umbrella) explicitly NOT folded (same-family-different-deliverable / orthogonal). No scope bloat. - Split-brain / duplication scan — DONE (boy-scout squad): the T1 divergent
primary_feature_dir_for_mission(the hidden 2nd instance of the mission's own bug) is the headline split-brain → FR-009/IC-01. Other duplications (T3 re-gate, T4 wrappers) mapped to ICs. - LOC / scope — bounded: ~5 resolver files +
mission_runtimeboundary + the T6 import migration; refactor-overlap linearized in the IC map. Not a codebase-wide rename. - Deprecation check — see Phase 0 research (the C-004
feature_dir_resolver.py+mission_read_path.pyshims ARE due deprecations this mission retires; confirm no other strangler shim in the surface is past its removal milestone). - Outcome: no scope expansion beyond the locked decisions + the single #1900 fold; the equivalence gate (C-004) is the safety mechanism for the collapse.