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:
parent
9ef609c460
commit
35c8a1c56b
3 changed files with 202 additions and 0 deletions
|
|
@ -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 "",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
165
docs/decisions/ADR-0142-epistemic-state-taxonomy.md
Normal file
165
docs/decisions/ADR-0142-epistemic-state-taxonomy.md
Normal 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: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.
|
||||
Loading…
Reference in a new issue