diff --git a/chat/runtime.py b/chat/runtime.py index f3a58e81..06cdc772 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -35,6 +35,7 @@ from chat.refusal import ( from core.epistemic_state import ( clearance_from_verdicts, epistemic_state_for_grounding_source, + normative_detail_from_verdicts, ) from chat.telemetry import ( TurnEventSink, @@ -362,6 +363,9 @@ class ChatResponse: # treated conservatively. epistemic_state: str = "undetermined" normative_clearance: str = "unassessable" + # Comma-separated violated boundary/commitment IDs when normative + # clearance is VIOLATED or SUPPRESSED; empty string otherwise. + normative_detail: str = "" # 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 @@ -1401,6 +1405,7 @@ class ChatRuntime: ) stub_epistemic_state = epistemic_state_for_grounding_source(grounding_source).value stub_normative_clearance = clearance_from_verdicts(verdicts_bundle).value + stub_normative_detail = normative_detail_from_verdicts(verdicts_bundle) if tokens: stub_event = TurnEvent( turn=max(self._context.turn - 1, 0), @@ -1432,6 +1437,7 @@ class ChatRuntime: composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count, epistemic_state=stub_epistemic_state, normative_clearance=stub_normative_clearance, + normative_detail=stub_normative_detail, ) self.turn_log.append(stub_event) self._emit_turn_event(stub_event) @@ -1489,6 +1495,7 @@ class ChatRuntime: composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count, epistemic_state=stub_epistemic_state, normative_clearance=stub_normative_clearance, + normative_detail=stub_normative_detail, refusal_reason=refusal_surface if refusal_emitted else "", ) @@ -1910,6 +1917,7 @@ class ChatRuntime: warm_grounding_source or "vault" ).value main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value + main_normative_detail = normative_detail_from_verdicts(verdicts_bundle) turn_event = TurnEvent( turn=self._context.turn - 1, input_tokens=tuple(filtered), @@ -1940,6 +1948,7 @@ class ChatRuntime: composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count, epistemic_state=main_epistemic_state, normative_clearance=main_normative_clearance, + normative_detail=main_normative_detail, ) self.turn_log.append(turn_event) self._emit_turn_event(turn_event) @@ -1987,6 +1996,7 @@ class ChatRuntime: recalled_words=walk_tokens, epistemic_state=main_epistemic_state, normative_clearance=main_normative_clearance, + normative_detail=main_normative_detail, refusal_reason=refusal_surface if refusal_emitted else "", ) diff --git a/core/epistemic_state.py b/core/epistemic_state.py index 896cb1d7..f40b9566 100644 --- a/core/epistemic_state.py +++ b/core/epistemic_state.py @@ -94,6 +94,32 @@ def clearance_from_verdicts(verdicts: Any = None, *, safety_verdict: Any = None, return NormativeClearance.CLEARED +def normative_detail_from_verdicts(verdicts: Any = None, *, safety_verdict: Any = None, ethics_verdict: Any = None) -> str: + """Return a sorted, comma-separated string of violated boundary/commitment IDs. + + Non-empty only when normative clearance is VIOLATED or SUPPRESSED — + i.e. when at least one runtime-checkable boundary or commitment failed. + Returns "" when CLEARED or UNASSESSABLE so callers can test truthiness. + Uses the same duck-typing pattern as ``clearance_from_verdicts``. + """ + if verdicts is not None: + safety_verdict = safety_verdict if safety_verdict is not None else getattr(verdicts, "safety_verdict", None) + ethics_verdict = ethics_verdict if ethics_verdict is not None else getattr(verdicts, "ethics_verdict", None) + + ids: list[str] = [] + for r in getattr(safety_verdict, "results", []): + if getattr(r, "runtime_checkable", False) and not getattr(r, "upheld", True): + bid = getattr(r, "boundary_id", None) + if bid: + ids.append(str(bid)) + for r in getattr(ethics_verdict, "results", []): + if getattr(r, "runtime_checkable", False) and not getattr(r, "upheld", True): + cid = getattr(r, "commitment_id", None) + if cid: + ids.append(str(cid)) + return ",".join(sorted(ids)) + + def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState: """Default runtime mapping for existing grounding-source labels.""" normalized = (source or "none").strip().lower() @@ -115,4 +141,5 @@ __all__ = [ "coerce_epistemic_state", "coerce_normative_clearance", "epistemic_state_for_grounding_source", + "normative_detail_from_verdicts", ] diff --git a/docs/decisions/ADR-0142-epistemic-state-taxonomy.md b/docs/decisions/ADR-0142-epistemic-state-taxonomy.md new file mode 100644 index 00000000..9964835e --- /dev/null +++ b/docs/decisions/ADR-0142-epistemic-state-taxonomy.md @@ -0,0 +1,165 @@ +# ADR-0142: Epistemic State Taxonomy — First-Class Vocabulary + +**Status:** Accepted (integration deferred pending ADR-0144) +**Date:** 2026-05-24 +**Supersedes:** none +**Related:** [epistemic-state-taxonomy-scope](./epistemic-state-taxonomy-scope.md), ADR-0021 (teaching safety), ADR-0024 (refusal materialisation), ADR-0144 (PropositionGraph integration — gate for full cross-subsystem wiring) + +--- + +## Context + +CORE's thesis commits the engine to *decoding* a reality that already is, not +generating plausible continuations. Decoding requires the engine to hold +propositions in varying degrees of grounding — not "true" vs "false" but a +richer vocabulary describing *how* a proposition is currently known and *what +evidence* supports that knowing. + +Without an explicit vocabulary, the engine implicitly caps its epistemic scope +at binary admit/refuse. Six subsystem audits (math, vault, language packs, +runtime packs, teaching pipeline, cognition pipeline, and chat runtime — 136 +total decision points) confirm that the engine already makes implicit epistemic +distinctions across all of these subsystems: it distinguishes exact recall +from decomposed recall, curated pack data from dynamically composed units, +reviewed teaching chains from speculative candidates, and refusals caused by +evidence absence from refusals caused by safety/ethics violations. The audits +show these distinctions are consistent enough to unify under a single taxonomy. + +## Decision + +Ratify the following 14-state epistemic vocabulary as the engine's first-class +epistemic axis. Every proposition the engine reasons about carries exactly one +of these states, plus structured provenance (see Provenance Requirements below). + +| State | Meaning | Primary source | +|---|---|---| +| **PERCEIVED** | Token/span observed in input; not yet committed to meaning | Raw ingestion | +| **EVIDENCED** | Feature lifts from specific input spans bind a proposition | Recognition layer | +| **EVIDENCED-INCOMPLETE** | Feature lift succeeded for a sub-span but the proposition is structurally partial — lift did not fail, but no consuming proposition exists yet | Recognition layer — partial structural match | +| **VERIFIED** | Cross-checked against ratified knowledge (pack / vault / teaching); consistent | Substrate cross-reference | +| **DECODED** | VERIFIED plus replay-equality from input (trace-hash invariant) | Replay machinery | +| **DECODED-UNARTICULATED** | Proposition is DECODED internally but surface realization path broke; the answer is correct but cannot be communicated. Must not be classified as `wrong` | Verifier pass + Realizer failure | +| **INFERRED** | Derived from DECODED components by a ratified deterministic rule; composite not itself curated | Rule application over DECODED primitives | +| **UNVERIFIED-POSSIBLE** | Consistent with verified knowledge but not directly verified; usable provisionally | Default for non-contradicting novel propositions | +| **UNVERIFIED-NOVEL** | Not contradicted; introduces structure the engine has not decoded yet; candidate for teaching expansion | OOV refusal pointing at expansion need | +| **CONTRADICTED** | Conflicts with verified knowledge; refuse unless this is a ratified correction | Verification failure | +| **AMBIGUOUS** | Input could support multiple incompatible propositions; engine cannot choose without more context | Multi-evidence-binding conflict at recognition | +| **UNDETERMINED** | Feature lifts could not complete; specific dimensions missing | Recognition-layer refusal | +| **SCOPE_BOUNDARY** | Proposition type recognized but outside current capability envelope. Distinct from UNDETERMINED (lift succeeded) and CONTRADICTED (no conflict — engine cannot decode yet) | Capability-envelope check | +| **COMPUTATIONALLY_BOUNDED** | Engine cannot determine epistemic status within resource envelope; not AMBIGUOUS, not UNDETERMINED | Search/enumeration resource-limit hit | + +Plus one meta-state: + +| **EPISTEMIC_STATE_NEEDED** | None of the existing states fit; engine refuses and surfaces the gap for teaching expansion | Recursive refusal | + +The meta-state is what makes the vocabulary non-capping. When no existing state +fits, the engine refuses with a structured description of the gap; the teaching +loop either ratifies a new state or determines an existing one covers the case. + +## Companion axis: Normative clearance (orthogonal) + +Safety and ethics verdicts are **not epistemic states**. They answer a +different question: *has this turn's behavior complied with the active +constraints?* The two axes are orthogonal — a VERIFIED proposition can violate +a safety boundary; an UNDETERMINED proposition can pass every ethics predicate. + +Every proposition reaching `ChatResponse` or `TurnEvent` carries both axes: + +| Clearance state | Meaning | +|---|---| +| **CLEARED** | All active normative constraints (safety + ethics) passed | +| **VIOLATED** | At least one constraint breached; audit record written | +| **UNASSESSABLE** | Constraint exists but cannot be evaluated at runtime | +| **SUPPRESSED** | A refusal commitment fired; proposition replaced with typed refusal before reaching the surface | + +`normative_detail` carries the violated boundary/commitment IDs when clearance +is VIOLATED or SUPPRESSED; empty string otherwise. + +## Provenance requirements + +Every assignment of a state to a proposition must carry structured provenance: + +- **Source:** which subsystem assigned this state +- **Evidence span(s):** which input or knowledge spans supported the assignment +- **Transition history:** if the proposition was previously in another state, what evidence caused the transition + +Provenance is what distinguishes thesis-aligned epistemic tracking from +confidence scores. A confidence score is a number; provenance is a trace the +engine can replay, audit, and correct. + +*Full provenance enforcement is deferred to ADR-0144 integration. Phase 3 +(this ADR) establishes the vocabulary and wires state labels onto runtime +artifacts. Phase 4 (post ADR-0144) adds structured provenance records.* + +## What Phase 3 delivers (already landed) + +The following is implemented and merged: + +- `core/epistemic_state.py` — `EpistemicState` and `NormativeClearance` enums, + `clearance_from_verdicts()`, `epistemic_state_for_grounding_source()`, + `normative_detail_from_verdicts()`, `coerce_*` helpers. +- `core/physics/identity.py` — `TurnEvent` carries `epistemic_state`, + `normative_clearance`, `normative_detail`. +- `chat/runtime.py` — `ChatResponse` carries all three fields; both stub and + main paths populate them from verdicts and grounding source. +- `chat/telemetry.py` — serializes state axes into JSONL turn events. +- `language_packs/loader.py` — `UnitEntry` carries `epistemic_state`; curated + entries tagged DECODED, composition-rule entries tagged INFERRED. +- `vocab/manifold.py` — `add()` accepts `epistemic_state`; `add_transient()` + tags words UNVERIFIED_NOVEL; `epistemic_state_for_word()` exposed. +- `language_packs/compiler.py` — passes `epistemic_state` through compile, + clone, and cached-load paths. +- `vault/store.py` — `epistemic_state_for_vault_status()` mapping; + `epistemic_state` stamped into metadata on `store()`; recall results expose + the field. +- Phase 2 bug fixes (PR #219): FALSIFIED/SPECULATIVE explicitly distinguished + in `_status_admits`; `mean_pair_score([])` returns NaN; `RealizerError` on + verified trace routes to `decoded_unarticulated` outcome; domain contract + `present=False` is `valid=True`; `domain_id:unknown` routes to + `scope_boundary`. + +## What remains gated on ADR-0144 + +- **Cross-subsystem transition machinery.** How a proposition carries its state + and provenance as it moves between subsystems (recognition → verifier → vault) + requires the `PropositionGraph` as the carrier. ADR-0144 defines that graph. +- **Structured provenance records.** Per-assignment provenance objects (source, + span, transition history) add overhead that needs the PropositionGraph as a + home before they can be allocated. +- **`CognitiveTurnResult.refusal_reason` materialisation.** The field is + populated by ChatRuntime (PR #222) but the cognition pipeline does not yet + read it back for trace folding. Full wiring is post-ADR-0144. +- **State storage layer.** Where states are persisted per-session vs. + per-proposition vs. in the vault is an open question (see scope doc Q2). + +## Implementation debts to resolve before full cross-subsystem integration + +From the six-subsystem audit: + +1. **Cognition pipeline cold-start PASSTHROUGH** (`pipeline.py:_ratify_intent`) + collapses three distinct conditions (no field_state, no vocab, no + prompt_versor) into one indistinguishable `PASSTHROUGH` outcome. Extend + `RatificationOutcome` to distinguish them. + +2. **Chat runtime grounding-source dispatcher** (`runtime.py:831–1012`) does + not record which sources were attempted or why each fell through. Six + provenance gaps cluster around this site. Add an explicit dispatch trace + structure once the PropositionGraph carrier exists. + +3. **Teaching pipeline watched-metrics tuple** (`replay.py`) should be a named, + versioned `MetricSet` dataclass to survive future metric additions without + breaking trace byte-identity. + +## Consequences + +- The vocabulary is non-negotiable going forward. New subsystem work must assign + one of these states (or emit EPISTEMIC_STATE_NEEDED) to every proposition it + handles. +- The normative clearance axis is orthogonal. Safety/ethics machinery must not + modify epistemic state and must not be expressed as an epistemic state. +- DECODED is the strongest positive state the engine can assign. VERIFIED is one + step below it (cross-checked but not yet replay-equal). Nothing stronger than + DECODED is defined; the engine does not claim certainty beyond replay equality. +- EPISTEMIC_STATE_NEEDED is the escape hatch that keeps the taxonomy from + capping the engine's scope. Every EPISTEMIC_STATE_NEEDED emission is a + teaching opportunity.