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:
parent
a45eab1fe3
commit
ab4c7cb0c3
11 changed files with 404 additions and 23 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
118
core/epistemic_state.py
Normal 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",
|
||||||
|
]
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
109
tests/test_epistemic_phase3_state_tagging.py
Normal file
109
tests/test_epistemic_phase3_state_tagging.py
Normal 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
|
||||||
|
|
@ -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``: FALSIFIED→CONTRADICTED,
|
||||||
|
SPECULATIVE→UNVERIFIED_POSSIBLE, CONTESTED→AMBIGUOUS. 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]
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue