feat(epistemic): populate normative_detail on TurnEvent and ChatResponse (#223)

* feat(epistemic): populate normative_detail on TurnEvent and ChatResponse

Adds normative_detail_from_verdicts() to core.epistemic_state and wires
it into both the stub and main ChatResponse/TurnEvent construction sites.
The field carries a sorted comma-separated list of violated boundary or
commitment IDs when normative clearance is VIOLATED or SUPPRESSED; empty
string otherwise.

* docs(ADR-0142): ratify epistemic state taxonomy — 14-state vocabulary + normative clearance axis

Formalises the six-subsystem Framing 1 audit findings into a first-class
decision. Accepts the 14-state taxonomy and companion 4-value normative
clearance axis. Documents Phase 3 deliverables already landed and defers
structured provenance + cross-subsystem transition machinery to ADR-0144.
This commit is contained in:
Shay 2026-05-24 11:56:34 -07:00 committed by GitHub
parent 9ef609c460
commit 35c8a1c56b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 202 additions and 0 deletions

View file

@ -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 "",
)

View file

@ -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",
]

View file

@ -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:8311012`) 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.