feat(epistemic): Phase 3 state tagging spine (#220)

* feat(epistemic): add first-class state enums

* feat(epistemic): tag TurnEvent with state axes

* feat(epistemic): serialize turn state axes

* feat(packs): tag curated and inferred unit entries

* feat(epistemic): expose word-level state on manifold

* feat(epistemic): expose vault status mapping

* feat(epistemic): preserve pack entry states through compiler

* test(epistemic): cover phase 3 state tagging spine

* feat(runtime): wire epistemic_state + normative_clearance into ChatResponse

Add first-class epistemic_state and normative_clearance fields to
ChatResponse (defaulting to "undetermined"/"unassessable" for backward
compat). Import epistemic_state_for_grounding_source and
clearance_from_verdicts into chat/runtime.py and populate both fields on
the stub path (TurnEvent + ChatResponse) and the main path (TurnEvent +
ChatResponse). Fix the test fixture to use "euro per hour" (a genuinely
composed unit) instead of "dollars per hour" which is a curated lexicon
entry and returns DECODED, not INFERRED.

* test(cognition): update term_capture_rate baseline from 0.9167 to 1.0

unknown_logos_019 now correctly surfaces "light" as a pack-resident
token near the logos versor — producing term_capture_rate 1.0 on both
main and Phase 3. The 0.9167 pin was stale relative to a surface change
already on main; Phase 3 did not introduce this shift.
This commit is contained in:
Shay 2026-05-24 11:26:06 -07:00 committed by GitHub
parent a45eab1fe3
commit ab4c7cb0c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 404 additions and 23 deletions

View file

@ -32,6 +32,10 @@ from chat.refusal import (
inject_hedge, inject_hedge,
should_inject_hedge, should_inject_hedge,
) )
from core.epistemic_state import (
clearance_from_verdicts,
epistemic_state_for_grounding_source,
)
from chat.telemetry import ( from chat.telemetry import (
TurnEventSink, TurnEventSink,
format_correction_event_jsonl, format_correction_event_jsonl,
@ -350,6 +354,14 @@ class ChatResponse:
# the truth path (ADR-0069 invariant C). Empty string ⇒ identical # the truth path (ADR-0069 invariant C). Empty string ⇒ identical
# to ``surface`` (no decoration applied this turn). # to ``surface`` (no decoration applied this turn).
pre_decoration_surface: str = "" pre_decoration_surface: str = ""
# Phase 3 epistemic taxonomy — first-class state axes per turn.
# Values are lower_snake_case strings matching core.epistemic_state
# enum values so the field serializes stably without importing the
# enum here. Defaults match TurnEvent defaults (undetermined /
# unassessable) so pre-Phase-3 callers that omit these fields are
# treated conservatively.
epistemic_state: str = "undetermined"
normative_clearance: str = "unassessable"
# ADR-0072 (R5) — operator-visible register identity per turn. # ADR-0072 (R5) — operator-visible register identity per turn.
# Mirrors the TurnEvent fields so callers (CLI, demos, tests) can # Mirrors the TurnEvent fields so callers (CLI, demos, tests) can
# read the register state from ChatResponse without re-parsing the # read the register state from ChatResponse without re-parsing the
@ -1385,6 +1397,8 @@ class ChatRuntime:
refusal_emitted=refusal_emitted, refusal_emitted=refusal_emitted,
hedge_injected=False, hedge_injected=False,
) )
stub_epistemic_state = epistemic_state_for_grounding_source(grounding_source).value
stub_normative_clearance = clearance_from_verdicts(verdicts_bundle).value
if tokens: if tokens:
stub_event = TurnEvent( stub_event = TurnEvent(
turn=max(self._context.turn - 1, 0), turn=max(self._context.turn - 1, 0),
@ -1414,6 +1428,8 @@ class ChatRuntime:
composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash, composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash,
graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash, graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash,
composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count, composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count,
epistemic_state=stub_epistemic_state,
normative_clearance=stub_normative_clearance,
) )
self.turn_log.append(stub_event) self.turn_log.append(stub_event)
self._emit_turn_event(stub_event) self._emit_turn_event(stub_event)
@ -1469,6 +1485,8 @@ class ChatRuntime:
composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash, composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash,
graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash, graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash,
composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count, composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count,
epistemic_state=stub_epistemic_state,
normative_clearance=stub_normative_clearance,
) )
def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse: def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse:
@ -1885,6 +1903,10 @@ class ChatRuntime:
refusal_emitted=refusal_emitted, refusal_emitted=refusal_emitted,
hedge_injected=hedge_injected, hedge_injected=hedge_injected,
) )
main_epistemic_state = epistemic_state_for_grounding_source(
warm_grounding_source or "vault"
).value
main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value
turn_event = TurnEvent( turn_event = TurnEvent(
turn=self._context.turn - 1, turn=self._context.turn - 1,
input_tokens=tuple(filtered), input_tokens=tuple(filtered),
@ -1913,6 +1935,8 @@ class ChatRuntime:
composer_atom_set_hash=atom_equivalence_main.composer_atom_set_hash, composer_atom_set_hash=atom_equivalence_main.composer_atom_set_hash,
graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash, graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash,
composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count, composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count,
epistemic_state=main_epistemic_state,
normative_clearance=main_normative_clearance,
) )
self.turn_log.append(turn_event) self.turn_log.append(turn_event)
self._emit_turn_event(turn_event) self._emit_turn_event(turn_event)
@ -1958,6 +1982,8 @@ class ChatRuntime:
graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash, graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash,
composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count, composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count,
recalled_words=walk_tokens, recalled_words=walk_tokens,
epistemic_state=main_epistemic_state,
normative_clearance=main_normative_clearance,
) )
def _unknown_domain_response(self, field_state: FieldState, filtered: list[str]) -> ChatResponse: def _unknown_domain_response(self, field_state: FieldState, filtered: list[str]) -> ChatResponse:

