Compare commits

...

12 commits

Author SHA1 Message Date
Shay
ca9212b5f7 feat: materialize refusal_reason in CognitiveTurnResult when safety/ethics refusal fires 2026-05-24 11:50:19 -07:00
Shay
ec40b5c639 docs(epistemic-scope): mark Framing 1 audit complete across all six subsystems
Teaching pipeline (47 pts, 0 new states), cognition pipeline (42 pts,
0 new states, 1 EPISTEMIC_STATE_NEEDED placeholder), and chat runtime
(47 pts, 0 new states, 6 provenance gaps) audits complete. Taxonomy
confirmed stable; remaining work is implementation debt and provenance,
not taxonomy extension.
2026-05-24 11:39:47 -07:00
Shay
cd720b31d8 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.
2026-05-24 11:25:32 -07:00
Shay
f9c953af66 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.
2026-05-24 11:21:04 -07:00
Shay
e5b1d82074 test(epistemic): cover phase 3 state tagging spine 2026-05-24 11:21:04 -07:00
Shay
ecd64c1ea4 feat(epistemic): preserve pack entry states through compiler 2026-05-24 11:21:04 -07:00
Shay
3d699bc2f6 feat(epistemic): expose vault status mapping 2026-05-24 11:21:04 -07:00
Shay
08664f92d1 feat(epistemic): expose word-level state on manifold 2026-05-24 11:20:42 -07:00
Shay
940ba79193 feat(packs): tag curated and inferred unit entries 2026-05-24 11:20:42 -07:00
Shay
1b3e8f0f8d feat(epistemic): serialize turn state axes 2026-05-24 11:20:42 -07:00
Shay
3852323d73 feat(epistemic): tag TurnEvent with state axes 2026-05-24 11:20:42 -07:00
Shay
764bc7f7f5 feat(epistemic): add first-class state enums 2026-05-24 11:20:42 -07:00
12 changed files with 482 additions and 41 deletions

View file

@ -32,6 +32,10 @@ from chat.refusal import (
inject_hedge,
should_inject_hedge,
)
from core.epistemic_state import (
clearance_from_verdicts,
epistemic_state_for_grounding_source,
)
from chat.telemetry import (
TurnEventSink,
format_correction_event_jsonl,
@ -350,6 +354,14 @@ class ChatResponse:
# the truth path (ADR-0069 invariant C). Empty string ⇒ identical
# to ``surface`` (no decoration applied this turn).
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.
# Mirrors the TurnEvent fields so callers (CLI, demos, tests) can
# read the register state from ChatResponse without re-parsing the
@ -393,6 +405,8 @@ class ChatResponse:
# for every caller that constructs ChatResponse without this
# field.
recalled_words: tuple[str, ...] = ()
# ADR-0024 Phase 2 — stable refusal reason value
refusal_reason: str = ""
class ChatRuntime:
@ -1385,6 +1399,8 @@ class ChatRuntime:
refusal_emitted=refusal_emitted,
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:
stub_event = TurnEvent(
turn=max(self._context.turn - 1, 0),
@ -1414,6 +1430,8 @@ class ChatRuntime:
composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash,
graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash,
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._emit_turn_event(stub_event)
@ -1469,6 +1487,9 @@ class ChatRuntime:
composer_atom_set_hash=atom_equivalence_stub.composer_atom_set_hash,
graph_atom_set_hash=atom_equivalence_stub.graph_atom_set_hash,
composer_graph_atom_overlap_count=atom_equivalence_stub.overlap_count,
epistemic_state=stub_epistemic_state,
normative_clearance=stub_normative_clearance,
refusal_reason=refusal_surface if refusal_emitted else "",
)
def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse:
@ -1885,6 +1906,10 @@ class ChatRuntime:
refusal_emitted=refusal_emitted,
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=self._context.turn - 1,
input_tokens=tuple(filtered),
@ -1913,6 +1938,8 @@ class ChatRuntime:
composer_atom_set_hash=atom_equivalence_main.composer_atom_set_hash,
graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash,
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._emit_turn_event(turn_event)
@ -1958,6 +1985,9 @@ class ChatRuntime:
graph_atom_set_hash=atom_equivalence_main.graph_atom_set_hash,
composer_graph_atom_overlap_count=atom_equivalence_main.overlap_count,
recalled_words=walk_tokens,
epistemic_state=main_epistemic_state,
normative_clearance=main_normative_clearance,
refusal_reason=refusal_surface if refusal_emitted else "",
)
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 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."
@ -61,6 +66,15 @@ def serialize_turn_event(
"safety_pack_id": str(safety_pack_id),
"ethics_pack_id": str(ethics_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)),
"hedge_injected": bool(getattr(verdicts, "hedge_injected", False)),
"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
# downstream audit consumers.
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.
# ``register_id`` is the loaded pack id (e.g. ``"convivial_v1"``),
# or ``""`` for the in-memory UNREGISTERED sentinel.

