spec-kitty / spec-kitty/spec-kitty
runtime→doctrine boundary ratchet sees only module-level imports; 61 function-local imports across 30 files bypass it
- Dominant language
- Python
- Stars
- 1.6k
- Forks
- 165
- Avg merge
- 14h 52m
- Merged PRs (30d)
- 303
Description
## The runtime→doctrine boundary ratchet only sees module-level imports; 61 function-local imports have walked through it
Found while running mission `doctrine-silence-guards-01KYFV7Q` (WP08). Independently reproduced.
### The blind spot
ADR `2026-03-27-1` says runtime modules under `src/specify_cli/` reach doctrine only through the `charter.*` facade. `tests/architectural/test_runtime_charter_doctrine_boundary.py` enforces it — but inspects **module-level imports only**. Its own docstring names this as a deliberate scope choice.
A function-local `from doctrine.… import …` is therefore invisible to it:
```python
def _collect_org_layer_data(repo_root: Path) -> dict[str, object]:
from doctrine.drg.validator import validate_dangling_references # ratchet never sees this
```
That exact line shipped in this mission and passed the gate. It was caught by review, not by the ratchet built to catch it.
### Measured scope
```
function-local 'from doctrine.*' imports under src/specify_cli/: 61 across 30 files
```
(AST walk; excludes the exempt `specify_cli/doctrine/` subpackage. Counts `ImportFrom` nodes whose module starts with `doctrine` and which are not in the module body.)
So the boundary is enforced against one import style and not the other, and the unenforced style is the one people reach for when they hit a circular import — i.e. exactly the situation where the boundary matters most.
### Why this is worth a mission, not a patch
WP08's own change is net **−1** (its import moved to the facade). Widening the ratchet would light up 30 files at once, most far outside any current mission's surface. Each needs a real decision — route through the facade, or justify the exemption — and some may be genuine cycle-breaks that need the facade to grow first.
Suggested shape:
1. Widen the ratchet to walk all `ImportFrom` nodes, with a **frozen shrink-only baseline** of the 61 (growth fails, shrinkage warns — the pattern this repo already uses).
2. Drain the baseline in batches, promoting symbols onto the `charter.*` facades as needed.
3. Remove the baseline when it reaches zero.
Step 1 alone stops the bleeding and is small.
### Adjacent: the facade contract doc has no parity test either
`kitty-specs/charter-mediated-doctrine-selection-01KRTZCA/contracts/charter-facade-modules.md` carries per-facade symbol tables. Checked against `__all__` on 2026-07-27: the `charter/drg.py` row listed **9** symbols where the facade exported **22** — thirteen behind, drifted silently across missions because nothing enforces it. Reconciled and marked unenforced, but a parity test belongs with the ratchet work: a facade you cannot trust the documentation of is a weaker boundary than one with no documentation.
### Related
- #2979 — CI marker partition blind to the tier clause (same shape: a gate enforcing one half of a two-part condition)
- #2977 — hand-restated DRG writers
- #2981 — plural↔singular map restated in 4+ places
- #2980 — `kitty-specs/` guards disagreeing
Contributor guide
Research direction
Start with tests/architectural/test_runtime_charter_doctrine_boundary.py, especially its docstring and current AST traversal, then inspect the repository’s existing frozen shrink-only baseline pattern. Review kitty-specs/charter-mediated-doctrine-selection-01KRTZCA/contracts/charter-facade-modules.md alongside the charter facade modules, including charter/drg.py. Done means local doctrine imports are covered without baseline growth and facade symbol tables have an enforcing parity test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, testing-qa
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100