Implementation Plan: ScopeSource gate follow-up — cleanup & correctness
Branch: fix/scopesource-gate-followup | Date: 2026-07-23 | Spec: spec.md Input: Feature specification from kitty-specs/scopesource-gate-followup-01KY6S9P/spec.md
Summary
Close the four #2873 follow-ups to the doctrine-controlled transition gates (half A, #2871), in three work packages sequenced A→C (hard) with B parallel:
- WP-A cleanup — retire the production-dead census-derivation tier + the dead
_mt_pre_review_gate_verdict; hoist the sharedScopeSourcefactory. - WP-B contract — decouple the
ScopeBreakdownSourceport (two independent predicates + afile_to_scopeprojection mixin). - WP-C correctness — close the baseline↔head command-authority split (incl. the parse-after-teardown asymmetry, B1); add a warn-shaped
GateOutcome.SOURCE_MISMATCH; add config-drivenScopeSourceselection (FR-014) so SC-001 is real for non-pytest consumer repos.
Design is fully grounded in the merged base eb06ca176 (PRs #2874 coord-commit-integrity + #2820 dossier-parity landed). Six locked decisions in research.md; entities/fields/outcomes in data-model.md; surface contracts in contracts/. Hardened by three adversarial squads (post-spec, fold/boyscout, post-plan).
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: existing spec-kitty surfaces only — specify_cli.review (pre_review_gate, scope_source, baseline, gate_registry, verdict_aggregation), specify_cli.cli.commands.agent (tasks_move_task, workflow_executor, workflow), charter/mission_runtime for the _resolve_workflow_read_dir(kind=WORK_PACKAGE_TASK) seam (#2874 C-008). No new third-party dependency. Storage: tasks/<wp>/baseline-tests.json (BaselineTestResult, gains a source_identity field); no DB. Testing: pytest (PYTHONPATH=$(pwd)/src); ATDD red-first (C-006); behavior-preservation goldens captured from the pre-mission commit against BOTH the registry hook (NFR-001) and the override tier (NFR-006), non-circular. Target Platform: Linux/macOS dev + CI (the spec-kitty CLI runtime). Project Type: single project (CLI/library). Performance Goals: N/A (correctness + cleanup mission; no perf target). Constraints: complexity ≤15/function; mypy --strict + ruff zero-issue/zero-warning, no new suppressions; ≥90% new-code coverage; behavior-preserving deletions (byte-identical live-path verdicts); half B (#2599) OUT. Scale/Scope: ~450 LoC of dead duplicate removed; 14 FR / 7 NFR across 3 WPs / 15 ICs (IC-00..IC-14); single subsystem (review/ + the gate hook).
Charter Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design. (charter context: compact)
- Single canonical authority — WP-A collapses the duplicated census logic to the single live
scope_source.pyauthority; WP-C unifies baseline+head onto oneScopeSourcecommand authority. ✅ advances the principle. - DDD + tiered rigour / ATDD-first — every IC is red-first (C-006) with a named failing artifact; the correctness ICs carry non-circular goldens. ✅
- Architectural gate discipline — the mission IS the pre-review gate; NFR-004 keeps the dead-symbol / compat-surface / census-parity / ratchet gates green (or retired with their target); #2825 baseline-red-gotcha check before attributing. ✅
- Canonical sources, no improvisation — FR-009 consumes #2874's existing
_resolve_workflow_read_dirseam rather than reconstructingfeature_dir. ✅ - Terminology canon — Mission not Feature; no forbidden terms (verified on spec + records). ✅
- No violations → Complexity Tracking below is empty.
Project Structure
Documentation (this mission)
kitty-specs/scopesource-gate-followup-01KY6S9P/
├── plan.md # This file
├── research.md # 6 locked decisions (D-1..D-6)
├── data-model.md # source_identity field, SOURCE_MISMATCH, factory/seam homes, predicates+mixin
├── contracts/ # scope-source-contract.md, gate-outcome-contract.md, baseline-identity-contract.md
├── reviews/ # post-spec-squad.md, fold-boyscout-squad.md
└── tasks.md # (created later by /spec-kitty.tasks — NOT here)
Source Code (repository root)
src/specify_cli/review/
├── pre_review_gate.py # WP-A: delete census tier; WP-B: two predicates (:881,:1013); WP-C: SOURCE_MISMATCH
├── scope_source.py # WP-A: factory home (resolve_scope_source); WP-B: ScopeBreakdownMixin mixin + predicates
├── baseline.py # WP-C: artifact-before-teardown, source_identity field, from_dict default; WP-A(tail): unused-import + ruff fold
├── gate_registry.py # WP-C: TransitionGateContext (unchanged shape)
└── verdict_aggregation.py # WP-C: SOURCE_MISMATCH is WARN_PROCEED by allowlist (assert, no filter edit)
src/specify_cli/cli/commands/agent/
├── tasks_move_task.py # WP-A: delete _mt_pre_review_gate_verdict, hoist _mt_resolve_scope_source, keep seams; WP-C: console-ladder branch (_mt_pre_review_gate_console_warning), consume #2874 kind-aware read seam
├── tasks.py # WP-A: compat re-export edit (golden 157→156)
└── workflow_executor.py # WP-C: implement_capture_baseline injects the shared factory
tests/review/ + tests/specify_cli/cli/commands/agent/ # migrate 8 verdict-diff tests; parity/golden/guard tests
ruff.toml # WP-A(tail): tighten baseline.py legacy-debt entry
Structure Decision: single-project edit confined to the review/ subsystem + its gate hook; no new packages. The one config touch (ruff.toml) is a one-line narrowing coupled to baseline.py.
Complexity Tracking
No Charter Check violations — section intentionally empty.
Implementation Concern Map
> Concerns are not work packages. /spec-kitty.tasks maps these to WPs. WP grouping (A cleanup / B contract / C correctness) and the A→C-hard, B-parallel sequencing are shown for context; the single hard cross-WP edge is IC-04 (factory hoist) → IC-09 (WP-C).
IC-00 — Behavior-preservation golden capture (canonical harness)
- Purpose: Adopt the EXISTING canonical parity harness (
tests/review/fixtures/parity/_capture.py+tests/review/test_transition_gate_parity.py) to capture the NFR-001 (registry hook) AND NFR-006 (override tier, non-empty scope) verdict/metadata tuples against the pinned baseeb06ca176— assertingHEAD==baseat capture, machine-emittingbase_commitprovenance — and commit them BEFORE any deletion. Prevents the circular-oracle trap; no improvised snapshot, no hand-typed SHA. - Relevant requirements: NFR-001, NFR-006, NFR-007
- Affected surfaces:
tests/review/fixtures/parity/_capture.py(extend),tests/review/test_transition_gate_parity.py(replay carrier), committed fixtures - Sequencing/depends-on: none — WP-A entry, strictly before IC-02
- Risks: capturing after the deletion (circular oracle) — the harness's
HEAD==baseassertion forbids it; the override golden must drive a non-empty scope or NFR-006 passes vacuously (run_scoped_tests_at_headnever runs).
IC-01 — Pre-deletion sole-caller characterization (precondition)
- Purpose: Characterize (green-on-base) that the census branch is the only live caller reaching
evaluate_pre_review_gatewithscope_source=None— the load-bearing precondition that makes "no coverage lost" falsifiable. - Relevant requirements: FR-002 (audit half)
- Affected surfaces:
pre_review_gate.py(evaluate_pre_review_gate), a characterization test - Sequencing/depends-on: IC-00 (WP-A entry)
- Risks: this is a characterization precondition, not an ATDD red-first artifact (it asserts a property already true on base); if a non-census
scope_source=Nonecaller exists, the whole WP-A premise changes — the audit must be exhaustive.
IC-02 — Delete dead census tier + verdict helper + atomic compat edit
- Purpose: Remove
derive_test_scope+ census helpers/constants frompre_review_gate.py(livescope_source.pycopy untouched) and the dead_mt_pre_review_gate_verdict, dropping thefilter_groups/composite_routing/scope_source=Nonesignature — in one atomic commit with the compat golden 157→156. - Relevant requirements: FR-001, FR-002 (deletion half), C-004; guards NFR-001, NFR-006, NFR-004
- Affected surfaces:
pre_review_gate.py,tasks_move_task.py(_mt_pre_review_gate_verdict),tasks.py:442re-export,test_tasks_compat_surface.py(tuple + golden 157→156) - Sequencing/depends-on: IC-01
- Risks: an accidental import break if the re-export/tuple/golden aren't edited atomically (C-004); KEEP-live set (
_CompositeRoute,evaluate_with_scope,run_scoped_tests_at_head,ScopeResult,_mt_pre_review_gate_with_override_scope,_mt_empty_scope_verdict) must survive — guarded by the NFR-006 override-tier golden.
IC-03 — Migrate/retire affected tests + coverage-parity inventory
- Purpose: Retire census-only tests, repoint the 2
derive_test_scope-oracle tests, and migrate the 8 verdict-diff tests (test_pre_review_gate_engine.py:827,843,868,897,918,936,961,996) from dropped-param toscope_source=injection — with an inventory mapping each retirement to a surviving equivalent. - Relevant requirements: FR-004; guards NFR-003, NFR-004
- Affected surfaces:
test_census_parity.py,test_pre_review_scope_singlesource.py,test_pre_review_gate_engine.py,test_scope_source.py - Sequencing/depends-on: IC-02 (the param drop turns the 8 tests red → migrate to green)
- Risks: losing the unique verdict-diff coverage (NO_COVERAGE warn, terminal-interruption, sentinel/None-baseline degradation, NEW_FAILURES) if the 8 are retired instead of migrated — they MUST be migrated.
IC-04 — Hoist the shared ScopeSource factory + seams
- Purpose: Extract
_mt_resolve_scope_sourcetoscope_source.py(already imported by bothbaseline.pyandpre_review_gate.py→ no new edge) importable by the head hook and the implement-time baseline path; keep the monkeypatched override seams intasks_move_task.pyand pass them as parameters (nobaseline.py→tasks_move_task.pycycle). The single artifact WP-C depends on. - Relevant requirements: FR-003; defines NFR-005
- Affected surfaces:
scope_source.py(newresolve_scope_source),tasks_move_task.py(seams stay, factory call-site updated),workflow_executor.py(baseline call-site) - Sequencing/depends-on: IC-02, IC-03 (natural WP-A order)
- Risks: an import cycle if the factory is placed wrong — the chosen
scope_source.pyhome + parameterized seams avoid it; NFR-005 equivalence test pins identicaltest_command()+ parse-mode at both call sites.
IC-05 — Docs/comment hygiene + baseline.py boyscout fold
- Purpose: Scrub stale docstrings/CHANGELOG/plan-doc references to deleted symbols + dropped params; delete
baseline.py:25's unusedtimezoneimport and tighten itsruff.tomlentry["ARG001","F401","S314","S602"]→["ARG001","S314"]. - Relevant requirements: FR-013; guarded by NFR-002 + docs-freshness gate
- Affected surfaces: docstrings across the deleted surface,
CHANGELOG.md/plan docs,baseline.py:25,ruff.toml - Sequencing/depends-on: docs half after IC-02/IC-03; the
baseline.pyhalf lands as the trailing commit after WP-C rewritesbaseline.py(cosmetic soft-edge onto IC-09, not a functional dependency) - Risks: doing the
baseline.pyruff-entry tighten before WP-C rewrites the file → premature; sequence it last.
IC-06 — Split the welded isinstance into two independent predicates
- Purpose: Replace
isinstance(scope_source, ScopeBreakdownSource)withexposes_scope_breakdown(capability, backed by theisinstance) andempty_scope_is_coverage_gap(policy, backed by a distincttreats_empty_scope_as_coverage_gapClassVar marker) — different backing signals so the weld is provably gone, not renamed. - Relevant requirements: FR-005
- Affected surfaces:
scope_source.py(predicates + marker),pre_review_gate.py:881,:1013(call sites) - Sequencing/depends-on: none (WP-B, parallel to A/C)
- Risks: making both predicates read the same signal (the carla-2 trap) — the ClassVar marker is what keeps them independent; a synthetic-source test must prove one-without-the-other.
IC-07 — file_to_scope default projection (ABC/mixin)
- Purpose: Provide a
ScopeBreakdownMixinABC/mixin (NOT a Protocol default — won't reach structural implementers) givingfile_to_scopeas a projection overscope_breakdown, soGateCoverageScopeSourceimplements onlyscope_breakdown. - Relevant requirements: FR-006
- Affected surfaces:
scope_source.py(mixin +GateCoverageScopeSourceinheritance) - Sequencing/depends-on: none (independent of IC-06)
- Risks:
DeclaredCommandScopeSourcemust remain a structural implementer with NOscope_breakdown(its absence is the "empty ≠ gap" signal) — the mixin must not force it to inherit.
IC-08 — Migrate the intent-encoding test
- Purpose: Repoint the test pinning "membership ⇒ empty-is-gap" off the raw
isinstanceonto the two independent predicates, including a synthetic source satisfying one but not the other (US3 AS3). - Relevant requirements: FR-007
- Affected surfaces:
test_scope_source.py:121-128 - Sequencing/depends-on: IC-06
- Risks: the migrated test must fail against the old
isinstance-only path and pass only post-IC-06 — proves decoupling.
IC-09 — Artifact-parse-before-teardown + shared-factory injection (the B1 fix)
- Purpose: Inject the shared factory into
implement_capture_baseline(activating_capture_baseline_via_scope_source) so baseline+head share one authority, AND read/relocate the baseline artifact to a stable out-of-worktree path before worktree teardown — closing the parse-after-teardown asymmetry that reintroduces the bug forDeclaredCommandScopeSourcewith a worktree-relative--junitxml. - Relevant requirements: FR-008; enforces C-001/C-005
- Affected surfaces:
baseline.py(_capture_baseline_via_scope_source,capture_baseline),workflow_executor.py(implement_capture_baseline) - Sequencing/depends-on: IC-04 (hard — the only cross-WP edge)
- Red-first carrier (post-plan squad): a direct
capture_baseline/_capture_baseline_via_scope_sourceunit test withDeclaredCommandScopeSource+ a worktree-relative--junitxml, asserted red on base before the relocate fix (the workflow-routed path is dormant on base). Split: factory-injection lands with the red observed, then relocate greens it. The relative--junitxmlresolves againstcwd=tmp_worktree, not process cwd. - Risks: the B1 bug itself — a worktree-relative artifact deleted on teardown → text/synthetic identities → disjoint namespace.
IC-10 — Record + assert source/parse-mode identity (kind-aware seam)
- Purpose: Add a
source_identity(source class + parse-mode/artifact-presence — enough to catch B1's same-class-different-parse-mode) toBaselineTestResult; compute it via one helper used at both capture and diff; compare only in_evaluate_via_scope_source(never the override tier); read the baseline through #2874's_resolve_workflow_read_dir(kind=WORK_PACKAGE_TASK)seam;from_dictdefaults a missing field toUNVERIFIED_BASELINE, not a spuriousSOURCE_MISMATCH. - Relevant requirements: FR-009
- Affected surfaces:
baseline.py(BaselineTestResult,from_dict,diff_baseline),pre_review_gate.py(_evaluate_via_scope_source),tasks_move_task.py(identity read via the kind-aware seam) - Sequencing/depends-on: IC-09
- Risks: firing the compare on the shared override tier (which has no injected source) → spurious mismatch; the "unknown → UNVERIFIED_BASELINE" default protects straddling-upgrade + sentinel artifacts.
IC-11 — GateOutcome.SOURCE_MISMATCH + console-ladder exhaustiveness
- Purpose: Add a dedicated warn-shaped, fail-open
SOURCE_MISMATCH(notNO_COVERAGE, not a block); add an explicit branch to_mt_pre_review_gate_console_warningand convert its trailingreturn "…no new failures"into an explicitNO_NEW_FAILURESbranch + defensiveelserenderingoutcome.value— closing the latent silent-clean-pass class for any future member. - Relevant requirements: FR-011
- Affected surfaces:
pre_review_gate.py(GateOutcome),tasks_move_task.py:1156(console ladder),verdict_aggregation.py(assert-only: SOURCE_MISMATCH absent from_TERMINAL_OUTCOMES+ the NEW_FAILURES block → WARN_PROCEED by construction) - Sequencing/depends-on: IC-10
- Risks: editing the allowlist filters instead of asserting them (they're member-explicit → fail-open by construction); missing the console fall-through (the exhaustiveness gap alphonso/#2874-lens flagged).
IC-12 — Dual-impl/dual-parse-mode baseline↔head parity test
- Purpose: Prove baseline+head land in one failure-identity namespace under
GateCoverageScopeSourceANDDeclaredCommandScopeSource, for both a worktree-relative-JUnit case and a FAIL-text case (the B1 cases) — the SC-001/US1 proof; a deliberately mismatched pair raisesSOURCE_MISMATCH. - Relevant requirements: FR-010
- Affected surfaces:
tests/review/(new parity test, 2 sources × 2 parse modes) - Sequencing/depends-on: IC-09, IC-11
- Risks: a happy-path absolute-artifact case passing while the worktree-relative case (the real bug) is untested — all four combinations required.
IC-13 — Anti-narrowing guard
- Purpose: Assert the baseline runs the WHOLE command, without head's per-file
scope.test_targetsappended, so a future refactor can't silently narrow baseline and break the broad-baseline/narrow-head invariant (C-005). - Relevant requirements: FR-012
- Affected surfaces:
tests/review/(guard test),baseline.py(the invariant it guards) - Sequencing/depends-on: IC-09
- Risks: low — a targeted assertion; its value is preventing a future regression.
IC-14 — Config-driven ScopeSource selection (delivers SC-001)
- Purpose: Make
resolve_scope_sourceSELECT the source from config —DeclaredCommandScopeSourcewhenreview.test_commandis present (non-pytest), elseGateCoverageScopeSource— so a real non-pytest consumer repo runs baseline + head through one authority and SC-001 is achievable against an actual repository (not only IC-12's synthetic injection). Keeps_capture_baseline_via_config's behavior alive via the portable source; fixes the stalescope_source.py:51-55"lands with #2873" comment +docs/development/review-gates.md:174. - Relevant requirements: FR-014; makes SC-001 real; extends the FR-003/FR-008/NFR-005/FR-010 matrix to the selected
DeclaredCommandScopeSourcepath - Affected surfaces:
scope_source.py(resolve_scope_sourceselection branch),docs/development/review-gates.md, the stale comment - Sequencing/depends-on: IC-04 (the factory it makes selective); feeds IC-09/IC-10/IC-12 (the selected portable path is what makes the correctness proof real, not synthetic)
- Risks: the selection policy (presence of
review.test_command⇒ portable) must not accidentally route spec-kitty itself (which has noreview.test_command) away fromGateCoverageScopeSource; a wrong policy silently changes every consumer's gate. Test both branches.
WP grouping, sequencing & coverage (context for /spec-kitty.tasks)
- WP-A cleanup: IC-01 → IC-02 → IC-03 → IC-04 (⇢ IC-05 docs; IC-05 baseline-tail lands last). WP-B contract: IC-06 → IC-08, IC-07 (parallel, no cross-WP edges). WP-C correctness: IC-09 → IC-10 → IC-11 → IC-12, IC-13 (IC-09 depends only on IC-04).
- Hard cross-WP edges: IC-04 → IC-09 (factory) and IC-04 → IC-14 (selection); IC-14 feeds IC-09/IC-10/IC-12 (the selected portable path makes the correctness proof real). Everything else within-WP. Note (carla m2): IC-02 (~450 LoC delete) and IC-06/IC-07 (edits at
pre_review_gate.py:881,:1013) touch the same file — tasks must not schedule them as truly-simultaneous same-hunk edits. - FR coverage (no orphans): FR-001→IC-02, FR-002→IC-01+IC-02, FR-003→IC-04, FR-004→IC-03, FR-005→IC-06, FR-006→IC-07, FR-007→IC-08, FR-008→IC-09, FR-009→IC-10, FR-010→IC-12, FR-011→IC-11, FR-012→IC-13, FR-013→IC-05, FR-014→IC-14. NFR-001→IC-00(capture)+IC-02(replay), NFR-002→cross-cutting (carrier IC-05), NFR-003→cross-cutting (each IC's test), NFR-004→IC-02+IC-03, NFR-005→IC-04(defines)+IC-10/IC-14(consumes), NFR-006→IC-00+IC-02, NFR-007→IC-00. Constraints: C-001→IC-09/IC-14, C-002→IC-02(keep-live golden), C-003→this WP graph, C-004→IC-02, C-005→IC-09/IC-13, C-006→every IC's red-first test.
- MVP / first-landable slice: WP-A (IC-01→IC-04) + WP-C (IC-09→IC-13) — closes the P1 correctness bug (US1/SC-001/SC-004) and retires the dead duplicate WP-C's factory depends on. WP-B (IC-06/07/08) + IC-05 are zero-behavior-change hygiene, a fully-parallel fast-follow.
- #2825 baseline-red-gotcha: before attributing any
test_no_dead_symbols/test_golden_count_banfailure (IC-02/03/11 touch that gate family) to this diff, confirm it's pre-existing red oneb06ca176viaPYTHONPATH=<worktree>/srcagainstupstream/main— do not green-wash a category-1 pre-existing red.