diff --git a/chat/runtime.py b/chat/runtime.py index 20bea64c..4071bcb6 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -37,6 +37,7 @@ from core.epistemic_state import ( epistemic_state_for_grounding_source, normative_detail_from_verdicts, ) +from core.response_governance import govern_response, shape_surface from chat.telemetry import ( TurnEventSink, format_correction_event_jsonl, @@ -447,6 +448,13 @@ class ChatResponse: # Comma-separated violated boundary/commitment IDs when normative # clearance is VIOLATED or SUPPRESSED; empty string otherwise. normative_detail: str = "" + # ADR-0206 — Response Governance Bridge reach level for this turn. + # Mirrors the TurnEvent field so callers (CLI, demos, tests) can read + # the governed reach level from ChatResponse. Scaffold contract: + # always "strict" (govern_response is STRICT-only until widening is + # built). ``"strict"`` default preserves byte-identity for callers + # that construct ChatResponse without this field. + reach_level: str = "strict" # ADR-0072 (R5) — operator-visible register identity per turn. # Mirrors the TurnEvent fields so callers (CLI, demos, tests) can # read the register state from ChatResponse without re-parsing the @@ -2335,9 +2343,24 @@ class ChatRuntime: refusal_emitted=refusal_emitted, hedge_injected=hedge_injected, ) - main_epistemic_state = epistemic_state_for_grounding_source( - main_grounding_source - ).value + main_epistemic = epistemic_state_for_grounding_source(main_grounding_source) + main_epistemic_state = main_epistemic.value + # ADR-0206 — Response Governance Bridge seam (cognition path). + # govern_response is STRICT-only (scaffold), so shape_surface is the + # identity transform and response_surface is returned verbatim — + # byte-identical to the pre-bridge path. This is live wiring, not + # dead code: the response surface now flows through the policy + # consumer, and the ONLY thing keeping it strict is the STRICT + # return value (proven by the live-wiring test). The risk-reward + # widening pathway and the math-serving seam are deferred to their + # own PRs (ADR-0206 §5); wrong==0 is untouched here. + main_reach_policy = govern_response(epistemic_state=main_epistemic) + main_reach_level = main_reach_policy.level.value + response_surface = shape_surface( + main_reach_policy, + committed_surface=response_surface, + decode_state=main_epistemic, + ) main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value main_normative_detail = normative_detail_from_verdicts(verdicts_bundle) turn_event = TurnEvent( @@ -2371,6 +2394,7 @@ class ChatRuntime: epistemic_state=main_epistemic_state, normative_clearance=main_normative_clearance, normative_detail=main_normative_detail, + reach_level=main_reach_level, ) self.turn_log.append(turn_event) self._emit_turn_event(turn_event) @@ -2421,6 +2445,7 @@ class ChatRuntime: epistemic_state=main_epistemic_state, normative_clearance=main_normative_clearance, normative_detail=main_normative_detail, + reach_level=main_reach_level, refusal_reason=refusal_surface if refusal_emitted else "", dispatch_trace=dispatch_trace, ) diff --git a/core/physics/identity.py b/core/physics/identity.py index bc261814..d2968be5 100644 --- a/core/physics/identity.py +++ b/core/physics/identity.py @@ -298,6 +298,15 @@ class TurnEvent: epistemic_state: str = "undetermined" normative_clearance: str = "unassessable" normative_detail: str = "" + # ADR-0206 — Response Governance Bridge reach level for this turn. + # The reach policy that governed the response surface, as a + # lower_snake_case string mirroring core.response_governance.ReachLevel + # without importing that module here (preserving identity.py's + # low-coupling shared-value-type role). Scaffold contract: always + # "strict" — govern_response emits STRICT-only until the risk-reward + # widening loop is built (ADR-0206 §3). Default "strict" so callers + # that omit the field stay byte-identical and conservatively governed. + reach_level: str = "strict" # ADR-0153 (W-020a) — canonical SHA-256 trace hash for this turn, # back-stamped by ``CognitiveTurnPipeline.process`` after # ``compute_trace_hash`` runs. Empty string on construction; diff --git a/core/response_governance/__init__.py b/core/response_governance/__init__.py new file mode 100644 index 00000000..260ba58b --- /dev/null +++ b/core/response_governance/__init__.py @@ -0,0 +1,47 @@ +"""ADR-0206 — Response Governance Bridge (scaffold). + +The consumption bridge that lets CORE's two *built* epistemic substrates — +the ratified decode-state taxonomy (:mod:`core.epistemic_state`) and the +deterministic risk-reward gate (:mod:`core.reliability_gate`) — govern how a +response is shaped. Today that loop has **no consumer** (see ADR-0206 §1); +this package is its first, honestly-inert beginning. + +Scaffold contract (ADR-0206 §3): + +- :func:`govern_response` always returns :data:`STRICT_POLICY`. The + stakes-weighing / license-gated widening logic is *designed* in the ADR + and intentionally **not built**. +- :func:`shape_surface` is the IDENTITY transform at + :attr:`ReachLevel.STRICT` — the only level emitted today — so wiring it + into the response path is byte-identical to the pre-bridge path. + +The seam is therefore **live wiring held strict by exactly one return +value** (the stub's STRICT), not dead code. ``wrong == 0`` is preserved +because nothing widens: the math serving chokepoint +(``select_self_verified``) is untouched by this PR (deferred to its own +PR per ADR-0206 §5). +""" + +from __future__ import annotations + +from core.response_governance.policy import ( + ACTIVE_STATES, + RECONCILE_STATES, + RESERVED_STATES, + STRICT_POLICY, + ReachLevel, + ReachPolicy, + govern_response, + shape_surface, +) + +__all__ = [ + "ACTIVE_STATES", + "RECONCILE_STATES", + "RESERVED_STATES", + "STRICT_POLICY", + "ReachLevel", + "ReachPolicy", + "govern_response", + "shape_surface", +] diff --git a/core/response_governance/policy.py b/core/response_governance/policy.py new file mode 100644 index 00000000..2b267d8c --- /dev/null +++ b/core/response_governance/policy.py @@ -0,0 +1,186 @@ +"""ADR-0206 §2-§3 — the reach policy and its STRICT-only scaffold. + +A :class:`ReachPolicy` is the per-response decision about *how far into +uncertainty a response may reach* — the thing CORE today does not compute. +The full bridge will derive it from the decode-state of the grounding and +the reliability gate's license (ADR-0206 §1). This module ships only the +**STRICT** end of that spectrum, wired so the seam is live but inert. + +Two functions form the seam: + +``govern_response`` + The decision point. Scaffold: returns :data:`STRICT_POLICY` for every + input. This is the single return value the ``wrong == 0`` guarantee + rests on — mutate it and the live-wiring tests diverge loudly. + +``shape_surface`` + The consumer. At :attr:`ReachLevel.STRICT` it is the IDENTITY + transform (returns the committed surface verbatim). The higher reach + levels are the future widening pathway; they are *unreachable in + production today* because ``govern_response`` never emits them, but + they are real, policy-sensitive code so the seam is not dead. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum, unique + +from core.epistemic_state import EpistemicState + + +@unique +class ReachLevel(Enum): + """How far a governed response may reach beyond fully-grounded fact. + + Ordered conceptually STRICT < APPROXIMATE < EXTRAPOLATE < CREATIVE. + Only STRICT is emitted by the scaffold; the rest name the spectrum the + bridge will earn the right to enter via the reliability gate. + """ + + STRICT = "strict" # commit only fully-grounded; else refuse/disclose (TODAY) + APPROXIMATE = "approximate" # disclosed best-estimate from incomplete evidence + EXTRAPOLATE = "extrapolate" # disclosed inference past the evidence + CREATIVE = "creative" # disclosed imaginative / novel construction + + +@dataclass(frozen=True, slots=True) +class ReachPolicy: + """The per-response governance decision. + + ``admissible_states`` is the set of decode-states whose surface may be + *committed as-is* at this level; states outside it are either refused + (STRICT) or surfaced as a disclosed alternative (higher levels). + ``rationale`` and ``license_ratio`` keep the decision inspectable — + the same discipline the reliability gate uses for ``LicenseDecision``. + """ + + level: ReachLevel + admissible_states: frozenset[EpistemicState] + rationale: str + license_ratio: float = 0.0 + + +# --- Taxonomy scoping (ADR-0206 §4) ----------------------------------------- +# The 15 declared EpistemicState members, partitioned by their role in the +# governing loop. This is the "honestly scoped, not half-dead" contract: +# every state is either ACTIVE (produced today AND consumed by the loop), +# RESERVED (named here, not yet produced, each tied to a capability that +# must land first), or RECONCILE (a real drift, fixed in its own PR). +# +# Verified against the source tree on the ADR-0206 branch: exactly 9 states +# are ever produced (DECODED ×6, UNDETERMINED ×3, EPISTEMIC_STATE_NEEDED ×3, +# UNVERIFIED_POSSIBLE/UNVERIFIED_NOVEL/CONTRADICTED/AMBIGUOUS ×2, +# EVIDENCED_INCOMPLETE/INFERRED ×1) and 6 are never produced. Of those 6, +# five are RESERVED and one (EVIDENCED) is RECONCILE. + +ACTIVE_STATES: frozenset[EpistemicState] = frozenset( + { + EpistemicState.DECODED, # fully grounded — strict-admissible + EpistemicState.EVIDENCED_INCOMPLETE, # partial grounding — approximate+ + EpistemicState.INFERRED, # derived — extrapolate+ + EpistemicState.UNVERIFIED_POSSIBLE, # possibility — extrapolate+ (disclosed) + EpistemicState.UNVERIFIED_NOVEL, # OOV — creative only (disclosed) + EpistemicState.CONTRADICTED, # never committed — force refuse + EpistemicState.AMBIGUOUS, # never committed — force refuse/clarify + EpistemicState.UNDETERMINED, # never committed — refuse + EpistemicState.EPISTEMIC_STATE_NEEDED, # sentinel — force situate step + } +) + +# Never produced today; each unlocks only when its named capability lands. +RESERVED_STATES: frozenset[EpistemicState] = frozenset( + { + EpistemicState.VERIFIED, # needs canonical-comparison pass (soundness != correctness); + # the ONLY state that will license widening past gold. + EpistemicState.COMPUTATIONALLY_BOUNDED, # search-budget exhausted; search.py is the near-term emitter. + EpistemicState.SCOPE_BOUNDARY, # out-of-domain refusal; needs domain-scope detection. + EpistemicState.PERCEIVED, # raw ingest pre-comprehension; needs a perception lane. + EpistemicState.DECODED_UNARTICULATED, # decoded but no realizer surface; articulation-gap case. + } +) + +# Declared here but never produced, while recognition/outcome.py defines its +# OWN "evidenced" constant. Real drift — reconciled in a dedicated PR +# (ADR-0206 §5), kept OUT of this purely-additive scaffold. +RECONCILE_STATES: frozenset[EpistemicState] = frozenset({EpistemicState.EVIDENCED}) + + +# --- The scaffold's single governing decision ------------------------------- + +# STRICT admits only the fully-grounded decode-state; everything else is +# refused/disclosed by the existing response path. This is informational +# at STRICT (shape_surface short-circuits to identity) but records the +# contract the bridge will enforce once widening is built. +_STRICT_ADMISSIBLE: frozenset[EpistemicState] = frozenset({EpistemicState.DECODED}) + +STRICT_POLICY: ReachPolicy = ReachPolicy( + level=ReachLevel.STRICT, + admissible_states=_STRICT_ADMISSIBLE, + rationale="scaffold:strict-only (ADR-0206 §3 — risk-reward widening not yet built)", + license_ratio=0.0, +) + +# Disclosure prefixes for the (currently unreachable) widening levels. Real +# code so the higher-level branch of shape_surface is genuinely +# policy-sensitive, exercised by the live-wiring test. +_DISCLOSURE_PREFIX: dict[ReachLevel, str] = { + ReachLevel.APPROXIMATE: "[approximate]", + ReachLevel.EXTRAPOLATE: "[extrapolated]", + ReachLevel.CREATIVE: "[exploratory]", +} + + +def govern_response( + *, + epistemic_state: EpistemicState | None = None, + license_decision: object | None = None, + stakes: object | None = None, +) -> ReachPolicy: + """Decide the reach policy for a response. + + SCAFFOLD (ADR-0206 §3): returns :data:`STRICT_POLICY` unconditionally. + The inputs are accepted now so the call site is the final shape, but + the stakes-weighing and license-gated widening they will drive is + *designed, not built*. Every response is therefore governed at STRICT + — commit-only-when-grounded, else the existing refuse/disclose path — + which is exactly the pre-bridge behavior. + + This single return value is the load-bearing line for ``wrong == 0``: + the live-wiring tests prove that the *only* thing keeping the response + path strict is this STRICT return, not the absence of a consumer. + """ + return STRICT_POLICY + + +def shape_surface( + policy: ReachPolicy, + *, + committed_surface: str, + decode_state: EpistemicState, + disclosed_alternative: str | None = None, +) -> str: + """Apply ``policy`` to a committed surface — the response-path consumer. + + At :attr:`ReachLevel.STRICT` (the only level :func:`govern_response` + emits today) this is the IDENTITY transform: the committed surface is + returned verbatim, byte-identical to the pre-bridge path. This is why + wiring the seam into :mod:`chat.runtime` changes no behavior. + + The higher reach levels are the future widening pathway. They are + real and policy-sensitive — a non-admissible decode-state is surfaced + as a *disclosed alternative* rather than committed silently — so the + seam is live wiring, not dead code. They are unreachable in production + today only because ``govern_response`` never emits them. + """ + if policy.level is ReachLevel.STRICT: + return committed_surface + + # --- Future widening pathway (unreachable in production today) --- + alternative = ( + committed_surface if disclosed_alternative is None else disclosed_alternative + ) + if decode_state in policy.admissible_states: + return alternative + prefix = _DISCLOSURE_PREFIX.get(policy.level, "") + return f"{prefix} {alternative}".strip() if prefix else alternative diff --git a/docs/decisions/ADR-0206-response-governance-bridge.md b/docs/decisions/ADR-0206-response-governance-bridge.md new file mode 100644 index 00000000..2856f5f0 --- /dev/null +++ b/docs/decisions/ADR-0206-response-governance-bridge.md @@ -0,0 +1,167 @@ +# ADR-0206 — Response Governance Bridge (scaffold) + +- **Status:** Accepted (scaffold step; widening deferred) +- **Date:** 2026-06-03 +- **Supersedes / relates to:** ADR-0175 (calibrated-learning reliability + gate), the Epistemic-State program (Phase 3, `core/epistemic_state.py`) +- **Scope of THIS PR:** purely additive — design on record + cognition-path + seam, STRICT-only, no behavior change. + +## Context — the consumption gap + +CORE has two **built** epistemic substrates: + +1. **A ratified decode-state taxonomy** — `core/epistemic_state.py`, a + `@unique` 15-member `EpistemicState` enum plus an orthogonal + `NormativeClearance` axis. Every chat turn is *labeled* with its + decode-state (derived from the grounding source) and the label is + stamped onto `TurnEvent` / `ChatResponse` and emitted to telemetry. + +2. **A deterministic risk-reward gate** — `core/reliability_gate/` + (ADR-0175). `license_for(class, action, ceilings)` decides whether a + class has *earned* an action via `measured_reliability / θ_required ≥ 1`. + +A source audit (2026-06-03) established the load-bearing fact this ADR +addresses: + +> **The decode-state taxonomy is LABEL-ONLY.** No consumer reads a +> decode-state to weigh stakes, modulate response risk, or gate +> approximation/creativity. Every read is a stamp, serialize, badge, or +> filter. The reliability gate is likewise *explicitly not serving-wired* +> (`core/reliability_gate/__init__.py`: "NOT wired into the serving/eval +> path"). The loop that would let these substrates **govern response +> behavior** has zero consumers. + +This is the same shape as the ratify-vs-consume gap: the artifacts exist, +the runtime does not read them to act. + +## Decision + +Introduce the **Response Governance Bridge** — the missing consumer that +reads the two substrates and produces a per-response `ReachPolicy` +governing *how far into uncertainty a response may reach*. This ADR ships +only the **scaffold**: the design on record + a live but inert seam on the +cognition path. The risk-reward widening logic and the math-serving seam +are explicitly deferred (§5). + +### §1 — The governing loop (designed; not all built) + +```mermaid +flowchart LR + T[turn] --> S[SITUATE
stakes/gravity] + S --> L[LICENSE
reliability_gate.license_for] + L --> G[GOVERN
govern_response → ReachPolicy] + G --> H[SHAPE
shape_surface reads policy] + H --> D[DISCLOSE
decode-state label] + D --> F[FEED BACK
outcome → ledger] + F -. next turn .-> L +``` + +| Step | Status | +|------|--------| +| SITUATE (stakes/gravity from intent + domain + decode-state) | **designed** — not built | +| LICENSE (`reliability_gate.license_for`) | **built** — not yet called from serving | +| GOVERN (`govern_response → ReachPolicy`) | **this PR** — stubbed to STRICT | +| SHAPE (`shape_surface`, response path reads policy) | **this PR** — STRICT = identity | +| DISCLOSE (decode-state label) | **built** | +| FEED BACK (outcome → reliability ledger) | **designed** — not built | + +Only **GOVERN + SHAPE** ship here. SITUATE and FEED-BACK are named and +deferred to follow-ups. + +### §2 — The reach policy + +`core/response_governance/policy.py` defines: + +- `ReachLevel` — `STRICT < APPROXIMATE < EXTRAPOLATE < CREATIVE`. Only + `STRICT` is emitted today; the rest name the spectrum the bridge will + earn entry to via the reliability gate. +- `ReachPolicy(level, admissible_states, rationale, license_ratio)` — the + inspectable per-response decision (same discipline as + `reliability_gate.LicenseDecision`). +- `govern_response(*, epistemic_state, license_decision, stakes)` — the + decision point. **Scaffold: returns `STRICT_POLICY` for every input.** +- `shape_surface(policy, *, committed_surface, decode_state, + disclosed_alternative)` — the consumer. **At `STRICT` it is the IDENTITY + transform.** Higher levels surface a *disclosed alternative* for + non-admissible decode-states — real, policy-sensitive code so the seam is + live, but unreachable in production because `govern_response` never emits + them. + +### §3 — Why STRICT-only scaffold + +The scaffold makes the email's "in active development" claim **true the +moment it lands** while guaranteeing `wrong == 0` is untouched: + +- `govern_response → STRICT` is the **single load-bearing return**. The + cognition response surface now flows through `shape_surface`, but STRICT + is identity, so the path is **byte-identical** to pre-bridge. +- The seam is **live wiring, not dead code**: `test_seam_is_live_wiring` + forces `APPROXIMATE` and proves the *same consumer the production path + calls* yields a different surface. The cognition path's strictness is + held by exactly the STRICT return — mutate it and the proofs diverge. +- The **math-serving chokepoint (`select_self_verified`) is not touched.** + It is the single most load-bearing line for `wrong == 0`; after the + ProofNode lesson (a merge silently killed a guard) it does not get its + first edit in a scaffold PR. The deferred math seam (§5) carries its own + widening test. + +### §4 — Taxonomy scoping (honestly scoped, not half-dead) + +Source-verified split of the 15 `EpistemicState` members (2026-06-03, +including the mapping functions in `epistemic_state.py`): **9 produced, 6 +never produced.** (The earlier audit's "7 never produced" over-counted — +its grep excluded `epistemic_state.py`, where the partial→`EVIDENCED_INCOMPLETE` +mapping lives. `EVIDENCED_INCOMPLETE` *is* produced; corrected count is 6.) + +| Role | Count | States | Consumed by loop as | +|------|-------|--------|---------------------| +| **ACTIVE** (produced + consumed) | 9 | `DECODED`, `EVIDENCED_INCOMPLETE`, `INFERRED`, `UNVERIFIED_POSSIBLE`, `UNVERIFIED_NOVEL`, `CONTRADICTED`, `AMBIGUOUS`, `UNDETERMINED`, `EPISTEMIC_STATE_NEEDED` | strict-admissible / approximate+ / extrapolate+ / creative / force-refuse / sentinel | +| **RESERVED** (named trigger, not produced) | 5 | `VERIFIED`, `COMPUTATIONALLY_BOUNDED`, `SCOPE_BOUNDARY`, `PERCEIVED`, `DECODED_UNARTICULATED` | each gated on a capability that must land first | +| **RECONCILE** (drift, own PR) | 1 | `EVIDENCED` | declared but never produced; `recognition/outcome.py` owns its own `"evidenced"` | + +Reserved triggers: +- `VERIFIED` — needs a canonical-comparison pass (the soundness ≠ + correctness gap); the **only** state that will license widening past gold. +- `COMPUTATIONALLY_BOUNDED` — search-budget exhausted; `generate/derivation/search.py` + is the near-term emitter. +- `SCOPE_BOUNDARY` — out-of-domain refusal with a reason; needs domain-scope + detection. +- `PERCEIVED` — raw ingest pre-comprehension; needs a perception lane. +- `DECODED_UNARTICULATED` — decoded but no realizer surface; articulation-gap. + +The partition is enforced by `test_taxonomy_partition_is_total_and_disjoint` +(total + pairwise-disjoint over the live enum), so adding an enum member +without scoping it fails the build. + +### §5 — Deferred (one kind of change per PR) + +- **Math-serving seam** — parameterizing `select_self_verified` with a + `ReachPolicy` (STRICT branch byte-identical) + the full math-serving + widening test. Its own PR; smaller blast radius on the wrong==0 organ. +- **`EVIDENCED` reconcile** — map recognition's outcome into + `EpistemicState.EVIDENCED` at the boundary (or document the two axes). A + real corrective defect; kept out of this purely-additive scaffold. +- **JSONL emission of `reach_level`** — `reach_level` is carried on + `TurnEvent` / `ChatResponse` but **not** added to the telemetry audit + dict here, so every pinned lane SHA stays byte-identical. Emission + + SHA re-pin is a follow-up (re-pinning a frozen gate is not "additive"). +- **SITUATE / FEED-BACK / widening** — the risk-reward logic itself. + +## Consequences + +- **Honest claim unlocked:** decode-state taxonomy + risk-reward gate = + *built*; the bridge that lets them govern response reach = *in active + development* (committed seam, STRICT-only, design on record, widening + gated behind wrong==0 proofs). Never "live." +- **`wrong == 0` preserved:** STRICT identity + untouched + `select_self_verified`; serving metric stays `3/47/0`. +- **No frozen gate touched:** telemetry JSONL byte-identical → all pinned + lane SHAs green. + +## Verification + +- `tests/test_response_governance.py` — STRICT-only contract, STRICT + identity, **live-wiring proof (cognition path)**, taxonomy partition. +- Full-surface suite green (doctrine: not smoke). +- `scripts/verify_lane_shas.py` green; `3/47/0` preserved. diff --git a/tests/test_response_governance.py b/tests/test_response_governance.py new file mode 100644 index 00000000..c6817819 --- /dev/null +++ b/tests/test_response_governance.py @@ -0,0 +1,178 @@ +"""ADR-0206 — Response Governance Bridge scaffold tests. + +These are the schema-defined proof obligations for the scaffold (CLAUDE.md +"Schema-Defined Proof Obligations"). Each test is written so it would +**meaningfully fail** if the property it guards were silently broken: + +- ``test_govern_response_is_strict_only`` fails if the stub ever emits a + non-STRICT policy. +- ``test_strict_is_identity`` fails if STRICT stops being byte-identical. +- ``test_seam_is_live_wiring`` fails if ``shape_surface`` stops being + policy-sensitive — i.e. if the seam became dead code that ignores its + policy. This is the proof that the cognition seam is live wiring held + strict by exactly the STRICT return, NOT dead code. +- ``test_taxonomy_partition_is_total_and_disjoint`` fails if the 9/5/1 + scoping drifts from the actual EpistemicState enum. + +No test here imports ``generate.derivation.verify`` — the math-serving +chokepoint (``select_self_verified``) is untouched by this PR (ADR-0206 §5). +""" + +from __future__ import annotations + +import pytest + +from core.epistemic_state import EpistemicState +from core.response_governance import ( + ACTIVE_STATES, + RECONCILE_STATES, + RESERVED_STATES, + STRICT_POLICY, + ReachLevel, + ReachPolicy, + govern_response, + shape_surface, +) + +# A small but representative cross-product of governance inputs. The +# scaffold must return STRICT for ALL of them. +_ALL_STATES = tuple(EpistemicState) +_LICENSE_STANDINS = (None, object(), {"licensed": True}, {"licensed": False}) +_STAKES_STANDINS = (None, "high", "low", 0.0, 1.0) + + +# --- govern_response: STRICT-only contract ---------------------------------- + + +@pytest.mark.parametrize("state", _ALL_STATES) +@pytest.mark.parametrize("license_decision", _LICENSE_STANDINS) +@pytest.mark.parametrize("stakes", _STAKES_STANDINS) +def test_govern_response_is_strict_only(state, license_decision, stakes): + """The stub governs every input at STRICT — the wrong==0 load-bearing line. + + If this fails, the scaffold has begun widening before the risk-reward + loop and its proofs exist; that is the exact regression the STRICT-only + contract forbids. + """ + policy = govern_response( + epistemic_state=state, license_decision=license_decision, stakes=stakes + ) + assert policy.level is ReachLevel.STRICT + assert policy is STRICT_POLICY + + +def test_strict_policy_admits_only_decoded(): + """STRICT's admissible set is exactly {DECODED} — fully-grounded only.""" + assert STRICT_POLICY.admissible_states == frozenset({EpistemicState.DECODED}) + + +# --- shape_surface: STRICT identity + live wiring --------------------------- + + +@pytest.mark.parametrize("state", _ALL_STATES) +def test_strict_is_identity(state): + """At STRICT, shape_surface returns the committed surface verbatim. + + This is the byte-identity guarantee that makes the cognition seam a + no-op. A disclosed_alternative is supplied to prove STRICT ignores it + (it must NOT leak into the surface at STRICT). + """ + committed = f"grounded answer for {state.value}" + out = shape_surface( + STRICT_POLICY, + committed_surface=committed, + decode_state=state, + disclosed_alternative="A DIFFERENT, RISKIER STRING", + ) + assert out == committed + + +def test_seam_is_live_wiring(): + """LIVE-WIRING PROOF (cognition path, no select_self_verified touched). + + Force the governor's decision to APPROXIMATE and show the SAME consumer + the production path calls produces a DIFFERENT surface than at STRICT + for the same input. This proves shape_surface is genuinely + policy-sensitive — the seam reads its policy and is not dead code — and + therefore that the cognition path's strictness is held by exactly the + STRICT return value, nothing else. + """ + committed = "grounded answer" + alternative = "best-effort estimate" + decode_state = EpistemicState.UNVERIFIED_POSSIBLE # not strict-admissible + + strict_out = shape_surface( + STRICT_POLICY, + committed_surface=committed, + decode_state=decode_state, + disclosed_alternative=alternative, + ) + + approximate_policy = ReachPolicy( + level=ReachLevel.APPROXIMATE, + admissible_states=frozenset({EpistemicState.DECODED}), + rationale="test:forced-approximate", + license_ratio=1.0, + ) + approx_out = shape_surface( + approximate_policy, + committed_surface=committed, + decode_state=decode_state, + disclosed_alternative=alternative, + ) + + # The consumer read the policy: STRICT committed verbatim; APPROXIMATE + # surfaced the disclosed alternative with an epistemic prefix. + assert strict_out == committed + assert approx_out != strict_out + assert alternative in approx_out + assert approx_out.startswith("[approximate]") + + +def test_widening_admissible_state_commits_without_disclosure(): + """A widening level commits an admissible decode-state without a prefix. + + Guards the branch that distinguishes "admissible at this level" (commit + the alternative as-is) from "inadmissible" (disclose). Keeps the + future-pathway logic honest and exercised. + """ + widening = ReachPolicy( + level=ReachLevel.APPROXIMATE, + admissible_states=frozenset({EpistemicState.EVIDENCED_INCOMPLETE}), + rationale="test:admissible", + ) + out = shape_surface( + widening, + committed_surface="committed", + decode_state=EpistemicState.EVIDENCED_INCOMPLETE, + disclosed_alternative="alt", + ) + assert out == "alt" # admitted: no disclosure prefix + + +# --- Taxonomy scoping (ADR-0206 §4) ----------------------------------------- + + +def test_taxonomy_partition_is_total_and_disjoint(): + """The 9/5/1 partition covers every EpistemicState exactly once. + + If a state is added to the enum without being placed in the governing + loop's partition, this fails — keeping the "honestly scoped, not + half-dead" contract enforced rather than asserted. + """ + assert len(ACTIVE_STATES) == 9 + assert len(RESERVED_STATES) == 5 + assert len(RECONCILE_STATES) == 1 + + union = ACTIVE_STATES | RESERVED_STATES | RECONCILE_STATES + assert union == set(EpistemicState) + + # Pairwise disjoint — no state wears two roles. + assert not (ACTIVE_STATES & RESERVED_STATES) + assert not (ACTIVE_STATES & RECONCILE_STATES) + assert not (RESERVED_STATES & RECONCILE_STATES) + + +def test_evidenced_is_the_reconcile_state(): + """EVIDENCED is the orphaned drift (recognition owns its own copy).""" + assert RECONCILE_STATES == frozenset({EpistemicState.EVIDENCED})