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

IDDescriptionWPParallel
T001Add builtin_mission_type_ids() (@functools.cache, sorted) + frozenset convenience in doctrineWP01
T002Expose 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 firstWP01
T004[ATDD] loud-fail transitivity test (bad mission-type YAML → accessor raises)WP01
T005ruff/mypy/complexity + targeted test runWP01
T006[ATDD] import-time-I/O guard: importing charter modules triggers zero mission_types/ reads — REDWP02
T007Retire CANONICAL_MISSION_TYPES; remove from charter/__init__.__all__; migrate 3 test consumersWP02
T008Derive _BUILTIN_MISSION_TYPE_IDS lazily inside _read_activated_mission_types (preserve frozenset)WP02
T009Confirm frozenset-equality contract preserved (test_pack_context)WP02
T010ruff/mypy/complexity + targeted tests + arch_shard_1 poleWP02
T011[ATDD] behavioral test: a software-dev identifier answer still resolves after E is derived — REDWP03
T012Roster C: migration reads MissionTypeRepository.default().ids() at apply(); update migration testsWP03[P]
T013Roster D: `ALLOWED_MISSION_TYPES = frozenset(ids) \{any,generic}`; keep frozenset; refresh body_hashWP03
T014Roster E: derive _MISSION_IDENTIFIER_ANSWERS, preserve software_dev underscore aliasWP03
T015ruff/mypy/complexity + targeted tests + arch_shard_1 dead-symbol body_hashWP03
T016[ATDD] fail-loud tests: present-but-invalid raises; missing→fallback; empty-well-formed→empty — REDWP04
T017Add ActionIndexError(ValueError); raise on non-mapping root / non-list field / unparseable YAMLWP04
T018Re-pin the 2 lenient test_action_index.py tests to pytest.raises (re-pin, not delete)WP04
T019ruff/mypy/complexity + targeted testsWP04
T020[ATDD] CLI test: doctor doctrine --json shape + RC=1 on a synthetic collision — REDWP05
T021Wire scan into doctrine_check (catch CrossGrainDoubleDeclarationError → unhealthy + RC=1 + json)WP05
T022Re-add scan_builtin_cross_grain_duplicates to action_grain.__all__ (same change — C-003)WP05
T023Add a dedicated CI structural gate test (arch tier), independent of the broad runWP05
T024ruff/mypy/complexity + targeted tests + arch_shard_1 dead-symbol gate greenWP05
T025[ATDD] test: builtin_missions_root() returns src/doctrine/missions — REDWP06
T026Promote builtin_missions_root() (module-level fn; classmethod delegates)WP06
T027Drop # noqa: SLF001 at action_grain.py:202builtin_missions_root() (out-of-map, documented)WP06
T028Drop # noqa: SLF001 at mission_type_profiles.py:649builtin_missions_root() (out-of-map)WP06
T029ruff/mypy/complexity + full arch_shard_1 pole + terminology guardWP06

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_clear test 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_TYPES as 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_IDS lazily (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-dev identifier answer resolves after E derived — RED (WP03)
  • ✅ T012 Roster C: migration live-read at apply(); update migration tests (WP03)
  • ✅ T013 Roster D: ALLOWED_MISSION_TYPES derive + keep frozenset + refresh body_hash (WP03)
  • ✅ T014 Roster E: derive _MISSION_IDENTIFIER_ANSWERS, preserve software_dev alias (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_duplicates to action_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() 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 (out-of-map, documented) (WP06)
  • □ T028 Drop # noqa: SLF001 at mission_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.