Contracts
architectural-gates.md
Contract: test_no_dead_doctrine_paths.py Split + Fixture Decoupling (FR-001, FR-002)
Not an HTTP/API contract — this is the pass/fail contract the split gate modules and the redriven NFR-002 proof must satisfy, so /spec-kitty.tasks and implementation have an unambiguous Definition of Done.
Pre-state (verified against the current file, 841 lines)
| Gate | Function | Scope | Discriminator-proof tests |
|---|---|---|---|
| A | scan_graph_monolith_paths/_shipped | src/ (all of it) | test_project_tier_graph_path_would_false_red_without_its_discriminator |
| B | scan_shipped_pack_paths/_shipped | src/ (all of it — not doctrine-scoped) | test_shipped_prose_would_false_red_without_the_path_shape_discriminator, test_frozen_seed_mirror_would_false_red_without_its_discriminator |
| C | scan_doctrine_cross_links/_shipped | src/doctrine/ (the only doctrine-scoped gate) | test_code_example_links_would_false_red_without_their_discriminator, test_placeholder_links_would_false_red_without_their_discriminator |
| D | test_no_live_doc_names_a_pre_move_builtin_path | docs/ | (self-contained) |
Currently, Gate A's discriminator-proof (test_forbidding_mention_would_false_red_without_its_discriminator, :517) is pinned to the live artifact packs/built-in/agent_profiles/doctrine-daphne.agent.yaml's src/doctrine/graph.yaml mention (line 136) — the exact contradiction #3036 tracks. (This is the current path — the profile was relocated from src/doctrine/agent_profiles/built-in/ by an earlier, already-merged mission.)
> Correction (verified 2026-08-04, pre-implementation). An earlier draft of this contract attributed this test to Gate C. That is factually wrong: forbidding_mentions is a field of GraphMonolithScan (:124), populated by scan_graph_monolith_paths (:158), and the test calls scan_graph_monolith_shipped() (:519) — it is a Gate A proof. Consequence for FR-002/WP02: the fixture decoupling lands in the CLI-wide (A+B) module WP01 creates, not the narrowed doctrine-scoped Gate C module. Gate C's own analogous case (the on-disk cross-link resolution requirement, US2-AS3) is a separate second deliverable of FR-002 and does live in the Gate C module. test_gate_a_discriminators_do_not_swallow_a_planted_violation (:546) already plants a tmp_path fixture that exercises forbidding_mentions — that is the established local idiom to build the decoupled proof on.
All three gate functions (A, B, C) share a common set of helpers today: the Site dataclass, _rel, _read_lines, _text_files (an lru_cache-backed reader), and the _REPO_ROOT/_SRC_ROOT/_DOCTRINE_ROOT/_PACKS_ROOT/_TEXT_SUFFIXES constants.
Post-state contract (FR-001)
1. Three (or more, if the implementer finds a reason) modules exist, each scoped to exactly the gates named above, grouped as: {A, B} together (both src/-wide), {C} alone (doctrine-scoped), {D} alone (docs/-scoped). 2. Every assertion present in the pre-state file is present in exactly one post-state module — none dropped, none duplicated. 3. No module's scan root is narrower than its pre-state scope (specifically: the module hosting Gate B must still scan src/, not src/doctrine/, even though it now lives alongside Gate C's module or separately — whichever the implementer chooses, as long as scope is preserved). 4. The shared helpers (Site/_rel/_read_lines/_text_files/root constants) are extracted into one common module both post-split modules import — not duplicated into each, matching this directory's existing convention of underscore-prefixed shared modules (_gate_coverage.py, _sole_door_scan.py).
Post-state contract (FR-002)
1. test_forbidding_mention_would_false_red_without_its_discriminator (or its post-split equivalent) is redriven against a tmp_path-planted synthetic fixture, not doctrine-daphne.agent.yaml. 2. The fixture-based proof still fails (reds) if the discriminator's effect set is empty, or if it silently swallows a new, unexpected exclusion (the anti-widening property) — i.e. it is provably equivalent in strength to the pre-state live-artifact proof, just decoupled from which artifact demonstrates it. 3. packs/built-in/agent_profiles/doctrine-daphne.agent.yaml's avoidance-boundary (line 136) no longer mentions src/doctrine/graph.yaml (the "daphne cleanup"), and the full gate suite is green with that removal in place. (Corrected 2026-08-04: this item previously cited the pre-relocation path src/doctrine/agent_profiles/built-in/..., which no longer exists on disk.) 4. Gate C's cross-link case receives the same fixture-decoupling treatment for its own on-disk-resolution requirement (per US2-AS3).
Falsification
This contract is falsified if, after implementation: (a) removing the doctrine-daphne.agent.yaml repo-local reference fails the gate suite, (b) a planted violation against the new fixture does NOT fail the gate suite, or (c) any pre-state assertion is missing from the post-state modules.
kernel-resolution-primitive.md
Contract: Kernel-Owned Sibling-Path-Resolution Primitive (FR-004)
Not an HTTP/API contract — this mission has no network surface. This is the behavioral contract the new kernel primitive and its two call sites (kernel.paths.get_package_asset_root(), doctrine.pack_paths._resolve_built_in()) must satisfy.
Inputs
| Input | Source | Constraint |
|---|---|---|
anchor_file | The calling module's own __file__ | Must be the caller's own file — never a string naming another package. |
env_override | Caller-read environment variable (e.g. SPEC_KITTY_TEMPLATE_ROOT, SPEC_KITTY_PACKS_ROOT) | The primitive receives the resolved override value, if any; it does not know environment-variable names (those stay caller-specific, since kernel.paths and doctrine.pack_paths use different variable names today). |
sibling_relative_path | Caller-supplied relative path under the resolved root | e.g. "missions" (for kernel's asset root) or "built-in" (for doctrine's pack root). |
Resolution order (must match pack_paths._resolve_built_in's existing 4-step order exactly)
1. Env override wins, if the resolved override path (joined with sibling_relative_path) is a directory. 2. Editable checkout: walk anchor_file.resolve().parents, looking for packs/built-in (or the equivalent sibling shape) at each ancestor; .resolve() happens before the walk so symlinked editable installs still reach the real repo root. 3. Installed wheel: anchor_file's own containing package directory's parent, joined with the sibling path — this works today because packs/ is force-included as a site-packages sibling of every top-level package in the current monolith wheel, not specifically doctrine (verified against the root pyproject.toml). 4. Fail closed: raise a named exception (analogous to PackRootNotFound) naming what was sought and where it was not found. Never return a nonexistent path; never fall back to an arbitrary tree.
Postconditions
- For every existing passing test in
tests/kernel/,tests/doctrine/, and any test exercisingget_package_asset_root()/resolve_pack_root("built-in")/MissionTemplateRepository.default_missions_root(), behavior is unchanged (NFR-001). - No code path inside
src/kernel/— including any transient/interim state during implementation — holds the literal string"doctrine","specify_cli", or any mission-type name ("software-dev","documentation","research","plan"). - Three call sites converge onto this primitive, not two:
kernel.paths.get_package_asset_root(),doctrine.pack_paths._resolve_built_in(), anddoctrine.missions.repository.MissionTemplateRepository.default_missions_root()(the authority a prior mission's WP06 already promoted, pertests/charter/test_missions_root_authority.py, and explicitly deferred converging to this issue). doctrine.pack_paths.doctrine_package_dir()is untouched — a separate, identity-pinned public symbol (tests/doctrine/test_built_in_location_authority.py) also consumed directly bydrg/migration/extractor.py. This contract replaces_resolve_built_in()'s internal call to it, never the symbol itself.- Since kernel cannot import
doctrine.pack_paths.PackRootNotFound(layer direction), the primitive raises its own exception type.pack_paths._resolve_built_in()'s call site must catch-and-re-raise asPackRootNotFound— at least one consumer (specify_cli/doctrine/pack_validator.py:793,except (PackRootNotFound, BuiltInContentDirNotAvailable)) depends on that specific type surviving at thepack_pathsboundary.
Verification (NFR-002)
A new kernel-scoped architectural test, in the same AST-walk idiom as tests/architectural/test_charter_no_specify_cli_import.py (that gate's own docstring explains why pytestarch's import-edge analysis is insufficient here — it does not see a string-literal importlib.resources.files(...) call), must:
1. Walk every module under src/kernel/** and fail if any AST node contains the literal strings named above (import, call-argument, or f-string component). 2. Demonstrate non-vacuity via self-mutation: temporarily reintroducing the pattern must turn the new gate red, naming the exact site (mirrors the charter gate's own NFR-004-style proof).