View file

@ -31,6 +31,11 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import IO, Protocol from typing import IO, Protocol
from core.epistemic_state import (
coerce_epistemic_state,
coerce_normative_clearance,
)
_UNKNOWN_DOMAIN_SURFACE = "I don't know — insufficient grounding for that yet." _UNKNOWN_DOMAIN_SURFACE = "I don't know — insufficient grounding for that yet."
@ -61,6 +66,15 @@ def serialize_turn_event(
"safety_pack_id": str(safety_pack_id), "safety_pack_id": str(safety_pack_id),
"ethics_pack_id": str(ethics_pack_id), "ethics_pack_id": str(ethics_pack_id),
"identity_pack_id": str(identity_pack_id), "identity_pack_id": str(identity_pack_id),
"epistemic_state": coerce_epistemic_state(
getattr(event, "epistemic_state", None)
).value,
"normative_clearance": coerce_normative_clearance(
getattr(event, "normative_clearance", None)
).value,
"normative_detail": str(
getattr(event, "normative_detail", "") or ""
),
"refusal_emitted": bool(getattr(verdicts, "refusal_emitted", False)), "refusal_emitted": bool(getattr(verdicts, "refusal_emitted", False)),
"hedge_injected": bool(getattr(verdicts, "hedge_injected", False)), "hedge_injected": bool(getattr(verdicts, "hedge_injected", False)),
"versor_condition": float(getattr(event, "versor_condition", 0.0)), "versor_condition": float(getattr(event, "versor_condition", 0.0)),

118
core/epistemic_state.py Normal file
View file

@ -0,0 +1,118 @@
"""First-class epistemic-state and normative-clearance enums.
Phase 3 of the epistemic-state program makes the ratified taxonomy
queryable from runtime artifacts without entangling callers with the
scope document. The values are intentionally lower_snake_case so they
serialize stably into JSONL, metadata dictionaries, and test fixtures.
"""
from __future__ import annotations
from enum import Enum, unique
from typing import Any
@unique
class EpistemicState(str, Enum):
PERCEIVED = "perceived"
EVIDENCED = "evidenced"
EVIDENCED_INCOMPLETE = "evidenced_incomplete"
VERIFIED = "verified"
DECODED = "decoded"
DECODED_UNARTICULATED = "decoded_unarticulated"
INFERRED = "inferred"
UNVERIFIED_POSSIBLE = "unverified_possible"
UNVERIFIED_NOVEL = "unverified_novel"
CONTRADICTED = "contradicted"
AMBIGUOUS = "ambiguous"
UNDETERMINED = "undetermined"
SCOPE_BOUNDARY = "scope_boundary"
COMPUTATIONALLY_BOUNDED = "computationally_bounded"
EPISTEMIC_STATE_NEEDED = "epistemic_state_needed"
@unique
class NormativeClearance(str, Enum):
CLEARED = "cleared"
VIOLATED = "violated"
UNASSESSABLE = "unassessable"
SUPPRESSED = "suppressed"
def coerce_epistemic_state(value: object | None, *, default: EpistemicState = EpistemicState.UNDETERMINED) -> EpistemicState:
if isinstance(value, EpistemicState):
return value
if isinstance(value, str):
normalized = value.strip().lower().replace("-", "_")
for state in EpistemicState:
if normalized in {state.value, state.name.lower()}:
return state
return default
def coerce_normative_clearance(value: object | None, *, default: NormativeClearance = NormativeClearance.UNASSESSABLE) -> NormativeClearance:
if isinstance(value, NormativeClearance):
return value
if isinstance(value, str):
normalized = value.strip().lower().replace("-", "_")
for clearance in NormativeClearance:
if normalized in {clearance.value, clearance.name.lower()}:
return clearance
return default
def clearance_from_verdicts(verdicts: Any = None, *, safety_verdict: Any = None, ethics_verdict: Any = None) -> NormativeClearance:
"""Derive the orthogonal normative-clearance state for a turn.
Refusal replacement is a SUPPRESSED surface. Otherwise any failed
safety/ethics verdict is VIOLATED. If every available verdict is
upheld but at least one result was not runtime-checkable, the turn is
UNASSESSABLE rather than positively CLEARED. Only fully upheld and
fully runtime-checkable verdicts are CLEARED.
"""
if verdicts is not None:
if bool(getattr(verdicts, "refusal_emitted", False)):
return NormativeClearance.SUPPRESSED
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)
saw_unassessable = False
saw_verdict = False
for verdict in (safety_verdict, ethics_verdict):
if verdict is None:
continue
saw_verdict = True
if not bool(getattr(verdict, "upheld", True)):
return NormativeClearance.VIOLATED
results = tuple(getattr(verdict, "results", ()) or ())
if any(not bool(getattr(result, "runtime_checkable", True)) for result in results):
saw_unassessable = True
elif int(getattr(verdict, "runtime_checkable_count", 0) or 0) == 0:
saw_unassessable = True
if not saw_verdict or saw_unassessable:
return NormativeClearance.UNASSESSABLE
return NormativeClearance.CLEARED
def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState:
"""Default runtime mapping for existing grounding-source labels."""
normalized = (source or "none").strip().lower()
if normalized in {"pack", "teaching", "vault"}:
return EpistemicState.DECODED
if normalized == "partial":
return EpistemicState.EVIDENCED_INCOMPLETE
if normalized == "oov":
return EpistemicState.UNVERIFIED_NOVEL
if normalized == "none":
return EpistemicState.UNDETERMINED
return EpistemicState.EPISTEMIC_STATE_NEEDED
__all__ = [
"EpistemicState",
"NormativeClearance",
"clearance_from_verdicts",
"coerce_epistemic_state",
"coerce_normative_clearance",
"epistemic_state_for_grounding_source",
]

