spec-kitty / spec-kitty/spec-kitty

runtime→doctrine boundary ratchet sees only module-level imports; 61 function-local imports across 30 files bypass it

Open
#2,986 3 comments 0 reactions 0 assignees View on GitHub
doctrine priority:P1
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.