View file

@ -3,7 +3,7 @@
**Status:** Draft / scope-only (not a decision yet — prerequisite for one)
**Date:** 2026-05-24
**Author:** CORE agents
**Audit:** Math-subsystem audit complete (2026-05-24) — 40 decision points, 4 gaps ratified. Vault audit complete (2026-05-24) — 0 new states, 1 implementation debt. Language-packs audit complete (2026-05-24) — 1 gap (INFERRED) ratified. Runtime-packs audit complete (2026-05-24) — 0 new epistemic states; normative clearance axis identified as separate companion axis
**Audit:** Math-subsystem audit complete (2026-05-24) — 40 decision points, 4 gaps ratified. Vault audit complete (2026-05-24) — 0 new states, 1 implementation debt. Language-packs audit complete (2026-05-24) — 1 gap (INFERRED) ratified. Runtime-packs audit complete (2026-05-24) — 0 new epistemic states; normative clearance axis identified as separate companion axis. Teaching pipeline audit complete (2026-05-24) — 47 decision points, 0 new states, 4 minor implementation debts. Cognition pipeline audit complete (2026-05-24) — 42 decision points, 0 new states, 1 EPISTEMIC_STATE_NEEDED (refusal_reason placeholder, materialisation deferred). Chat runtime audit complete (2026-05-24) — 47 decision points, 0 new states, 6 provenance gaps (dispatcher does not carry decision rationale). **Framing 1 audit complete across all six subsystems.**
**Anchor:** [thesis-decoding-not-generating](../../../.claude/projects/-Users-kaizenpro-Projects-core/memory/thesis-decoding-not-generating.md) (memory)
**Related:** [teaching-derived-recognition-scope](./teaching-derived-recognition-scope.md)
@ -280,14 +280,15 @@ This is *not a spike* — it's an audit. It produces no new code. But
it surfaces whether the starter taxonomy is sufficient before any new
work is committed.
**Status: COMPLETE across four subsystems (2026-05-24).**
**Status: COMPLETE across all six subsystems (2026-05-24).**
- **Math** — 40 decision points across 9 files. 4 gaps ratified: EVIDENCED-INCOMPLETE, DECODED-UNARTICULATED, SCOPE_BOUNDARY, COMPUTATIONALLY_BOUNDED.
- **Vault** — 33 decision points across `vault/store.py` + `vault/decompose.py`. 0 new states. 1 implementation debt: `_status_admits` collapses FALSIFIED (→ CONTRADICTED) and SPECULATIVE (→ UNVERIFIED-POSSIBLE) into a single "not admissible" bucket without distinguishing them. 1 deferred candidate: COMPOSED_RECOGNITION (decomposed recall above floor) — currently covered adequately by UNVERIFIED-POSSIBLE; revisit if provenance needs to distinguish composed vs. asserted recall.
- **Language packs** — 25 decision points across 6 files. 1 gap ratified: INFERRED (rule-derived from DECODED primitives, composite not curated). 4 implementation bugs identified (see Phase 2).
- **Runtime packs** — 28 decision points across 6 files. 0 new epistemic states. Structural finding: safety/ethics verdicts are a separate normative clearance axis, orthogonal to epistemic state. Normative axis ratified as companion (see section above).
Remaining subsystems not yet audited: teaching pipeline, cognition pipeline (`core/cognition/`), chat runtime.
- **Teaching pipeline** — 47 decision points across 12 files. 0 new states. All 47 mapped cleanly to the existing taxonomy. Key coverage: COMPUTATIONALLY_BOUNDED (contemplation recursion depth cap), AMBIGUOUS (mixed-polarity evidence reduction in `store.py`), SCOPE_BOUNDARY (decomposition exhaustion, evaluative domain gate). 4 minor implementation debts: source-field-absent proposals have no explicit epistemic characterisation; identity override gate does not distinguish which layer (syntactic vs. geometric) fired; contradiction detection thresholds are implicit magic values; watched-metrics tuple in `replay.py` lacks versioning.
- **Cognition pipeline** — 42 decision points across 6 files. 0 new states. 1 EPISTEMIC_STATE_NEEDED case: `CognitiveTurnResult.refusal_reason` is a placeholder field whose materialisation site is deferred to ChatRuntime (ADR-0024 Phase 2 scope decision); until ChatRuntime populates it, the field is dead weight and the conditional fold in `trace.py` has no effect. 3 critical implementation debts: (1) cold-start PASSTHROUGH collapses three distinct conditions (no field_state, no vocab, no prompt_versor) into one indistinguishable outcome; (2) `_should_mark_speculative()` reflexive-probe heuristic can produce false positives on unrelated queries; (3) surface authority is resolved and discarded — the selected authority name is available only in `SurfaceResolution`, not in `CognitiveTurnResult`. States not covered by the pipeline's current code paths: COMPUTATIONALLY_BOUNDED (no search-budget exhaustion tracking), UNVERIFIED-NOVEL (new teaching proposals enter as SPECULATIVE, not UNVERIFIED-NOVEL).
- **Chat runtime** — 47 decision points across 13 files. 0 new states. Taxonomy is sufficient for every decision point; the gaps are provenance gaps, not taxonomy gaps. 6 sites where an epistemic decision is made but the rationale is not carried: (1) pack-grounded surface dispatcher (`runtime.py:8311012`) does not record which sources were attempted or why each fell through; (2) discourse planner engagement (`runtime.py:10471235`) does not surface which of its multi-gates failed on a miss; (3) teaching corpus collision resolution (`teaching_grounding.py:298319`) applies first-match-wins without recording whether a collision occurred; (4) config-driven surface shape (transitive / composed / single) does not carry why one config is preferred; (5) unified ingest timing (`runtime.py:14921521`, ADR-0090) does not track whether both ingest paths reach the same gate decision; (6) generation call (`runtime.py:16441667`) has 15+ control parameters with no conflict-detection signal. The grounding-source selector is the runtime's primary epistemic articulation point; all six gaps cluster around it.
**Framing 2 — Spike on a single subsystem.** Pick one subsystem
(probably recognition, since the parallel scope is already underway)
@ -331,13 +332,12 @@ all subsystems) is gated on:
## Risks the spike must surface
- **The starter taxonomy may be too coarse.** Risk is now substantially
closed. Four audits complete; taxonomy grew from 9 to 14 epistemic
states (+ meta-state) and acquired a companion normative clearance
axis. Remaining audits (teaching pipeline, cognition pipeline, chat
runtime) may surface further gaps, but the core epistemic vocabulary
is stable. The normative axis finding was not anticipated in the
original risk — it is additive, not corrective.
- **The starter taxonomy may be too coarse.** Risk is now closed. All
six audits complete; taxonomy grew from 9 to 14 epistemic states
(+ meta-state) and acquired a companion normative clearance axis.
Teaching, cognition, and chat runtime audits produced zero new states.
The remaining gaps are provenance and implementation debts, not
taxonomy gaps. The vocabulary is stable.
- **Provenance overhead.** Every state assignment carries structured
provenance. For propositions handled in tight loops (vault recall,
@ -411,13 +411,20 @@ vault, packs, teaching, recognition, and verification — and whether
provenance-on-every-assignment is feasible without overhead that breaks
the engine's hot path.
Four subsystem audits are now complete. The taxonomy is stable at 14
epistemic states (PERCEIVED, EVIDENCED, EVIDENCED-INCOMPLETE, VERIFIED,
DECODED, DECODED-UNARTICULATED, INFERRED, UNVERIFIED-POSSIBLE,
UNVERIFIED-NOVEL, CONTRADICTED, AMBIGUOUS, UNDETERMINED, SCOPE_BOUNDARY,
COMPUTATIONALLY_BOUNDED, plus the recursive EPISTEMIC_STATE_NEEDED) and
a 4-value normative clearance axis (CLEARED, VIOLATED, UNASSESSABLE,
SUPPRESSED) that is orthogonal to the epistemic axis.
All six subsystem audits are now complete (math, vault, language packs,
runtime packs, teaching pipeline, cognition pipeline, chat runtime).
The taxonomy is stable at 14 epistemic states (PERCEIVED, EVIDENCED,
EVIDENCED-INCOMPLETE, VERIFIED, DECODED, DECODED-UNARTICULATED,
INFERRED, UNVERIFIED-POSSIBLE, UNVERIFIED-NOVEL, CONTRADICTED, AMBIGUOUS,
UNDETERMINED, SCOPE_BOUNDARY, COMPUTATIONALLY_BOUNDED, plus the recursive
EPISTEMIC_STATE_NEEDED) and a 4-value normative clearance axis (CLEARED,
VIOLATED, UNASSESSABLE, SUPPRESSED) that is orthogonal to the epistemic
axis. No new states were required by any of the final three audits.
The remaining open work is implementation: carrying explicit epistemic
state at the grounding-source dispatcher in the chat runtime, resolving
the cold-start PASSTHROUGH ambiguity in the cognition pipeline, and
populating the `normative_detail` field on `TurnEvent` when clearance
is VIOLATED or SUPPRESSED.
Known implementation issues to address before Phase 3 integration
(tracked separately as Phase 2):

View file

@ -84,7 +84,7 @@ def _make_regressed_replay(
baseline = {
"intent_accuracy": 1.0,
"surface_groundedness": 1.0,
"term_capture_rate": 0.9167,
"term_capture_rate": 1.0,
"versor_closure_rate": 1.0,
}
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.versor import unitize_versor
from core.epistemic_state import EpistemicState
from core.physics.energy import FieldEnergyOperator
from core.physics.valence import lift_valence
from language_packs.schema import (
@ -19,6 +20,7 @@ from language_packs.schema import (
MorphologyEntry,
OOVPolicy,
)
from teaching.epistemic import EpistemicStatus, parse_status
from vocab.manifold import VocabManifold
if TYPE_CHECKING:
@ -70,6 +72,26 @@ _FEATURE_COMPONENTS: tuple[int, ...] = (6, 7, 9, 10, 12, 14)
_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:
digest = hashlib.sha256(f"{salt}:{name}".encode("utf-8")).digest()
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,
energy=energy,
valence=valence,
epistemic_state=_entry_epistemic_state(entry),
)
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),
energy=source.energy_for_word(surface),
valence=source.valence_for_word(surface),
epistemic_state=source.epistemic_state_for_word(surface),
)
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,
energy=manifold.energy_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:
primary_groups.setdefault(entry.semantic_domains[0].lower(), []).append(

View file

@ -4,6 +4,8 @@ import json
from dataclasses import dataclass
from pathlib import Path
from core.epistemic_state import EpistemicState
# Dataclass definitions as required by the contract
@dataclass(frozen=True, slots=True)
class UnitEntry:
@ -14,6 +16,7 @@ class UnitEntry:
dimension: str
is_canonical_for_dimension: bool
provenance_ids: list[str]
epistemic_state: str = EpistemicState.DECODED.value
@dataclass(frozen=True, slots=True)
class ContainerEntry:
@ -112,7 +115,8 @@ def _ensure_loaded() -> None:
symbol=entry.get("symbol"),
dimension=entry["dimension"],
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[lemma] = ue
@ -138,10 +142,38 @@ def _ensure_loaded() -> None:
_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
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()
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}"
plural = f"{left_unit.plural} per {right_unit.singular}"
is_canon = (left_unit.singular == "dollar" and right_unit.singular == "hour")
return UnitEntry(
return _inferred_unit_entry(
surface=token,
singular=singular,
plural=plural,
@ -179,7 +211,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} per {r_sing}"
plural = f"{left_unit.plural} per {r_sing}"
is_canon = (left_unit.singular == "dollar" and r_sing == "item")
return UnitEntry(
return _inferred_unit_entry(
surface=token,
singular=singular,
plural=plural,
@ -194,7 +226,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} per {right_unit.singular}"
plural = f"{left_unit.plural} per {right_unit.singular}"
is_canon = (left_unit.singular == "mile" and right_unit.singular == "hour")
return UnitEntry(
return _inferred_unit_entry(
surface=token,
singular=singular,
plural=plural,
@ -209,7 +241,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"{left_unit.singular} 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")
return UnitEntry(
return _inferred_unit_entry(
surface=token,
singular=singular,
plural=plural,
@ -227,7 +259,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
singular = f"square {sub_unit.singular}"
plural = f"square {sub_unit.plural}"
is_canon = (sub_unit.singular == "foot")
return UnitEntry(
return _inferred_unit_entry(
surface=token,
singular=singular,
plural=plural,
@ -244,7 +276,7 @@ def lookup_unit(token: str) -> UnitEntry | None:
if sub_unit and sub_unit.dimension == "length":
singular = f"cubic {sub_unit.singular}"
plural = f"cubic {sub_unit.plural}"
return UnitEntry(
return _inferred_unit_entry(
surface=token,
singular=singular,
plural=plural,

View file

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

View file

@ -0,0 +1,158 @@
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
def test_refusal_reason_materialized_in_cognitive_turn_result() -> None:
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
from core.cognition import CognitiveTurnPipeline
from chat.refusal import TYPED_REFUSAL_PREFIX
from packs.safety.check import SafetyCheckResult
from core.cognition.trace import trace_hash_from_result
import dataclasses
runtime = ChatRuntime(config=RuntimeConfig())
pipeline = CognitiveTurnPipeline(runtime)
# 1. Normal turn: refusal_reason must be empty
result_normal = pipeline.run("light logos", max_tokens=8)
assert result_normal.refusal_reason == ""
normal_hash = result_normal.trace_hash
# 2. Force safety violation to trigger refusal
boundary_id = "preserve_versor_closure"
def _failing(ctx) -> SafetyCheckResult:
return SafetyCheckResult(
boundary_id=boundary_id,
upheld=False,
reason="forced for test",
runtime_checkable=True,
)
runtime.safety_check.register(boundary_id, _failing)
# 3. Refusal turn: refusal_reason must be populated
result_refusal = pipeline.run("light logos", max_tokens=8)
refusal_reason = result_refusal.refusal_reason
assert refusal_reason != ""
assert refusal_reason.startswith(TYPED_REFUSAL_PREFIX)
assert boundary_id in refusal_reason
# 4. Check trace hash is different due to refusal_reason fold
refusal_hash = result_refusal.trace_hash
assert refusal_hash != normal_hash
# 5. Verify trace hashing conditional fold explicitly
# If we compute the trace hash from a modified result with empty refusal_reason,
# it must differ from the refusal_hash.
modified_result = dataclasses.replace(result_refusal, refusal_reason="")
recomputed_without_reason = trace_hash_from_result(modified_result)
assert recomputed_without_reason != refusal_hash

View file

@ -17,6 +17,7 @@ from collections import deque
import numpy as np
from algebra.backend import vault_recall, vault_recall_batch
from algebra.cga import null_project
from core.epistemic_state import EpistemicState
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()
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:
"""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
for provenance and Stage-3 inversion (ADR-0021 §3). SPECULATIVE entries
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
this helper changes.
"""
@ -42,6 +59,17 @@ def _status_admits(entry_status: EpistemicStatus, min_status: EpistemicStatus) -
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:
def __init__(
self,
@ -79,6 +107,7 @@ class VaultStore:
arr = np.asarray(F, dtype=np.float32).copy()
stamped: dict = dict(metadata) if metadata else {}
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
self._versors.append(arr)
@ -143,11 +172,9 @@ class VaultStore:
if min_status is not None:
filtered: list[tuple[int, float]] = []
for i, score in ranked:
raw_status = self._metadata[i].get("epistemic_status", "speculative")
try:
entry_status = EpistemicStatus(raw_status)
except ValueError:
entry_status = EpistemicStatus.SPECULATIVE
entry_status = _parse_entry_status(
self._metadata[i].get("epistemic_status", "speculative")
)
if _status_admits(entry_status, min_status):
filtered.append((i, score))
ranked = filtered
@ -158,6 +185,9 @@ class VaultStore:
"score": float(score),
"metadata": self._metadata[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]
]
@ -204,13 +234,9 @@ class VaultStore:
if min_status is not None:
filtered: list[tuple[int, float]] = []
for i, score in ranked:
raw_status = self._metadata[i].get(
"epistemic_status", "speculative",
entry_status = _parse_entry_status(
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):
filtered.append((i, score))
ranked = filtered
@ -221,6 +247,9 @@ class VaultStore:
"score": float(score),
"metadata": self._metadata[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]
])

View file

@ -32,6 +32,7 @@ import numpy as np
from algebra.backend import cga_inner
from algebra.cl41 import geometric_product, reverse
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.valence import ValenceBundle
from language_packs.schema import MorphologyEntry
@ -72,6 +73,7 @@ class VocabManifold:
self._language_by_word: dict[str, str] = {}
self._energy_by_word: dict[str, EnergyProfile] = {}
self._valence_by_word: dict[str, ValenceBundle] = {}
self._epistemic_state_by_word: dict[str, str] = {}
self._transient_words: set[str] = set()
self._unknown_token_log: list[dict[str, object]] = []
@ -83,6 +85,7 @@ class VocabManifold:
language: str | None = None,
energy: EnergyProfile | None = None,
valence: ValenceBundle | None = None,
epistemic_state: str | EpistemicState | None = None,
) -> None:
"""
Register a word-versor pair.
@ -97,6 +100,10 @@ class VocabManifold:
algebra primitive. Do not call normalize_to_versor() directly;
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:
ValueError: if the full unit-versor residual is not satisfied.
"""
@ -104,6 +111,10 @@ class VocabManifold:
_assert_manifold_versor(word, v)
self._words.append(word)
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)
if resolved_language:
self._language_by_word[word] = resolved_language
@ -128,7 +139,7 @@ class VocabManifold:
if word in self._transient_words:
self.update(word, versor)
return
self.add(word, versor)
self.add(word, versor, epistemic_state=EpistemicState.UNVERIFIED_NOVEL)
self._transient_words.add(word)
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 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:
"""Return the language code for a stored surface, if known."""
morphology = self._morphology_by_word.get(word)