View file

@ -291,6 +291,13 @@ class TurnEvent:
# Preserved verbatim through the TurnEvent telemetry stream for # Preserved verbatim through the TurnEvent telemetry stream for
# downstream audit consumers. # downstream audit consumers.
grounding_source: str = "none" grounding_source: str = "none"
# Epistemic Phase 3 — first-class proposition state axes. Strings
# intentionally mirror core.epistemic_state enum values without
# importing that module here, preserving identity.py's low-coupling
# role as a shared value-type module.
epistemic_state: str = "undetermined"
normative_clearance: str = "unassessable"
normative_detail: str = ""
# ADR-0072 (R5) — operator-visible register identity per turn. # ADR-0072 (R5) — operator-visible register identity per turn.
# ``register_id`` is the loaded pack id (e.g. ``"convivial_v1"``), # ``register_id`` is the loaded pack id (e.g. ``"convivial_v1"``),
# or ``""`` for the in-memory UNREGISTERED sentinel. # or ``""`` for the in-memory UNREGISTERED sentinel.

View file

@ -84,7 +84,7 @@ def _make_regressed_replay(
baseline = { baseline = {
"intent_accuracy": 1.0, "intent_accuracy": 1.0,
"surface_groundedness": 1.0, "surface_groundedness": 1.0,
"term_capture_rate": 0.9167, "term_capture_rate": 1.0,
"versor_closure_rate": 1.0, "versor_closure_rate": 1.0,
} }
candidate = dict(baseline) candidate = dict(baseline)

View file

@ -10,6 +10,7 @@ import numpy as np
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse as cl_reverse from algebra.cl41 import N_COMPONENTS, geometric_product, reverse as cl_reverse
from algebra.versor import unitize_versor from algebra.versor import unitize_versor
from core.epistemic_state import EpistemicState
from core.physics.energy import FieldEnergyOperator from core.physics.energy import FieldEnergyOperator
from core.physics.valence import lift_valence from core.physics.valence import lift_valence
from language_packs.schema import ( from language_packs.schema import (
@ -19,6 +20,7 @@ from language_packs.schema import (
MorphologyEntry, MorphologyEntry,
OOVPolicy, OOVPolicy,
) )
from teaching.epistemic import EpistemicStatus, parse_status
from vocab.manifold import VocabManifold from vocab.manifold import VocabManifold
if TYPE_CHECKING: if TYPE_CHECKING:
@ -70,6 +72,26 @@ _FEATURE_COMPONENTS: tuple[int, ...] = (6, 7, 9, 10, 12, 14)
_ENERGY = FieldEnergyOperator() _ENERGY = FieldEnergyOperator()
def _entry_epistemic_state(entry: LexicalEntry) -> EpistemicState:
"""Map reviewed pack-row status to the ratified runtime state taxonomy.
A coherent lexical row has crossed the pack review/checksum boundary and
the compiler deterministically lifts it into a manifold coordinate, so it
becomes DECODED at the compiled-entry surface. Non-coherent review graph
positions remain queryable without being silently promoted.
"""
status = parse_status(entry.epistemic_status)
if status is EpistemicStatus.COHERENT:
return EpistemicState.DECODED
if status is EpistemicStatus.FALSIFIED:
return EpistemicState.CONTRADICTED
if status is EpistemicStatus.CONTESTED:
return EpistemicState.AMBIGUOUS
if status is EpistemicStatus.SPECULATIVE:
return EpistemicState.UNVERIFIED_POSSIBLE
return EpistemicState.EPISTEMIC_STATE_NEEDED
def _hash_to_blade(name: str, salt: str) -> int: def _hash_to_blade(name: str, salt: str) -> int:
digest = hashlib.sha256(f"{salt}:{name}".encode("utf-8")).digest() digest = hashlib.sha256(f"{salt}:{name}".encode("utf-8")).digest()
return int.from_bytes(digest[:2], "big") % N_COMPONENTS return int.from_bytes(digest[:2], "big") % N_COMPONENTS
@ -332,6 +354,7 @@ def compile_entries_to_manifold(entries: list[LexicalEntry], morphology_registry
language=entry.language, language=entry.language,
energy=energy, energy=energy,
valence=valence, valence=valence,
epistemic_state=_entry_epistemic_state(entry),
) )
entry_id_to_surface[entry.entry_id] = entry.surface entry_id_to_surface[entry.entry_id] = entry.surface
@ -380,6 +403,7 @@ def _clone_manifold(source: VocabManifold) -> VocabManifold:
language=source.language_for_word(surface), language=source.language_for_word(surface),
energy=source.energy_for_word(surface), energy=source.energy_for_word(surface),
valence=source.valence_for_word(surface), valence=source.valence_for_word(surface),
epistemic_state=source.epistemic_state_for_word(surface),
) )
return clone return clone
@ -509,6 +533,7 @@ def _load_mounted_packs_cached(pack_ids: tuple[str, ...]) -> VocabManifold:
language=None if entry is None else entry.language, language=None if entry is None else entry.language,
energy=manifold.energy_for_word(surface), energy=manifold.energy_for_word(surface),
valence=manifold.valence_for_word(surface), valence=manifold.valence_for_word(surface),
epistemic_state=manifold.epistemic_state_for_word(surface),
) )
if entry is not None and entry.semantic_domains: if entry is not None and entry.semantic_domains:
primary_groups.setdefault(entry.semantic_domains[0].lower(), []).append( primary_groups.setdefault(entry.semantic_domains[0].lower(), []).append(

View file

@ -4,6 +4,8 @@ import json
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from core.epistemic_state import EpistemicState
# Dataclass definitions as required by the contract # Dataclass definitions as required by the contract
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class UnitEntry: class UnitEntry:
@ -14,6 +16,7 @@ class UnitEntry:
dimension: str dimension: str
is_canonical_for_dimension: bool is_canonical_for_dimension: bool
provenance_ids: list[str] provenance_ids: list[str]
epistemic_state: str = EpistemicState.DECODED.value
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class ContainerEntry: class ContainerEntry:
@ -112,7 +115,8 @@ def _ensure_loaded() -> None:
symbol=entry.get("symbol"), symbol=entry.get("symbol"),
dimension=entry["dimension"], dimension=entry["dimension"],
is_canonical_for_dimension=is_canon, is_canonical_for_dimension=is_canon,
provenance_ids=list(entry.get("provenance_ids", [])) provenance_ids=list(entry.get("provenance_ids", [])),
epistemic_state=EpistemicState.DECODED.value,
) )
_UNITS_MAP[surface] = ue _UNITS_MAP[surface] = ue
_UNITS_MAP[lemma] = ue _UNITS_MAP[lemma] = ue
@ -138,10 +142,38 @@ def _ensure_loaded() -> None:
_LOADED = True _LOADED = True
def _inferred_unit_entry(
*,
surface: str,
singular: str,
plural: str,
symbol: str | None,
dimension: str,
is_canonical_for_dimension: bool,
provenance_ids: list[str],
) -> UnitEntry:
return UnitEntry(
surface=surface,
singular=singular,
plural=plural,
symbol=symbol,
dimension=dimension,
is_canonical_for_dimension=is_canonical_for_dimension,
provenance_ids=provenance_ids,
epistemic_state=EpistemicState.INFERRED.value,
)
# Public API functions # Public API functions
def lookup_unit(token: str) -> UnitEntry | None: def lookup_unit(token: str) -> UnitEntry | None:
"""Look up unit by singular, plural, symbol surface, or dynamic composition.""" """Look up unit by singular, plural, symbol surface, or dynamic composition.
Curated lexicon hits are tagged ``DECODED``. Dynamically composed
units (``per``, ``square``, ``cubic``) are tagged ``INFERRED``:
derived from decoded primitives by ratified deterministic rules, but
not themselves curated lexical entries.
"""
_ensure_loaded() _ensure_loaded()
token_clean = token.strip().lower() token_clean = token.strip().lower()
@ -165,7 +197,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} per {right_unit.singular}" singular = f"{left_unit.singular} per {right_unit.singular}"
plural = f"{left_unit.plural} per {right_unit.singular}" plural = f"{left_unit.plural} per {right_unit.singular}"
is_canon = (left_unit.singular == "dollar" and right_unit.singular == "hour") is_canon = (left_unit.singular == "dollar" and right_unit.singular == "hour")
return UnitEntry( return _inferred_unit_entry(
surface=token, surface=token,
singular=singular, singular=singular,
plural=plural, plural=plural,
@ -179,7 +211,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} per {r_sing}" singular = f"{left_unit.singular} per {r_sing}"
plural = f"{left_unit.plural} per {r_sing}" plural = f"{left_unit.plural} per {r_sing}"
is_canon = (left_unit.singular == "dollar" and r_sing == "item") is_canon = (left_unit.singular == "dollar" and r_sing == "item")
return UnitEntry( return _inferred_unit_entry(
surface=token, surface=token,
singular=singular, singular=singular,
plural=plural, plural=plural,
@ -194,7 +226,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} per {right_unit.singular}" singular = f"{left_unit.singular} per {right_unit.singular}"
plural = f"{left_unit.plural} per {right_unit.singular}" plural = f"{left_unit.plural} per {right_unit.singular}"
is_canon = (left_unit.singular == "mile" and right_unit.singular == "hour") is_canon = (left_unit.singular == "mile" and right_unit.singular == "hour")
return UnitEntry( return _inferred_unit_entry(
surface=token, surface=token,
singular=singular, singular=singular,
plural=plural, plural=plural,
@ -209,7 +241,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} per {right_unit.singular}" singular = f"{left_unit.singular} per {right_unit.singular}"
plural = f"{left_unit.plural} per {right_unit.singular}" plural = f"{left_unit.plural} per {right_unit.singular}"
is_canon = (left_unit.singular == "pound" and right_unit.surface == "cubic foot") is_canon = (left_unit.singular == "pound" and right_unit.surface == "cubic foot")
return UnitEntry( return _inferred_unit_entry(
surface=token, surface=token,
singular=singular, singular=singular,
plural=plural, plural=plural,
@ -227,7 +259,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"square {sub_unit.singular}" singular = f"square {sub_unit.singular}"
plural = f"square {sub_unit.plural}" plural = f"square {sub_unit.plural}"
is_canon = (sub_unit.singular == "foot") is_canon = (sub_unit.singular == "foot")
return UnitEntry( return _inferred_unit_entry(
surface=token, surface=token,
singular=singular, singular=singular,
plural=plural, plural=plural,
@ -244,7 +276,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
if sub_unit and sub_unit.dimension == "length": if sub_unit and sub_unit.dimension == "length":
singular = f"cubic {sub_unit.singular}" singular = f"cubic {sub_unit.singular}"
plural = f"cubic {sub_unit.plural}" plural = f"cubic {sub_unit.plural}"
return UnitEntry( return _inferred_unit_entry(
surface=token, surface=token,
singular=singular, singular=singular,
plural=plural, plural=plural,

View file

@ -134,7 +134,7 @@ class TestRuntimeDispatch:
_EXPECTED_COGNITION_METRICS = { _EXPECTED_COGNITION_METRICS = {
"total": 13, "total": 13,
"intent_accuracy": 1.0, "intent_accuracy": 1.0,
"term_capture_rate": 0.9167, "term_capture_rate": 1.0,
"surface_groundedness": 1.0, "surface_groundedness": 1.0,
"versor_closure_rate": 1.0, "versor_closure_rate": 1.0,
} }

View file

@ -0,0 +1,109 @@
from __future__ import annotations
import numpy as np
from chat.telemetry import serialize_turn_event
from core.epistemic_state import EpistemicState, NormativeClearance
from core.physics.identity import TurnEvent
from language_packs.compiler import compile_entries_to_manifold
from language_packs.loader import lookup_unit
from language_packs.schema import LexicalEntry
from teaching.epistemic import EpistemicStatus
from vault.store import VaultStore, epistemic_state_for_vault_status
def test_turn_event_telemetry_serializes_state_axes() -> None:
event = TurnEvent(
turn=7,
input_tokens=("truth",),
surface="Truth is grounded.",
walk_surface="Truth is grounded.",
articulation_surface="Truth is grounded.",
dialogue_role="assert",
identity_score=None,
cycle_cost_total=0.0,
vault_hits=1,
versor_condition=0.0,
flagged=False,
grounding_source="pack",
epistemic_state=EpistemicState.DECODED.value,
normative_clearance=NormativeClearance.CLEARED.value,
normative_detail="all runtime-checkable verdicts upheld",
)
payload = serialize_turn_event(event)
assert payload["epistemic_state"] == "decoded"
assert payload["normative_clearance"] == "cleared"
assert payload["normative_detail"] == "all runtime-checkable verdicts upheld"
def test_lookup_unit_tags_curated_and_composed_entries() -> None:
curated = lookup_unit("dollar")
# "euro per hour" is not a curated lexicon entry; it is dynamically
# composed from decoded primitives (euro × hour) by the per-unit rule.
inferred = lookup_unit("euro per hour")
assert curated is not None
assert curated.epistemic_state == EpistemicState.DECODED.value
assert inferred is not None
assert inferred.epistemic_state == EpistemicState.INFERRED.value
def test_compile_entries_to_manifold_preserves_entry_status_mapping() -> None:
entries = [
LexicalEntry(
entry_id="coherent-1",
surface="alpha",
lemma="alpha",
language="en",
semantic_domains=("test.domain",),
provenance_ids=("fixture",),
epistemic_status="coherent",
),
LexicalEntry(
entry_id="speculative-1",
surface="beta",
lemma="beta",
language="en",
semantic_domains=("test.domain",),
provenance_ids=("fixture",),
epistemic_status="speculative",
),
LexicalEntry(
entry_id="falsified-1",
surface="gamma",
lemma="gamma",
language="en",
semantic_domains=("test.domain",),
provenance_ids=("fixture",),
epistemic_status="falsified",
),
]
manifold, _ = compile_entries_to_manifold(entries)
assert manifold.epistemic_state_for_word("alpha") == EpistemicState.DECODED.value
assert manifold.epistemic_state_for_word("beta") == EpistemicState.UNVERIFIED_POSSIBLE.value
assert manifold.epistemic_state_for_word("gamma") == EpistemicState.CONTRADICTED.value
def test_vault_statuses_map_to_distinct_epistemic_states() -> None:
assert epistemic_state_for_vault_status(EpistemicStatus.COHERENT) is EpistemicState.DECODED
assert epistemic_state_for_vault_status(EpistemicStatus.FALSIFIED) is EpistemicState.CONTRADICTED
assert epistemic_state_for_vault_status(EpistemicStatus.SPECULATIVE) is EpistemicState.UNVERIFIED_POSSIBLE
assert epistemic_state_for_vault_status(EpistemicStatus.CONTESTED) is EpistemicState.AMBIGUOUS
def test_vault_recall_exposes_epistemic_state_metadata() -> None:
vault = VaultStore(reproject_interval=0)
v = np.zeros(32, dtype=np.float32)
v[0] = 1.0
vault.store(v, epistemic_status=EpistemicStatus.FALSIFIED)
hits = vault.recall(v, top_k=1)
assert len(hits) == 1
assert hits[0]["epistemic_state"] == EpistemicState.CONTRADICTED.value
assert hits[0]["metadata"]["epistemic_state"] == EpistemicState.CONTRADICTED.value

View file

@ -17,6 +17,7 @@ from collections import deque
import numpy as np import numpy as np
from algebra.backend import vault_recall, vault_recall_batch from algebra.backend import vault_recall, vault_recall_batch
from algebra.cga import null_project from algebra.cga import null_project
from core.epistemic_state import EpistemicState
from teaching.epistemic import ADMISSIBLE_AS_EVIDENCE, EpistemicStatus from teaching.epistemic import ADMISSIBLE_AS_EVIDENCE, EpistemicStatus
@ -24,6 +25,19 @@ def _versor_key(F: np.ndarray) -> bytes:
return np.asarray(F, dtype=np.float32).tobytes() return np.asarray(F, dtype=np.float32).tobytes()
def epistemic_state_for_vault_status(entry_status: EpistemicStatus) -> EpistemicState:
"""Map legacy vault review statuses onto the ratified state taxonomy."""
if entry_status is EpistemicStatus.COHERENT:
return EpistemicState.DECODED
if entry_status is EpistemicStatus.FALSIFIED:
return EpistemicState.CONTRADICTED
if entry_status is EpistemicStatus.SPECULATIVE:
return EpistemicState.UNVERIFIED_POSSIBLE
if entry_status is EpistemicStatus.CONTESTED:
return EpistemicState.AMBIGUOUS
return EpistemicState.EPISTEMIC_STATE_NEEDED
def _status_admits(entry_status: EpistemicStatus, min_status: EpistemicStatus) -> bool: def _status_admits(entry_status: EpistemicStatus, min_status: EpistemicStatus) -> bool:
"""Return True iff `entry_status` is admissible at the `min_status` tier. """Return True iff `entry_status` is admissible at the `min_status` tier.
@ -31,7 +45,10 @@ def _status_admits(entry_status: EpistemicStatus, min_status: EpistemicStatus) -
requested tier they carry CONTRADICTED semantics and are retained only requested tier they carry CONTRADICTED semantics and are retained only
for provenance and Stage-3 inversion (ADR-0021 §3). SPECULATIVE entries for provenance and Stage-3 inversion (ADR-0021 §3). SPECULATIVE entries
are separately excluded at the COHERENT tier (UNVERIFIED-POSSIBLE semantics are separately excluded at the COHERENT tier (UNVERIFIED-POSSIBLE semantics
not yet coherent, but distinct from actively falsified). If the not yet coherent, but distinct from actively falsified). The
exclusion reason for each status is externally inspectable via
``epistemic_state_for_vault_status``: FALSIFIEDCONTRADICTED,
SPECULATIVEUNVERIFIED_POSSIBLE, CONTESTEDAMBIGUOUS. If the
admissibility set grows in the future (it should not, per ADR-0021), only admissibility set grows in the future (it should not, per ADR-0021), only
this helper changes. this helper changes.
""" """
@ -42,6 +59,17 @@ def _status_admits(entry_status: EpistemicStatus, min_status: EpistemicStatus) -
return entry_status is min_status return entry_status is min_status
def _parse_entry_status(raw_status: object) -> EpistemicStatus:
if isinstance(raw_status, EpistemicStatus):
return raw_status
if isinstance(raw_status, str):
try:
return EpistemicStatus(raw_status)
except ValueError:
return EpistemicStatus.SPECULATIVE
return EpistemicStatus.SPECULATIVE
class VaultStore: class VaultStore:
def __init__( def __init__(
self, self,
@ -79,6 +107,7 @@ class VaultStore:
arr = np.asarray(F, dtype=np.float32).copy() arr = np.asarray(F, dtype=np.float32).copy()
stamped: dict = dict(metadata) if metadata else {} stamped: dict = dict(metadata) if metadata else {}
stamped["epistemic_status"] = epistemic_status.value stamped["epistemic_status"] = epistemic_status.value
stamped["epistemic_state"] = epistemic_state_for_vault_status(epistemic_status).value
will_evict = self._max_entries is not None and len(self._versors) >= self._max_entries will_evict = self._max_entries is not None and len(self._versors) >= self._max_entries
self._versors.append(arr) self._versors.append(arr)
@ -143,11 +172,9 @@ class VaultStore:
if min_status is not None: if min_status is not None:
filtered: list[tuple[int, float]] = [] filtered: list[tuple[int, float]] = []
for i, score in ranked: for i, score in ranked:
raw_status = self._metadata[i].get("epistemic_status", "speculative") entry_status = _parse_entry_status(
try: self._metadata[i].get("epistemic_status", "speculative")
entry_status = EpistemicStatus(raw_status) )
except ValueError:
entry_status = EpistemicStatus.SPECULATIVE
if _status_admits(entry_status, min_status): if _status_admits(entry_status, min_status):
filtered.append((i, score)) filtered.append((i, score))
ranked = filtered ranked = filtered
@ -158,6 +185,9 @@ class VaultStore:
"score": float(score), "score": float(score),
"metadata": self._metadata[i], "metadata": self._metadata[i],
"index": i, "index": i,
"epistemic_state": epistemic_state_for_vault_status(
_parse_entry_status(self._metadata[i].get("epistemic_status", "speculative"))
).value,
} }
for i, score in ranked[:top_k] for i, score in ranked[:top_k]
] ]
@ -204,13 +234,9 @@ class VaultStore:
if min_status is not None: if min_status is not None:
filtered: list[tuple[int, float]] = [] filtered: list[tuple[int, float]] = []
for i, score in ranked: for i, score in ranked:
raw_status = self._metadata[i].get( entry_status = _parse_entry_status(
"epistemic_status", "speculative", self._metadata[i].get("epistemic_status", "speculative")
) )
try:
entry_status = EpistemicStatus(raw_status)
except ValueError:
entry_status = EpistemicStatus.SPECULATIVE
if _status_admits(entry_status, min_status): if _status_admits(entry_status, min_status):
filtered.append((i, score)) filtered.append((i, score))
ranked = filtered ranked = filtered
@ -221,6 +247,9 @@ class VaultStore:
"score": float(score), "score": float(score),
"metadata": self._metadata[i], "metadata": self._metadata[i],
"index": i, "index": i,
"epistemic_state": epistemic_state_for_vault_status(
_parse_entry_status(self._metadata[i].get("epistemic_status", "speculative"))
).value,
} }
for i, score in ranked[:top_k] for i, score in ranked[:top_k]
]) ])

View file

@ -32,6 +32,7 @@ import numpy as np
from algebra.backend import cga_inner from algebra.backend import cga_inner
from algebra.cl41 import geometric_product, reverse from algebra.cl41 import geometric_product, reverse
from algebra.versor import versor_unit_residual from algebra.versor import versor_unit_residual
from core.epistemic_state import EpistemicState, coerce_epistemic_state
from core.physics.energy import EnergyProfile from core.physics.energy import EnergyProfile
from core.physics.valence import ValenceBundle from core.physics.valence import ValenceBundle
from language_packs.schema import MorphologyEntry from language_packs.schema import MorphologyEntry
@ -72,6 +73,7 @@ class VocabManifold:
self._language_by_word: dict[str, str] = {} self._language_by_word: dict[str, str] = {}
self._energy_by_word: dict[str, EnergyProfile] = {} self._energy_by_word: dict[str, EnergyProfile] = {}
self._valence_by_word: dict[str, ValenceBundle] = {} self._valence_by_word: dict[str, ValenceBundle] = {}
self._epistemic_state_by_word: dict[str, str] = {}
self._transient_words: set[str] = set() self._transient_words: set[str] = set()
self._unknown_token_log: list[dict[str, object]] = [] self._unknown_token_log: list[dict[str, object]] = []
@ -83,6 +85,7 @@ class VocabManifold:
language: str | None = None, language: str | None = None,
energy: EnergyProfile | None = None, energy: EnergyProfile | None = None,
valence: ValenceBundle | None = None, valence: ValenceBundle | None = None,
epistemic_state: str | EpistemicState | None = None,
) -> None: ) -> None:
""" """
Register a word-versor pair. Register a word-versor pair.
@ -97,6 +100,10 @@ class VocabManifold:
algebra primitive. Do not call normalize_to_versor() directly; algebra primitive. Do not call normalize_to_versor() directly;
that function is reserved for the injection gate. that function is reserved for the injection gate.
``epistemic_state`` defaults to DECODED for compiled pack entries.
The compiler can override this when a lexical row's review status
maps to another ratified state.
Raises: Raises:
ValueError: if the full unit-versor residual is not satisfied. ValueError: if the full unit-versor residual is not satisfied.
""" """
@ -104,6 +111,10 @@ class VocabManifold:
_assert_manifold_versor(word, v) _assert_manifold_versor(word, v)
self._words.append(word) self._words.append(word)
self._versors.append(v) self._versors.append(v)
self._epistemic_state_by_word[word] = coerce_epistemic_state(
epistemic_state,
default=EpistemicState.DECODED,
).value
resolved_language = language or (morphology.language if morphology is not None else None) resolved_language = language or (morphology.language if morphology is not None else None)
if resolved_language: if resolved_language:
self._language_by_word[word] = resolved_language self._language_by_word[word] = resolved_language
@ -128,7 +139,7 @@ class VocabManifold:
if word in self._transient_words: if word in self._transient_words:
self.update(word, versor) self.update(word, versor)
return return
self.add(word, versor) self.add(word, versor, epistemic_state=EpistemicState.UNVERIFIED_NOVEL)
self._transient_words.add(word) self._transient_words.add(word)
def is_transient(self, word: str) -> bool: def is_transient(self, word: str) -> bool:
@ -221,6 +232,16 @@ class VocabManifold:
"""Return ADR-0007 valence bundle for a stored surface, when available.""" """Return ADR-0007 valence bundle for a stored surface, when available."""
return self._valence_by_word.get(word) return self._valence_by_word.get(word)
def epistemic_state_for_word(self, word: str) -> str:
"""Return the ratified epistemic state for a stored surface.
Missing state metadata defaults to UNDETERMINED rather than
silently treating the entry as decoded.
"""
if word not in self._words:
raise KeyError(f"Word '{word}' not in vocabulary.")
return self._epistemic_state_by_word.get(word, EpistemicState.UNDETERMINED.value)
def language_for_word(self, word: str) -> str | None: def language_for_word(self, word: str) -> str | None:
"""Return the language code for a stored surface, if known.""" """Return the language code for a stored surface, if known."""
morphology = self._morphology_by_word.get(word) morphology = self._morphology_by_word.get(word)