Contracts
context-decomposition-parity.md
Contract: context.py decomposition parity & completion (US3 / FR-007…FR-009, NFR-001/002/005, SC-004)
Behaviour-preserving surface
build_charter_context, build_charter_context_include, build_charter_context_json produce byte-identical output before/after decomposition.
Non-fakeable parity corpus (NFR-001)
The golden fixture MUST include, at minimum, one input each that traverses:
Enforcement (not just prose): the parity test enumerates the three named cases and independently asserts each hit its distinguishing marker (over-budget → fetch-stanza swap line present; miss → structured miss stanza present; first-load → state file written). Deleting any input reds the suite — this is what makes "0 diffs" meaningful (a no-op passes byte-parity over any corpus).
- token-budget substitution (over-budget body → fetch-stanza swap),
- catalog-miss fall-through (missing artefact → structured miss stanza),
- first-load state bookkeeping (state-file write path).
Baseline provenance (Decision 10)
The parity-baseline-capture WP depends on the US1 (empty-charter) + US2 (prose) WPs being approved, and the captured golden must include the empty-charter input producing the generic-agent / empty-Directive-IDs output — proving the baseline was generated post-US1 (guards the US1∩US3 collision, since all WPs share one mission with no inter-workstream merge).
Completion signal (SC-004) — WIRED, non-fakeable
A dedicated test tests/charter/test_context_decomposition_completion.py (owned by the last US3 WP) asserts BOTH: 1. Primary (un-fakeable): seam-existence manifest — each named seam module (see data-model.md) exists AND is imported by ≥1 non-context caller (imported from the seam, not only re-exported through context.py). 2. Secondary: LOC gate — wc -l src/charter/context.py ≤ 600 (grounded floor ≈500–540; ≤500 is the aspirational stretch; the earlier 400 is dropped). If context.py cannot reach ≤600, that is a BLOCKER requiring explicit operator re-sign-off — NOT an implementer-side ceiling adjustment.
Regression guards (already green on the monolith — NOT completion signals)
test_layer_rules.py,test_runtime_charter_doctrine_boundary.py— nospecify_cliimport undersrc/charter/.test_no_dead_symbols.py— everysrc/charter/module declares__all__, every export called.
Cycle dissolution (NFR-001 acyclicity)
The 4 symbols profile_sections.py function-locally imports from charter.context (_render_fetch_stanza, _budget_estimate, _diagnose_catalog_miss, _PROFILE_INLINE_BODY_LIMIT_CHARS) move to LEAF homes; profile_sections.py imports them at top level; both # noqa: PLC0415 are deleted; no new cycle.
FR-009 preserved surface
Every private symbol imported from charter.context by tests (test-only; ~40 incl. _reset_agent_profile_cache used by 4 files) stays importable via a # FR-009 preserved surface re-export block in context.py.
Ordering (Decision 8)
US3 parity baseline captured only after US1+US2 merge; profile_resolution + doctrine_service_builder seams extracted LAST against the frozen US1 wrapper region.
empty-charter-fallback.md
Contract: Empty-charter dispatch fallback (US1 / FR-002…FR-006)
Seam
invocation/executor.py auto-route branch (:255-259, the only auto-route entry point — verified) calls invocation/empty_charter.py::resolve_generic_fallback(repo_root, request_text) -> RouterDecision | None.
Predicate (composite — ALL charter-activatable dimensions)
pc = PackContext.from_config(repo_root)
is_empty = (charter_activated_urns(repo_root) == set()) # 6 URN kinds
and pc.activated_agent_profiles is None
and pc.activated_mission_step_contracts is None
and pc.activated_glossary_packs is None
and pc.org_roots == () # no org/project packs
# anti_pattern is NOT charter-activatable → excluded.
Predicate truth table (all MUST be tested)
| Charter state | is_empty |
|---|---|
| nothing activated, no org packs | True → fallback |
| any URN kind activated | False |
activated_agent_profiles non-None (incl. []) | False |
activated_mission_step_contracts non-None | False |
activated_glossary_packs non-None | False |
org/project pack present (org_roots != ()) | False |
Routing behaviour
| Given | When | Then |
|---|---|---|
is_empty, no --profile hint | dispatch auto-routes | RouterDecision(profile_id="generic-agent", action=<derived from verb>, confidence="generic_fallback"); InvocationPayload.empty_charter_fallback = True |
is_empty, explicit --profile architect-alphonso | dispatch | specialist resolves normally (fallback returns None) |
| any activation present | dispatch | resolve_generic_fallback returns None; existing routing unchanged |
is_empty | software-dev mission type requested | still available (always-on) — assert explicitly |
Governance-context agreement (NOT free — US1 scope)
Under an empty charter, render_compact_view (compact.py:216-230) merges resolver_directives from resolve_project_governance, and _resolve_directives_selection (resolver.py:233-260) catalog-falls-back to ALL built-in DIR-### when nothing is selected. So the generic-agent dispatch would leak the full directive canon.
- Requirement: on the
empty_charter_fallbackpath, the governance block must NOT merge the project catalog-fallback directives (preferred:build_charter_context(..., suppress_project_resolver=True)or a scoped minimal-governance render for the fallback; do NOT globally change_resolve_directives_selection). - Red-first agreement test: dispatch under a wholly-empty charter; assert
payload.profile_id == "generic-agent", the governanceDirective IDs:block is empty (or exactly generic-agent's own cited directives), no specialist marker,empty_charter_fallback is True. RED before the scoping lands.
Warning surface
cli/commands/dispatch.py::_render_rich_payload (lines 59-91), gated on getattr(payload, "empty_charter_fallback", False) (payload uses dynamic __init__ — read defensively; always thread the kwarg at the single construction site). One yellow panel; --json exposes the boolean.
Default charter asset (FR-005)
Ship src/doctrine/assets/built-in/charter_scaffold_minimal.yml + .asset.yaml (id: common-charter-scaffold-minimal, mime: application/yaml). Resolve via spec-kitty doctrine asset path common-charter-scaffold-minimal. Done-line: (a) resolves; (b) pack_validator mime guard test; (c) activatability test — activate the scaffold in a temp repo, assert the resulting charter validates, no user charter touched.
Untouched (regression proof — MUST stay green, NOT owned)
test_doctrine_service_factory.py::, test_registry_builtin_activation_parity.py::{test_excluded_builtin_absent_from_routing_and_context, test_no_activation_key_admits_all_builtins_in_routing}, tests/charter/test_activation_authority.py::.
New coverage (owned)
tests/specify_cli/invocation/test_empty_charter_fallback.py (predicate truth table, decision shape, action-derivation preserved, RouterDecision Literal), tests/specify_cli/invocation/cli/test_dispatch.py (auto-route→generic-agent + warning + governance-agreement (Directive IDs empty) + --json flag + explicit-profile bypass + software-dev availability), plus the asset activatability + mime tests.
fetch-stanza-normalization.md
Contract: Fetch-stanza when-clause normalization (US2 / FR-001, NFR-003)
Site
context_renderers/fetch_stanza.py::fetch_stanza_lines (line ~133) — the single composition choke-point for When you {clause}, run this command and apply the returned rule.
Pinned wire contract (must keep matching, per-stanza)
(tests/specify_cli/next/test_wp_prompt_governance_contract.py:221.)
_FETCH_CMD_RE— theRun: spec-kitty charter context --include <selector>line._WHEN_DOING_RE = r"when\s+you\s+(are\s+about\s+to|need\s+to|encounter|introduce|rename|review)"— closed 6-verb set.
Behaviour
| Given clause shape | Then emitted line |
|---|---|
leading gerund (designing or reviewing significant code changes) | grammatical, headed by a closed lead-in (e.g. are about to …), matches _WHEN_DOING_RE |
full sentence w/ trailing period (STATED_DEFAULT_WHEN fallback) | no When you <sentence>., … doubling; grammatical; matches |
already well-formed (are about to apply a code change) | byte-unchanged from today |
Assertion upgrade
NFR-003 is asserted per rendered stanza via a dedicated helper, not the current whole-prompt _WHEN_DOING_RE.search(prompt) (which passes if any single line matches).
Escape hatch
If a clause genuinely cannot be mapped into the closed set, _WHEN_DOING_RE is widened as a deliberate, documented contract change kept in sync with kitty-specs/wp-prompt-governance-payload-01KRR8HS/contracts/charter-context-resolver.md. Default is to normalize, not widen.