Tasks: Mission-Type Single-Source + Gate Wiring
Mission: mission-type-single-source-gate-wiring-01KXKHVZ Spec: spec.md · Plan: plan.md (IC-1..IC-4) Delivery order (correctness-bearing, C-002): IC-1 (#2669) → IC-2 (#2667) → IC-3 (#2666) → IC-4 (#2668)
Dependency graph
WP01 (accessor, doctrine) ──┬─► WP02 (Rosters A+B) ──┐
└─► WP03 (Rosters C+D+E) ─┤
WP04 (fail-loud) ─► WP05 (doctor+gate) ──────────────┴─► WP06 (promote + noqa drops, LAST)
# noqa: SLF001 bypasses as small documented out-of-map edits after the file owners are done.
- WP01 and WP04 have no dependencies → start in parallel.
- WP02 ∥ WP03 (both depend on WP01, touch disjoint files).
- WP05 depends on WP04 (gate is vacuous over a silently-degraded index until fail-loud lands).
- WP06 is terminal (depends on WP02, WP03, WP05) — it consumes the promoted accessor and drops the two
Subtask Index
| ID | Description | WP | Parallel |
|---|---|---|---|
| T001 | Add builtin_mission_type_ids() (@functools.cache, sorted) + frozenset convenience in doctrine | WP01 | |
| T002 | Expose a root-injection + cache_clear test seam (no shared-tree mutation) | WP01 | |
| T003 | [ATDD] SC-001 synthetic-type pickup test (monkeypatch root→tmp, cache_clear) — RED first | WP01 | |
| T004 | [ATDD] loud-fail transitivity test (bad mission-type YAML → accessor raises) | WP01 | |
| T005 | ruff/mypy/complexity + targeted test run | WP01 | |
| T006 | [ATDD] import-time-I/O guard: importing charter modules triggers zero mission_types/ reads — RED | WP02 | |
| T007 | Retire CANONICAL_MISSION_TYPES; remove from charter/__init__.__all__; migrate 3 test consumers | WP02 | |
| T008 | Derive _BUILTIN_MISSION_TYPE_IDS lazily inside _read_activated_mission_types (preserve frozenset) | WP02 | |
| T009 | Confirm frozenset-equality contract preserved (test_pack_context) | WP02 | |
| T010 | ruff/mypy/complexity + targeted tests + arch_shard_1 pole | WP02 | |
| T011 | [ATDD] behavioral test: a software-dev identifier answer still resolves after E is derived — RED | WP03 | |
| T012 | Roster C: migration reads MissionTypeRepository.default().ids() at apply(); update migration tests | WP03 | [P] |
| T013 | Roster D: `ALLOWED_MISSION_TYPES = frozenset(ids) \ | {any,generic}`; keep frozenset; refresh body_hash | WP03 |
| T014 | Roster E: derive _MISSION_IDENTIFIER_ANSWERS, preserve software_dev underscore alias | WP03 | |
| T015 | ruff/mypy/complexity + targeted tests + arch_shard_1 dead-symbol body_hash | WP03 | |
| T016 | [ATDD] fail-loud tests: present-but-invalid raises; missing→fallback; empty-well-formed→empty — RED | WP04 | |
| T017 | Add ActionIndexError(ValueError); raise on non-mapping root / non-list field / unparseable YAML | WP04 | |
| T018 | Re-pin the 2 lenient test_action_index.py tests to pytest.raises (re-pin, not delete) | WP04 | |
| T019 | ruff/mypy/complexity + targeted tests | WP04 | |
| T020 | [ATDD] CLI test: doctor doctrine --json shape + RC=1 on a synthetic collision — RED | WP05 | |
| T021 | Wire scan into doctrine_check (catch CrossGrainDoubleDeclarationError → unhealthy + RC=1 + json) | WP05 | |
| T022 | Re-add scan_builtin_cross_grain_duplicates to action_grain.__all__ (same change — C-003) | WP05 | |
| T023 | Add a dedicated CI structural gate test (arch tier), independent of the broad run | WP05 | |
| T024 | ruff/mypy/complexity + targeted tests + arch_shard_1 dead-symbol gate green | WP05 | |
| T025 | [ATDD] test: builtin_missions_root() returns src/doctrine/missions — RED | WP06 | |
| T026 | Promote builtin_missions_root() (module-level fn; classmethod delegates) | WP06 | |
| T027 | Drop # noqa: SLF001 at action_grain.py:202 → builtin_missions_root() (out-of-map, documented) | WP06 | |
| T028 | Drop # noqa: SLF001 at mission_type_profiles.py:649 → builtin_missions_root() (out-of-map) | WP06 | |
| T029 | ruff/mypy/complexity + full arch_shard_1 pole + terminology guard | WP06 |
WP01 — Canonical mission-type accessor (doctrine enabler)
MissionTypeRepository, with a test seam that lets the SC-001 guard inject a synthetic type without mutating the shared doctrine tree. Foundation for all roster derivations.
- Goal: One cached doctrine-layer accessor deriving the built-in mission-type ids from
- Priority: P0 (enabler). Dependencies: none. Independent test:
uv run pytest tests/doctrine/missions -k builtin_mission_type_ids -q. - Requirements: FR-001, NFR-002, C-010.
- Estimated prompt size: ~260 lines.
- ✅ T001 Add
builtin_mission_type_ids()(@functools.cache, sorted) + frozenset convenience (WP01) - ✅ T002 Expose a root-injection +
cache_cleartest seam (WP01) - ✅ T003 [ATDD] SC-001 synthetic-type pickup test — RED first (WP01)
- ✅ T004 [ATDD] loud-fail transitivity test (WP01)
- ✅ T005 ruff/mypy/complexity + targeted test run (WP01)
WP02 — Charter Rosters A + B (retire the constant; lazy frozenset)
lazily, with zero import-time filesystem I/O in charter.
- Goal: Retire
CANONICAL_MISSION_TYPESas a public constant and derive_BUILTIN_MISSION_TYPE_IDS - Priority: P1. Dependencies: WP01. Independent test:
uv run pytest tests/charter -q. - Requirements: FR-002, FR-003, NFR-001, C-011.
- Estimated prompt size: ~300 lines.
- ✅ T006 [ATDD] import-time-I/O guard — RED (WP02)
- ✅ T007 Retire
CANONICAL_MISSION_TYPES+ remove from__all__+ migrate 3 test consumers (WP02) - ✅ T008 Derive
_BUILTIN_MISSION_TYPE_IDSlazily (frozenset) (WP02) - ✅ T009 Confirm frozenset-equality contract preserved (WP02)
- ✅ T010 ruff/mypy/complexity + targeted tests + arch_shard_1 pole (WP02)
WP03 — Charter Rosters C + D + E (migration live-read; sentinels; alias)
the dead-symbol body_hash refreshed), and _MISSION_IDENTIFIER_ANSWERS (underscore alias preserved).
- Goal: Derive the migration roster (live-read),
ALLOWED_MISSION_TYPES(frozenset + sentinels, with - Priority: P1. Dependencies: WP01. Independent test:
uv run pytest tests/charter tests/upgrade -q. - Requirements: FR-004, FR-005, FR-006, C-004, C-011.
- Estimated prompt size: ~320 lines.
- ✅ T011 [ATDD] behavioral test:
software-devidentifier answer resolves after E derived — RED (WP03) - ✅ T012 Roster C: migration live-read at
apply(); update migration tests (WP03) - ✅ T013 Roster D:
ALLOWED_MISSION_TYPESderive + keep frozenset + refresh body_hash (WP03) - ✅ T014 Roster E: derive
_MISSION_IDENTIFIER_ANSWERS, preservesoftware_devalias (WP03) - ✅ T015 ruff/mypy/complexity + targeted tests + arch_shard_1 dead-symbol body_hash (WP03)
WP04 — load_action_index fails loud (IC-2)
stays a silent fallback; an intentionally-empty well-formed index stays empty.
- Goal: A present-but-malformed action index raises a structured
ActionIndexError; a missing file - Priority: P1 (must precede WP05). Dependencies: none. Independent test:
uv run pytest tests/doctrine/missions/test_action_index.py -q. - Requirements: FR-007, FR-008.
- Estimated prompt size: ~240 lines.
- ✅ T016 [ATDD] fail-loud tests (present-invalid raises; missing→fallback; empty→empty) — RED (WP04)
- ✅ T017 Add
ActionIndexError; raise on the 3 present-but-invalid cases (WP04) - ✅ T018 Re-pin the 2 lenient tests to
pytest.raises(WP04) - ✅ T019 ruff/mypy/complexity + targeted tests (WP04)
WP05 — Wire the FR-013 scan into doctor doctrine + CI gate (IC-3)
and a dedicated CI structural gate; the symbol is re-added to __all__ alongside its new src caller.
- Goal: The cross-grain scan runs from
spec-kitty doctor doctrine(RC=1 + json finding on collision) - Priority: P1. Dependencies: WP04. Independent test:
uv run pytest tests/specify_cli/cli/commands -k doctrine -q && uv run pytest tests/doctrine/drg/test_cross_grain_integrity.py -q. - Requirements: FR-009, FR-010, FR-011, C-003.
- Estimated prompt size: ~300 lines.
- ✅ T020 [ATDD] CLI test:
doctor doctrine --json+ RC=1 on synthetic collision — RED (WP05) - ✅ T021 Wire scan into
doctrine_check(unhealthy + RC=1 + json finding) (WP05) - ✅ T022 Re-add
scan_builtin_cross_grain_duplicatestoaction_grain.__all__(C-003) (WP05) - ✅ T023 Add a dedicated CI structural gate test (WP05)
- ✅ T024 ruff/mypy/complexity + targeted tests + arch_shard_1 dead-symbol gate green (WP05)
WP06 — Promote builtin_missions_root() + drop SLF001 (IC-4, terminal campsite)
bypasses. Pure refactor; single-class scope; lands last.
- Goal: Promote the private built-in-root accessor to a public seam and remove both
# noqa: SLF001 - Priority: P2 (campsite). Dependencies: WP02, WP03, WP05. Independent test:
uv run pytest tests/charter -k "builtin_missions_root or action_grain" -q. - Requirements: FR-012, C-005.
- Estimated prompt size: ~220 lines.
- □ T025 [ATDD] test:
builtin_missions_root()returnssrc/doctrine/missions— RED (WP06) - □ T026 Promote
builtin_missions_root()(module-level fn; classmethod delegates) (WP06) - □ T027 Drop
# noqa: SLF001ataction_grain.py:202(out-of-map, documented) (WP06) - □ T028 Drop
# noqa: SLF001atmission_type_profiles.py:649(out-of-map, documented) (WP06) - □ T029 ruff/mypy/complexity + full arch_shard_1 pole + terminology guard (WP06)
MVP scope
WP01 is the enabler; the smallest coherent shippable slice is WP01 → WP02 → WP03 (#2669 fully closed). WP04 → WP05 (#2667 + #2666) and WP06 (#2668) complete the bundle.