spec-kitty / spec-kitty/spec-kitty
Harden the FR-010 mission-type reader gate against indirection-evasion and add reader-module auto-discovery
- Dominant language
- Python
- Stars
- 1.6k
- Forks
- 165
- Avg merge
- 14h 52m
- Merged PRs (30d)
- 303
Description
## Context
Follow-up from the landing pass on #3649 (M5 canonical mission-type reader, epic #3410). The FR-010 reader gate (`tests/architectural/test_mission_type_reader_invariants.py`) is a parity registry + AST source-scan that pins every mission-type reader to the shared `read_mission_type(meta)` seam and fails the build on any new legacy `mission` read or `software-dev` default.
A fresh 4-lens adversarial squad on the rebased #3649 tip confirmed the gate **defends its real threat model well** — accidental reintroduction / straight revert (direct `.get`, subscript, and the `("mission_type","mission")` loop-tuple form are all caught, with a detector self-test). There is **no live exploit** in the tree today. These are hardening gaps worth their own scoped change, deferred from the landing pass rather than folded because closing them cleanly needs design (false-positive risk / a discovery mechanism), not a one-line fix.
## Gaps
1. **AST source-scan is evadable by deliberate indirection.** The scan matches literal `ast.Constant` field names, so these reintroduce legacy resolution without tripping it:
- `_L = "mission"; meta.get(_L)` (module-constant / variable indirection — documented as a known limit)
- `getattr(meta, "get")("mission")`
- `def resolve(mission=None, mission_type=None): return mission_type or mission` called `resolve(**meta)` — **no `"mission"` literal and no `.get` anywhere**; this one is *not* documented as a known limit and is a plausible (non-obfuscated) style
- default-side equivalents: `"software" + "-dev"`, `"-".join(("software","dev"))`
Each is only caught if the reader is hand-added to the Part-A parity registry (`_DICT_ADAPTERS` / `_FILE_ADAPTERS`), which currently enumerates 4 readers.
2. **`IN_SCOPE_READER_MODULES` is a hand-maintained allowlist, not auto-discovery.** A new runtime reader in a **new** module is invisible to both the source-scan and the parity registry until someone remembers to add it. (The landing pass added the two live readers that were already outside scope — `doc_state.py`, `mission_check_prerequisites.py` — so no live reader is currently uncovered, but nothing prevents the next one from slipping the net.)
## Suggested direction (to be designed)
- Either broaden the AST scan to resolve simple module-constant / kwargs-param indirection, or make the parity registry the enforced-complete backstop by **auto-discovering** candidate reader modules (e.g. any `src/` module that reads `mission_type` / `mission` from a `meta`-shaped dict) and asserting each is either registered, in-scope-scanned, or allow-listed — so "add a reader, forget the gate" fails closed.
- Weigh false-positive risk: the scan must not flag write-boundaries, DTO round-trips, or behavioral comparisons (e.g. `mission_check_prerequisites.py`'s `!= "software-dev"` policy branch).
Parent epic: #3410. Related: #2400 (single canonical source across metadata surfaces).
Contributor guide
Research direction
Start with tests/architectural/test_mission_type_reader_invariants.py, especially IN_SCOPE_READER_MODULES, _DICT_ADAPTERS, and _FILE_ADAPTERS, then review the reader modules named in the issue. Run the existing detector self-test and architectural tests before changing the gate. Done means indirection cases are covered, new reader modules cannot bypass enforcement, and documented non-reader cases avoid false positives.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- testing, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100