Contracts

doctrine-integrity-gates.md

Contract: Doctrine-Integrity Gates (acceptance)

The landed set must satisfy every gate below. These are the executable DoD for WS1 + the validation half of WS2. Run from the repo root with the isolated env active.

G-1 — Surface (SC-001, C-001)

asset pairs + READMEs exist under packs/built-in/… at the paths in data-model.md §1.

  • Assert: git ls-files 'src/doctrine//built-in/' returns empty; all 16 YAML + 5
  • Command: find packs/built-in -path 'writing' -o -name 'comms-cleo.agent.yaml' … (spot-check) + git grep -l 'built-in/built-in' || true (must be empty).

G-2 — Validation (SC-002, NFR-003)

resolves once assets are in place.

spec-kitty doctrine pack validate packs/built-in → OK.

  • Assert: every new artifact passes schema validation; the type: asset tactic reference
  • Command: spec-kitty doctrine validate <each new artifact> → 0 errors; and

G-3 — Doctor health (SC-002, NFR-001)

pack discovered_count == valid_count == 25, invalid_profiles == [], skipped_profiles contains none of the 7 new ids, org_drg.errors == [], exit 0.

  • Assert: spec-kitty doctor doctrine --jsonprofile_health.healthy == true, builtin

G-4 — Shipped-profiles gate (SC-002, D-04)

6-field contract (canonical-verbs, output-artifacts, mode-defaults+use-case, doctrine-layers, directive-references) + ≥1 role; both profile READMEs list exactly the 25 shipped ids.

  • Assert: EXPECTED_PROFILE_IDS contains the 7 new ids (25 total); all 7 satisfy the
  • Command: pytest tests/doctrine/test_shipped_profiles.py -q → green.

G-5 — DRG freshness & reachability (SC-003, NFR-001, C-002)

pytest tests/doctrine/drg/test_reachability.py -q → green (frozensets recomputed empirically; a moved pin carries a ledger row).

  • Assert: committed fragments are fresh; every new node is reachable (no new orphan).
  • Command: spec-kitty doctrine regenerate-graph --check → exit 0 (no staleness);

G-6 — Pack relocation doctor gate (SC-001, D-05)

post-regenerate measured values; glossary term count still 108.

  • Assert: EXPECTED_PROFILE_COUNT == 25; the (node_count, edge_count) tuple equals the
  • Command: pytest tests/doctrine/test_pack_relocation_doctor_gate.py -q → green.

G-7 — Terminology & no-greenwash (C-004)

  • Assert: new prose obeys the Terminology Canon; assets stay asset (no relabel).
  • Command: pytest tests/architectural/test_no_legacy_terminology.py -q → green.

G-8 — Full suite on CI (SC-006)

The full tests/ suite is the CI release authority and must be green before the operator merges. Not run in-session.

  • The targeted gates above run locally per-WP (charter Testing Requirements — targeted only).

routing-behavior.md

Contract: Routing Behavior (FR-005, NFR-002, SC-004)

Preserve legacy dispatch.

Required test form (non-fakeable): the regression MUST assert against the shipped profiles — construct a ProfileRegistry populated from the real packs/built-in/agent_profiles/ YAML (not fixture stubs, not MagicMock), so the test pins the narrowed YAML and not merely the router mechanic. The _make_mock_registry([...]) form shown below and in research D-03 is illustrative of the mechanic only — a mock hand-feeds role and priority, so it passes regardless of the shipped YAML and MUST NOT be the sole coverage. Each scenario also asserts the shipped profile.role (e.g. diagram-daisy.role == "diagram-author") so a mock cannot satisfy it.

R-1 — Incumbent designer preserved (negative regression; RED-first)

(role designer, priority 60) and no discriminating context,

diagram-daisy — the test fails. After the narrowing (roles[0]=diagram-author) it passes.

  • Given a registry with designer-dagmar (role designer, priority 50) and diagram-daisy
  • When route("design the login screen"),
  • Then decision.profile_id == "designer-dagmar".
  • RED-first: on the base (diagram-daisy still roles[0]=designer) this returns

R-2 — Incumbent curator preserved (negative regression; RED-first)

role curator, no context,

  • Given curator-carla@40, doctrine-daphne@48, comms-cleo@55, synthesizer-sam@50 all
  • When route("classify these documents"),
  • Then decision.profile_id in {"curator-carla", "doctrine-daphne"} (an incumbent).
  • RED-first: on the base returns comms-cleo; after narrowing cleo+sam leave the bucket → passes.

R-3 — New profile still routes for its own scope (positive guard)

  • Given diagram-daisy narrowed to diagram-author with its domain keywords,
  • When a request explicitly names diagram-as-code / the profile hint,
  • Then diagram-daisy is selected — narrowing did not strand it.

R-4 — No unintended re-collision from canonical-verbs (guard)

assert the narrowed profiles do NOT re-enter the DESIGNER/CURATOR buckets via a generic verb.

  • The canonical-verbs added for the shipped-profiles contract (D-04) must be domain-specific;

R-5 — Researcher non-collision (positive documentation; SC-004)

comms-cleo/synthesizer-sam do NOT collide on researcher (it is their secondary role). This proves the D-03 sharpening rather than leaving SC-004's researcher clause unmapped.

  • Given a bare researcher verb (e.g. route("research the market")) with no context,
  • Then an incumbent researcher (researcher-robbie) is selected — documenting that

Shipped-registry loading (required)

Populate a ProfileRegistry from the real packs/built-in/agent_profiles/ set (copy the shipped YAML into the registry dir — do NOT reuse FIXTURES_DIR stems, which _make_registry defaults to). Assert the registry actually contains diagram-daisy/comms-cleo/synthesizer-sam loaded from packs/built-in, and assert their narrowed profile.role values, so neither a mock nor a stale fixture can pass the regression.