core/core/cognition/pipeline.py
Shay bb95dfbe67 fix(runtime): defer the turn-event sink emission to the serve boundary (R7)
Audit-ledger R7, implementing docs/handoff/curriculum-license-loop-2026-07/
E1-R7-ASSERTION-SPEC.md. `ChatRuntime.chat` seals a TurnEvent before the
surface authority is known; `CognitiveTurnPipeline` back-stamps the resolved
surface and canonical trace_hash onto `turn_log[-1]` after `chat` returns. So
the in-memory log ended up correct and the DURABLE stream carried the
pre-override record — the thing that persists was the wrong one.

Mechanism, per the spec's M1-M3:

- M1 — stage an INDEX into `turn_log`, never the TurnEvent. The flush re-reads
  the log, so every back-stamp is picked up by construction and the staged
  record cannot go stale. Capturing the event object would be the original
  defect with extra steps.
- M2 — `attach_telemetry_sink` does NOT flush a staged turn event, the
  opposite of what it correctly does for `_pending_reboot_payload`. The reboot
  payload is final when buffered; a staged turn event is mid-flight. Flushing
  on attach would silently restore the defect.
- M3 — deferral is opt-in. The pipeline wraps the runtime rather than the
  reverse, so the runtime cannot detect whether a pipeline will run. A runtime
  used directly (eval and demo paths) emits inline, byte-identically and at
  the same moment as before.

The `finally` at the serve boundary is load-bearing (I5): a pipeline that
raises after `chat` returns must still flush, or an error path silently
swallows a telemetry record — worse than the staleness being fixed.

TWO CORRECTIONS TO MY OWN SPEC, reported rather than quietly absorbed:

1. I2 bullet 3 and I3 assert on an emitted `trace_hash`. There is no such
   field — `chat/telemetry.py` never serializes `TurnEvent.trace_hash`, so the
   sink never carried a trace_hash, stale or otherwise. R7 repairs the SURFACE
   only. Pinned by `test_trace_hash_is_not_in_the_wire_format_at_all` so the
   claim cannot be re-asserted; the replay-not-recomputation ruling is kept on
   the object that does carry the hash.
2. The surface divergence is unreachable under the DEFAULT config:
   `finalize_turn_surface` is a no-op on all 36 turns probed, because the
   runtime's own surface already wins the resolver. It fires under
   `realizer_grounded_authority` — 24 divergences over 84 turns. A test on the
   default config would have passed while proving nothing, exactly as the spec
   warned, so the fixture pins that flag and asserts the pre-fix divergence
   exists before asserting it is gone.

[Verification]: in-worktree on canonical CPython 3.12.13 with `uv sync
--locked`: smoke 591 (569 + 22), deductive 291 (unchanged). All seven existing
sink test files pass UNCHANGED (80 tests). warmed_session_consistency
telemetry_consistency_rate = 1.0. Mutation-checked all three mechanisms:
staging the event instead of the index -> 3 I2 tests red; flushing on attach ->
the M2 test red; dropping the `finally` -> the I5 test red. Registered in
`smoke` and `cognition`.
2026-07-26 10:56:03 -07:00

1377 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
CognitiveTurnPipeline — the cognitive spine.
Architecture:
listen -> ingest -> understand -> recall -> think -> articulate
-> learn_proposal -> trace
This first-pass implementation delegates to ChatRuntime internals so
future intelligence modules (IntentPropositionGraph, ArticulationRealizerV2,
ReviewedTeachingLoop, CognitiveEvalHarness) have a clean plug-in surface
without requiring a full ChatRuntime rewrite.
Constraint: ChatRuntime.chat() and ChatResponse contract are unchanged.
"""
from __future__ import annotations
import hashlib
import json
from collections import OrderedDict
import numpy as np
from algebra.backend import versor_condition
from algebra.cl41 import geometric_product, reverse, scalar_part
from field.state import FieldState
from core.cognition.geometric_coherence import evaluate_geometric_coherence
from core.cognition.inductive_closure import expand_relation_closure
from core.cognition.leeway import build_leeway_record
from core.cognition.result import CognitiveTurnResult
from core.cognition.surface_resolution import resolve_surface
from core.cognition.trace import compute_trace_hash, hash_admissibility_trace
from core.physics.goldtether import coherence_residual
from core.physics.wave_manifold import multivector_content_digest
from core.reasoning.adapters import evidence_from_entailment_trace
from generate.intent import classify_compound_intent
from generate.intent_bridge import _is_useful_surface
from generate.intent_ratifier import (
RatificationOutcome,
RatifiedIntent,
ratify_intent,
)
from generate.graph_planner import (
GraphNode,
PropositionGraph,
graph_from_intent,
ground_graph,
plan_articulation,
)
from recognition.anti_unifier import DerivedRecognizer, recognize
from recognition.carrier import EpistemicGraph, EpistemicNode
from recognition.connector import epistemic_node_to_graph_node
from recognition.depth_canonical import build_node_depths
from generate.realizer import realize_semantic
from generate.intent import IntentTag
from generate.operators import (
FrameComposeResult,
WalkResult,
compose_relations,
multi_relation_walk,
transitive_walk,
)
from generate.proof_chain import EntailmentTrace, evaluate_entailment_with_trace
from teaching.correction import CorrectionCandidate, extract_correction
from teaching.epistemic import EpistemicStatus
from teaching.review import ReviewedTeachingExample, review_correction
from teaching.store import PackMutationProposal, TeachingStore
# ADR-0021 §Articulation: surfaces backed by SPECULATIVE teaching material
# carry an explicit status marker. Wording must match SPECULATIVE_MARKERS in
# evals/articulation_of_status/runner.py: "speculative" and "not yet reviewed"
# are both checked.
_SPECULATIVE_SURFACE_MARKER = "(speculative, not yet reviewed) "
# Reflexive query shapes that almost always refer back to the immediately
# prior speculative teaching even when the subject token is not repeated:
# "Has this been reviewed?", "Is your answer about X confirmed?". Used to
# extend the marker beyond exact subject-token matches.
_REFLEXIVE_PROBE_MARKERS: tuple[str, ...] = (
"your answer",
"this answer",
"has this",
"is that",
"confirmed",
"reviewed",
"verified",
)
# Splitter for extracting individual subject tokens from a parsed-triple
# subject like "correction: wisdom" → ("correction", "wisdom") — so probes
# about "wisdom" still match a SPECULATIVE proposal whose triple parser
# included a clarifying prefix.
import re as _re
_SUBJECT_SPLIT_RE = _re.compile(r"[^a-z0-9]+")
_SUBJECT_STOPWORDS: frozenset[str] = frozenset({
"actually", "correction", "really", "indeed", "instead",
"the", "this", "that", "these", "those",
"is", "are", "was", "were", "been", "being",
"of", "for", "with", "and", "but", "from",
"your", "their", "answer",
})
# Conformal atom unification (dossier Subsystem C.1).
# Absolute "score > 1ε" is only meaningful for normalized null-cone points
# with self-inner ≈ 1. Pack versors can have cross-inner > 1, so we unify when
# the mutual reverse-product matches both self-products within ε (exact same
# geometric atom), else content-address by SHA-256 of components.
_UNIFY_EPS = 1e-4
# Finding 5 (audit 2026-05-20) — cap the speculative-subjects cache so a
# long teaching session cannot grow it without bound. 64 is large enough
# to cover every distinct teaching subject a single session realistically
# emits and small enough that the per-turn substring scan in
# ``_should_mark_speculative`` stays trivially cheap. LRU eviction: a
# subject re-encountered as SPECULATIVE refreshes its position; coherent
# promotion removes it explicitly.
_MAX_SPECULATIVE_SUBJECTS = 64
class CognitiveTurnPipeline:
"""Thin pipeline wrapper over ChatRuntime.
Phase 1 goal: extract the observability path so downstream modules have
a place to plug in. No new intelligence is added here.
"""
def __init__(
self,
runtime,
teaching_store: TeachingStore | None = None,
recognizer: DerivedRecognizer | None = None,
) -> None: # runtime: ChatRuntime (no import cycle)
self.runtime = runtime
self._last_node_id: str | None = None
self._current_node_depths: dict = {}
self._current_agent_node_id: str | None = None
self._last_node_depths: dict | None = None
self.teaching_store = teaching_store if teaching_store is not None else TeachingStore()
if recognizer is not None:
self._recognizer = recognizer
elif hasattr(runtime, "first_admitted_recognizer"):
self._recognizer = runtime.first_admitted_recognizer()
else:
self._recognizer = None
self._prior_surface: str | None = None
self._turn_number: int = 0
# ADR-0021 §Articulation: subjects of prior SPECULATIVE teaching
# proposals. When a later turn's input references one of these
# (by subject substring or reflexive query shape), the surface
# is prefixed with _SPECULATIVE_SURFACE_MARKER so the user can
# tell ratified knowledge from unreviewed teaching material.
#
# Finding 5 (audit 2026-05-20) — backed by an OrderedDict so the
# cache is bounded (LRU, cap ``_MAX_SPECULATIVE_SUBJECTS``) and
# supports explicit eviction when a proposal is promoted to
# COHERENT. Pre-fix this was a bare ``set`` that only grew,
# which both leaked speculative markers onto reviewed subjects
# forever and widened the per-turn substring scan unboundedly.
# Iteration order matches insertion / refresh order; lookups
# remain O(1).
self._speculative_subjects: OrderedDict[str, None] = OrderedDict()
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def run(self, text: str, max_tokens: int | None = None) -> CognitiveTurnResult:
"""Execute one full cognitive turn and return a complete result record.
Audit-ledger R7 — this pipeline owns the turn's telemetry emission.
``ChatRuntime.chat`` seals the ``TurnEvent`` before the surface
authority and the canonical ``trace_hash`` are known, so an inline
emission put a stale record into the durable stream while ``turn_log``
was back-stamped and correct. The runtime cannot detect that a pipeline
is wrapping it (the wrapping goes this way, not the other), so the
deferral is declared here and flushed at the serve boundary below.
The ``finally`` is load-bearing (I5): if anything between ``chat``
returning and the serve boundary raises, the staged event must still
reach the sink. Silently swallowing a telemetry record on the error path
would be a worse failure than the staleness this repairs.
"""
self.runtime.begin_deferred_turn_emission()
try:
return self._run_turn(text, max_tokens=max_tokens)
finally:
self.runtime.flush_deferred_turn_event()
def _run_turn(self, text: str, max_tokens: int | None = None) -> CognitiveTurnResult:
"""The turn body. Call :meth:`run` — it owns the emission boundary."""
# 0. TOKENIZE — once at the top; reused by recognition step and trace.
raw_tokens: tuple[str, ...] = tuple(self.runtime.tokenize(text))
# 0b. RECOGNIZE — if a DerivedRecognizer is attached (ADR-0144).
# Admitted → wrap in EpistemicGraph for observability and optional
# connector-grounded articulation. Refused or absent → None.
epistemic_graph: EpistemicGraph | None = None
# W-011 — recognition refusal_reason, materialized below into
# CognitiveTurnResult.refusal_reason when non-empty.
_recognition_refusal_reason: str = ""
if self._recognizer is not None:
# Same-turn depth for recognition: resolve he/grc roots from tokens
# before PropositionGraph exists (plan residual). Prefer early
# provisional t{i} depths; fall back to current/prior-turn graph
# depths for multi-turn chaining (AC1).
from chat.pack_resolver import resolve_token_depths
_early_depths, _early_agent = resolve_token_depths(raw_tokens)
_prior_depths = (
getattr(self, "_current_node_depths", None)
or getattr(self, "_last_node_depths", None)
or {}
)
_depths = _early_depths if _early_depths else _prior_depths
_agent_nid = _early_agent or getattr(self, "_current_agent_node_id", None)
_rec_outcome = recognize(
self._recognizer, raw_tokens, depths=_depths, agent_node_id=_agent_nid
)
if _rec_outcome.admitted:
_ep_node = EpistemicNode(
node_id=f"{self._recognizer.teaching_set_id}:{self._turn_number}",
recognition_outcome=_rec_outcome,
)
epistemic_graph = EpistemicGraph(
nodes=(_ep_node,),
recognizer_id=self._recognizer.teaching_set_id,
)
# ADR-0154 (W-020b) — producer-side wiring for the
# DerivedRecognizer registry. When a recognizer admits a
# turn, capture (tokens, bundle) so the registry can
# derive tighter recognizers via anti-unification at the
# next checkpoint. Pre-ADR-0154 the producer hook had
# no production caller (only tests invoked
# ``record_recognition_example``), so the registry
# could never grow from live traffic regardless of
# whether ``recognition_grounded_graph`` was enabled.
# The producer fires unconditionally; the consumer
# (``checkpoint_engine_state``'s derive_recognizer
# call) stays opt-in behind the same flag.
if (
_rec_outcome.proposition is not None
and hasattr(self.runtime, "record_recognition_example")
):
self.runtime.record_recognition_example(
raw_tokens, _rec_outcome.proposition
)
elif _rec_outcome.refusal_reason is not None:
from generate.exhaustion import RefusalReason as _ExhaustionRefusalReason
_recognition_refusal_reason = _ExhaustionRefusalReason.RECOGNITION_REFUSED.value
# 1. LISTEN — capture pre-turn field state. If absent, compile turn
# tokens into an initial Cl(4,1) wave-packet before intent ratification
# (Geometric Sovereignty — cold-start must compile a field first).
field_state_before: FieldState | None = self._capture_field_state()
if field_state_before is None or getattr(field_state_before, "F", None) is None:
field_state_before = self._compile_turn_wave_packet(raw_tokens)
# 1b. CLASSIFY — intent and proposition graph (deterministic, pre-chat)
# ADR-0089 Phase C1 (Finding 4, audit 2026-05-20) — run the
# compound classifier first and take its dominant clause as
# the seeded intent. ``classify_compound_intent`` already
# invokes ``classify_intent`` on the dominant fragment, so
# this is one regex cascade per turn instead of two (comb
# pass 2026-05-21). Secondary clauses surface on
# ``CognitiveTurnResult.dropped_compound_clauses`` as
# observability telemetry; the dominant clause continues to
# route through the existing single-intent path.
compound = classify_compound_intent(text)
seeded_intent = compound.primary
dropped_compound_clauses: tuple = (
tuple(compound.parts[1:]) if compound.is_compound() else ()
)
# 1b.i FIELD-RATIFY the seeded intent (ADR-0022 §TBD-1).
# The regex classifier is the *seed*; the field is the
# gate. A demoted intent routes the rest of the turn
# through the existing UNKNOWN-domain surface so the
# pipeline never silently relaxes a constraint to produce
# a fluent-but-ungrounded surface (§2 honest refusal).
ratified = self._ratify_intent(seeded_intent, field_state_before)
intent = ratified.intent
prior_node_id = self._last_node_id
graph = graph_from_intent(intent, prior_node_id=prior_node_id)
target = plan_articulation(graph)
# 1b.ii RECOGNITION-GROUNDED GRAPH (ADR-0144, opt-in).
# When recognition admitted and the operator has opted in, replace the
# intent-derived graph and articulation target with ones derived from
# the admitted EpistemicNode via the connector. Default False preserves
# byte-identity for every existing surface and trace_hash.
if self.runtime.config.recognition_grounded_graph and epistemic_graph is not None:
_derived_gn = epistemic_node_to_graph_node(
epistemic_graph.nodes[0], source_intent=intent.tag
)
graph = PropositionGraph().add_node(_derived_gn)
target = plan_articulation(graph)
# 1c. REALIZE — semantic realization from graph + intent.
# Pre-fix (and default today) the realizer fires on the
# ungrounded graph and emits ``<pending>`` / ``...`` surfaces
# that ``_is_useful_surface`` rejects. ADR-0088 Phase B opts
# operators into grounding the graph BEFORE the realizer so
# the realizer can compete as a real surface authority.
realized_plan = realize_semantic(target, graph)
# 27. INGEST / UNDERSTAND / RECALL / THINK / ARTICULATE / LEARN
# Delegated to ChatRuntime.chat().
# ChatResponse is the stable contract surface.
response = self.runtime.chat(text, max_tokens=max_tokens)
# ADR-0088 Phase B (audit Finding 2, 2026-05-20) — opt-in
# grounded realizer. When the runtime opts in, fill the
# graph's <pending> obj slots from the recall step's walk
# tokens (already alphabetic-filtered by ChatRuntime) and
# re-invoke ``realize_semantic`` on the grounded graph. The
# surface resolver (PR #76) then picks the realizer's
# grounded output when it clears ``_is_useful_surface`` and
# the unknown-domain gate did not fire. Default-off
# preserves byte-identity for every existing surface and
# trace_hash — the realizer continues to emit unusable
# placeholders and lose the resolver to the runtime path.
# Comb pass 2026-05-21 — direct attribute access; these fields
# all live on ChatResponse with documented defaults (PR #88 for
# ``realizer_grounded_authority`` + ``recalled_words``, ADR-0048
# for ``grounding_source``, ADR-0077 for
# ``register_canonical_surface``, ADR-0071 for
# ``pre_decoration_surface``). The historical ``getattr`` calls
# were ADR-introduction defensiveness now safe to drop.
# Grounding (when opted in) produces the effective graph for the
# supremacy decision + stored result + topological hash. The
# original intent-derived `graph` is the starting plan; effective
# is the substrate view after recall grounding (when active).
# This ensures the graph carried in CognitiveTurnResult and
# folded into trace_hash reflects the actual reasoning used.
effective_graph = graph
recalled_words = response.recalled_words or ()
# Depth enrichment is now DEFAULT (AC2) for 3-lang mastery on spine.
# ALWAYS attempt per-subject resolution + GraphNode lang/root enrichment
# (independent of is_fully_grounded) so node_depths / graph_anti_unify
# and result fields are populated for both OOV/pending and grounded 3-lang cases.
# The grounded authority flag only controls the recalled_words fill + re-realize.
# Collect unique subjects, resolve with depth packs for language/root/gloss.
# This feeds both recalled_words (for ground_graph) and per-node enrichment.
if effective_graph:
subjects = []
seen = set()
for n in effective_graph.nodes:
s = n.subject.strip().lower()
if s and s not in seen:
seen.add(s)
subjects.append(s)
from chat.pack_resolver import (
DEFAULT_RESOLVABLE_PACK_IDS,
resolve_entry,
resolve_gloss,
resolve_lemma,
)
# Master bidirectional entry point: LexicalResolution carries
# 3-language depth (Hebrew roots, Greek precision) usable for
# graph grounding (comprehension), later realization (articulation),
# and contemplation/reasoning on the shared PropositionGraph.
from chat.pack_resolver import DEPTH_PACK_IDS
depth_pack_ids = DEFAULT_RESOLVABLE_PACK_IDS + DEPTH_PACK_IDS
subject_to_res = {}
for s in subjects:
res = resolve_entry(s, pack_ids=depth_pack_ids)
subject_to_res[s] = res
# Collect glosses for pending nodes in order (feeds ground_graph sequentially)
# (only when not fully grounded, to preserve prior behavior for gloss fill)
if not effective_graph.is_fully_grounded():
recalled_glosses = []
for n in effective_graph.nodes:
obj = n.obj
if obj in (None, "", "<pending>", "<prior>") or (isinstance(obj, str) and "..." in obj):
s = n.subject.strip().lower()
res = subject_to_res.get(s)
if res and getattr(res, 'gloss', None):
recalled_glosses.append(res.gloss)
elif resolve_lemma(n.subject):
# legacy fallback per-subject
g = resolve_gloss(n.subject)
if g:
_, _, gloss_text = g
if gloss_text:
recalled_glosses.append(gloss_text)
if recalled_glosses:
recalled_words = tuple(recalled_glosses)
# Enrich every node with its subject's resolution (subject→node map)
# Immutable; only rebuild if any depth present. ALWAYS for 3-lang depth support.
if subject_to_res:
new_nodes = []
changed = False
for n in effective_graph.nodes:
s = n.subject.strip().lower()
res = subject_to_res.get(s)
if res and (getattr(res, 'language', None) or getattr(res, 'root', None) or getattr(res, 'morphology_id', None)):
enriched = GraphNode(
node_id=n.node_id,
subject=n.subject,
predicate=n.predicate,
obj=n.obj,
source_intent=n.source_intent,
language=getattr(res, 'language', None),
root=getattr(res, 'root', None),
morphology_id=getattr(res, 'morphology_id', None),
)
new_nodes.append(enriched)
changed = True
else:
new_nodes.append(n)
if changed:
effective_graph = PropositionGraph(
nodes=tuple(new_nodes),
edges=effective_graph.edges,
)
if self.runtime.config.realizer_grounded_authority and recalled_words:
# Ground using recalled_words + depth map (alongside) so
# 3-lang info propagates even if not pre-enriched on nodes.
# Flag only gates this recall-fill step for compat.
depth_map = {}
for n in effective_graph.nodes:
if n.language or n.root or n.morphology_id:
depth_map[n.node_id] = (n.language, n.root, n.morphology_id)
grounded_graph = ground_graph(effective_graph, recalled_words, depth=depth_map)
realized_plan = realize_semantic(target, grounded_graph)
effective_graph = grounded_graph
# Physical coherence only (vault_hits bookkeeping is not a gate).
# gate_fired True ⇒ residual failure ⇒ substrate realizer refused.
field_for_gate = self._capture_field_state()
F_gate = getattr(field_for_gate, "F", None) if field_for_gate is not None else None
if F_gate is None and field_state_before is not None:
F_gate = field_state_before.F
contract_assessment = self._geometry_contract_assessment(F_gate)
gate_fired = bool(
contract_assessment.missing_bindings
or contract_assessment.unresolved_hazards
)
canonical = response.register_canonical_surface
pre_decoration = response.pre_decoration_surface
# Comb pass 2026-05-21 — materialize teaching-store triples once
# per turn. Pre-fix both ``_maybe_transitive_walk`` and
# ``_maybe_compose_relations`` called ``self.teaching_store.triples()``
# independently, doubling the per-turn O(N) filter+tuple-build
# cost as the corpus grows.
triples = self.teaching_store.triples()
walk_result: WalkResult | None = self._maybe_transitive_walk(intent, triples)
walk_surface = ""
if walk_result is not None and len(walk_result.path) > 1:
walk_surface = CognitiveTurnPipeline._render_walk_surface(walk_result)
compose_result: FrameComposeResult | None = self._maybe_compose_relations(intent, triples)
compose_surface = ""
if compose_result is not None and (
compose_result.subject_tail is not None
or compose_result.frame_tail is not None
):
compose_surface = CognitiveTurnPipeline._render_compose_surface(compose_result)
# Stage 3C — bounded inductive closure over teaching-store relations.
# Provenance-preserving fixed-point; derived edges require geometric
# admissibility (closed Cl(4,1) versors when vocab can ground them).
def _geom_admissible(h: str, r: str, t: str) -> bool:
del r
hv = self._resolve_surface_versor(h)
tv = self._resolve_surface_versor(t)
if hv is None or tv is None:
# Ungrounded endpoints cannot be promoted as geometric facts.
return False
return (
float(versor_condition(hv)) < 1e-6
and float(versor_condition(tv)) < 1e-6
)
inductive_closure = expand_relation_closure(
triples,
budget=16,
geometric_admissible=_geom_admissible,
)
entailment_trace = self._maybe_entailment_trace(intent, triples)
# === SHADOW COHERENCE GATE WIRING ===
# Dual-competing: substrate supremacy requires fully grounded graph
# AND closed geometric contract (versor_condition + GoldTether residual).
# Grounding provenance (pack/teaching/vault/oov/…) is read once here and
# reused for the OOV telemetry below; the gate consumes it to route an
# open-geometry-but-pack-grounded surface to the hedge arm (T13 dec. 2).
grounding_src = getattr(response, "grounding_source", "") or ""
resolved = resolve_surface(
canonical_surface=canonical,
pre_decoration_surface=pre_decoration,
response_surface=response.surface,
response_articulation_surface=response.articulation_surface,
realized_surface=realized_plan.surface,
realizer_useful=_is_useful_surface(realized_plan.surface),
gate_fired=gate_fired,
walk_surface=walk_surface,
compose_surface=compose_surface,
proposition_graph=effective_graph,
contract_assessment=contract_assessment,
grounding_provenance=grounding_src,
)
surface = resolved.surface
# Truth-path bytes for trace_hash (ADR-0069 inv C): the served
# surface carries register R6/R4 transforms and MUST NOT move the
# hash; hash_surface is the register-invariant canonical-precedence
# capture, kept in lockstep with every served-surface mutation below.
hash_surface = resolved.hash_surface or resolved.surface
articulation_surface = resolved.articulation_surface
authority_source = resolved.authority
# === LOGOS MORPH AUTHORITY (bulk live seam) ===
# Same pure decision function as teaching store + four-arm ablation.
# Executable morph may force abstain/refuse; never soft-pass a
# certified singular-exclusivity claim against observed plural HE.
# English-only turns with no HE surface → no-op (None).
logos_decision_kind = ""
logos_decision_reason = ""
logos_rule_id = ""
logos_constraint_id = ""
logos_decision = None
try:
from generate.observed_he_morph_v0.authority import (
decision_as_coherence_refusal,
evaluate_logos_on_text,
first_logos_constraint,
logos_blocks_certified_answer,
)
logos_decision = evaluate_logos_on_text(text=text, mode="executable")
if logos_decision is not None:
logos_decision_kind = logos_decision.kind.value
logos_decision_reason = logos_decision.reason
logos_rule_id = logos_decision.rule_id or ""
lc = first_logos_constraint(logos_decision)
if lc is not None:
logos_constraint_id = lc.constraint_id
if logos_blocks_certified_answer(logos_decision):
refusal = decision_as_coherence_refusal(logos_decision)
surface = refusal.surface_message or refusal.message
hash_surface = surface
articulation_surface = surface
authority_source = "logos_morph_constraint"
except Exception:
# Pack load / catalog failure must not crash the turn spine;
# English path continues without morph authority.
logos_decision = None
# SUBSTRATE_BYPASS_HAZARD telemetry (data-driven roadmap).
# Only populated when a graph existed yet substrate did not win.
# This is *observability only* — never used to change control flow
# after the fact, never folded into trace_hash in Phase A.
substrate_hazard: tuple[str, ...] = ()
if effective_graph is not None and authority_source not in (
"substrate_realizer",
"realizer",
"logos_morph_constraint",
):
reasons: list[str] = []
if not effective_graph.is_fully_grounded():
reasons.append("unfilled_pending_slots")
# include the exact nodes for precision (Strangler diagnostic)
unresolved = effective_graph.get_unresolved_topology()
if unresolved:
reasons.append(f"unresolved_nodes={unresolved}")
if gate_fired:
reasons.append("unknown_domain_gate_fired")
if not _is_useful_surface(realized_plan.surface):
reasons.append("realizer_surface_not_useful")
substrate_hazard = tuple(reasons)
if logos_decision is not None and logos_blocks_certified_answer(logos_decision):
substrate_hazard = substrate_hazard + (
f"logos_morph_{logos_decision.kind.value}",
logos_decision.reason,
)
# Phase C (Geometric Anti-Unification) — read-only telemetry instrumentation.
# Captures the graph-structural context around any OOV/pending "hole"
# so that a future exact-CGA sub-graph anti-unifier can operate on
# conformal neighbors rather than lexical token match.
# Populated observationally; never affects surface, hash (yet), or
# any durable mutation. Uses only what the substrate already produced.
oov_geometric_context = None
# grounding_src is computed above at the gate-wiring seam and reused here.
has_pending = bool(effective_graph and any(
(n.obj or "") in ("", "<pending>") or "..." in (n.obj or "")
for n in effective_graph.nodes
))
# AC2: node_depths in oov_geometric_context by default for all PropGraph paths
# Use pure build_node_depths for canonical extraction (nid-keyed).
node_depths = build_node_depths(effective_graph.nodes) if effective_graph else {}
if grounding_src == "oov" or has_pending:
# Active conformal neighborhood probe (exact cga_inner over vault).
probe_performed = False
probe_neighbors: list[dict[str, object]] = []
try:
from algebra.backend import cga_inner as _cga_inner
F_probe = getattr(self._capture_field_state(), "F", None)
vault = getattr(getattr(self.runtime, "session", None), "vault", None)
if F_probe is not None and vault is not None and hasattr(vault, "entries"):
scores: list[tuple[float, str]] = []
for entry in list(vault.entries())[:64]:
versor = getattr(entry, "versor", None)
if versor is None and isinstance(entry, (tuple, list)) and entry:
versor = entry[0]
if versor is None:
continue
try:
s = float(_cga_inner(F_probe, versor))
except Exception:
continue
label = str(getattr(entry, "id", "") or getattr(entry, "key", "") or "")
scores.append((s, label))
scores.sort(key=lambda item: item[0], reverse=True)
probe_neighbors = [
{"cga_inner": s, "ref": lab} for s, lab in scores[:5]
]
probe_performed = True
except Exception:
probe_performed = False
oov_geometric_context = {
"unresolved_topology": effective_graph.get_unresolved_topology() if effective_graph else (),
"intent_tag": getattr(intent, "tag", None).value if intent and getattr(intent, "tag", None) else "unknown",
"geometric_probe_performed": probe_performed,
"conformal_neighbors": probe_neighbors,
"note": "Conformal anti-unification probe: vault neighbors via exact cga_inner.",
"node_depths": node_depths,
}
else:
# default for PropGraph: at least node_depths
if effective_graph:
oov_geometric_context = {
"node_depths": node_depths,
"note": "default depth context for PropGraph (AC2)",
}
# Phase 4: include graph-level anti-unify result (unresolved topo + roots) for observability on spine.
if node_depths and effective_graph:
try:
from recognition.anti_unifier import graph_anti_unify
topo = tuple(n.node_id for n in effective_graph.nodes)
if oov_geometric_context is None:
oov_geometric_context = {}
oov_geometric_context["graph_anti_unify"] = graph_anti_unify(topo, node_depths)
except Exception:
# Best-effort telemetry only; anti-unify failure must not affect main path.
pass
# Capture depths (post-enrich) to attrs for recognize chaining (AC1) + runtime contemplate depth= (real).
# Propagation contract (3-lang depth on PropGraph spine): pipeline (after OOV/PropGraph construction)
# writes _last_node_depths (and now also top-level on CognitiveTurnResult) so that
# runtime.contemplate(...) and teaching/contemplation paths forward depth= without
# re-resolving packs. Depths originate from pack_resolver + build_node_depths on GraphNode.
# This is the current minimal cross-component channel (observational). See docs/ and review.
if node_depths:
self._current_node_depths = node_depths
if effective_graph and effective_graph.nodes:
self._current_agent_node_id = effective_graph.nodes[0].node_id
self._last_node_depths = node_depths
# Direct assignment now that runtime explicitly declares the attr (cleanup of hasattr/setattr dance)
self.runtime._last_node_depths = node_depths
# Track last node id for correction-intent chaining
if graph.nodes:
self._last_node_id = graph.nodes[-1].node_id
# 8. CAPTURE post-turn field state
field_state_after: FieldState = self.runtime.session.state
# 9. Reconstruct input-layer tokens from the turn log
# (turn_log is appended inside chat(); last entry matches this turn)
# When the unknown-domain gate fires, chat() returns a stub without
# appending to turn_log — fall back to raw_tokens (set at step 0).
if self.runtime.turn_log:
last_turn = self.runtime.turn_log[-1]
filtered_tokens = last_turn.input_tokens
else:
filtered_tokens = raw_tokens
# 9b. ARTICULATE STATUS — if any prior turn produced a SPECULATIVE
# teaching proposal whose subject is referenced by the current
# input (subject substring or reflexive query shape), prepend a
# status marker so the user can distinguish reviewed knowledge
# from unreviewed teaching material. ADR-0021 §Articulation.
# Decision uses subjects seeded by prior turns; this turn's own
# proposal (if any) is added below for FUTURE turns to see.
if self._speculative_subjects and surface and self._should_mark_speculative(text, surface):
surface = _SPECULATIVE_SURFACE_MARKER + surface
hash_surface = _SPECULATIVE_SURFACE_MARKER + hash_surface
articulation_surface = _SPECULATIVE_SURFACE_MARKER + articulation_surface
# 10. TEACHING — correction capture, review, and store
teaching_candidate, reviewed_example, proposal = self._run_teaching(
text, intent, self._turn_number,
identity_score=response.identity_score,
)
# 10b. TRACK SPECULATIVE SUBJECTS — seed the marker decision for
# future turns. Done AFTER the marker check above so the teach
# turn itself does not self-mark; only subsequent probes do.
# Prefer the parsed-triple subject (clean: "truth") over the raw
# proposal.subject (often a fragment of the correction text);
# also split-and-add each ≥4-char token so prefixed parses like
# "correction: wisdom" still match a probe about "wisdom".
if proposal is not None:
sources: list[str] = []
if proposal.triple is not None and proposal.triple[0]:
sources.append(proposal.triple[0])
if proposal.subject:
sources.append(proposal.subject)
if proposal.epistemic_status is EpistemicStatus.SPECULATIVE:
for src in sources:
self._remember_speculative_subject(src)
for tok in _SUBJECT_SPLIT_RE.split(src.lower()):
if len(tok) >= 4 and tok not in _SUBJECT_STOPWORDS:
self._remember_speculative_subject(tok)
elif proposal.epistemic_status is EpistemicStatus.COHERENT:
# Finding 5 (audit 2026-05-20) — once teaching review
# promotes a proposal to COHERENT, the subject is no
# longer speculative; evict its tokens so the marker
# stops appearing on subsequent probes about it.
for src in sources:
self._forget_speculative_subject(src)
for tok in _SUBJECT_SPLIT_RE.split(src.lower()):
if len(tok) >= 4 and tok not in _SUBJECT_STOPWORDS:
self._forget_speculative_subject(tok)
# Advance turn counter and remember surface for next correction binding.
# The truth-path surface (not the served, register-decorated bytes)
# feeds correction binding so register packs cannot perturb teaching.
self._turn_number += 1
self._prior_surface = hash_surface
# 11. TRACE — deterministic hash (includes teaching IDs and any
# typed-operator invocation per ADR-0018).
review_hash = reviewed_example.review_hash if reviewed_example is not None else ""
proposal_id = proposal.proposal_id if proposal is not None else ""
epistemic_status = proposal.epistemic_status.value if proposal is not None else ""
walk_serialised = CognitiveTurnPipeline._serialize_operator(walk_result)
compose_serialised = CognitiveTurnPipeline._serialize_operator(compose_result)
entailment_serialised = CognitiveTurnPipeline._serialize_entailment_trace(
entailment_trace
)
# Stage 3C — inductive fixed-point provenance (before hash so replay
# includes multi-step derivation when teaching store has chains).
inductive_serialised = ""
if inductive_closure.derived or inductive_closure.contradictions:
inductive_serialised = json.dumps(
{
"inductive_closure": {
"n_derived": len(inductive_closure.derived),
"n_contradictions": len(inductive_closure.contradictions),
"steps_taken": inductive_closure.steps_taken,
"fixed_point": inductive_closure.fixed_point,
"truncated": inductive_closure.truncated,
"derived": [d.as_dict() for d in inductive_closure.derived[:8]],
}
},
sort_keys=True,
ensure_ascii=False,
)
# Deterministic concatenation: walk, compose, entailment, inductive.
# Empty strings are dropped so unaffected turns keep existing trace bytes.
operator_invocation = "|".join(
s
for s in (
walk_serialised,
compose_serialised,
entailment_serialised,
inductive_serialised,
)
if s
)
# ADR-0023 — admissibility trace + ratification provenance.
# Comb pass 2026-05-21 — direct attribute access; the fields
# are dataclass-defaulted on ChatResponse, so the prior
# ``getattr`` guard was dead defensiveness from the ADR
# introduction window.
admissibility_trace = response.admissibility_trace
region_was_unconstrained = response.region_was_unconstrained
admissibility_trace_hash = hash_admissibility_trace(admissibility_trace)
# Geometric ratification outcomes only (ratified | demoted).
ratification_outcome = ratified.outcome.value
_trace_ratification_outcome = ratification_outcome
# ADR-0024 Phase 2 + W-011 — refusal_reason precedence:
# recognition wins (earlier-fail boundary) over generation.
_generation_refusal_reason = getattr(response, "refusal_reason", "") or ""
refusal_reason = _recognition_refusal_reason or _generation_refusal_reason
# Logos morph abstain/fail-closed is answer-authority: surface the reason
# when morph blocked certification (does not override recognition refuse).
if not refusal_reason and logos_decision_kind in ("abstain", "fail_closed"):
refusal_reason = logos_decision_reason
# Phase B — Quantized Topological Hashing for the cognitive spine.
# Include the discrete (string-only) topological form of the
# PropositionGraph that was produced for this turn. This is the
# Merkle-DAG of the "think" step (nodes + directed relations).
# - Uses graph.to_json() which is canonical, sort_keys JSON of
# the DAG (no floats, no geometry).
# - Conditional on presence (already is) but the key is only added
# in compute_trace_hash when non-empty, preserving byte identity
# for any legacy pre-inclusion behavior in other contexts.
# - Addresses the Trace Equivalence Hazard: same topology on any
# hardware/backend produces identical contribution to the SHA.
# The continuous versor_condition (rounded) is kept only as the
# runtime guard; actual geometric state lives in field/vault and
# is never raw-hashed here.
graph_topo = effective_graph.to_json() if effective_graph is not None else ""
trace_hash = compute_trace_hash(
input_text=text,
filtered_tokens=filtered_tokens,
surface=hash_surface,
walk_surface=response.walk_surface,
articulation_surface=articulation_surface,
dialogue_role=str(response.dialogue_role),
versor_condition=response.versor_condition,
vault_hits=response.vault_hits,
intent_tag=intent.tag.value,
teaching_review_hash=review_hash,
teaching_proposal_id=proposal_id,
teaching_epistemic_status=epistemic_status,
operator_invocation=operator_invocation,
admissibility_trace_hash=admissibility_trace_hash,
ratification_outcome=_trace_ratification_outcome,
region_was_unconstrained=region_was_unconstrained,
refusal_reason=refusal_reason,
proposition_graph=graph_topo,
)
# ADR-0153 (W-020a) — back-stamp the canonical trace_hash onto
# the runtime's most-recent TurnEvent and any DiscoveryCandidate
# emitted during this turn. The runtime cannot compute
# trace_hash itself (the pipeline owns ``compute_trace_hash``),
# so candidates were previously persisted with empty
# ``source_turn_trace``. See runtime.finalize_turn_trace_hash.
self.runtime.finalize_turn_trace_hash(trace_hash)
# Weekly-audit T13 (2026-07-22) — back-stamp the pipeline's final
# *served* surface onto the same TurnEvent, sibling to the
# trace_hash back-stamp above. ``resolve_surface`` + the logos-morph
# authority (above) can override the runtime-owned surface AFTER
# ``runtime.chat`` sealed the TurnEvent, so without this the
# telemetry record disagrees with the served bytes
# (warmed_session_consistency telemetry_consistency_rate < 1.0;
# breaks audit/replay trust — Absolute Provenance).
self.runtime.finalize_turn_surface(surface, articulation_surface)
# B4 producer: capture the leeway the response path already decided —
# observational only (reads the runtime's introspection-only accrual and
# the response's reach level; never alters the surface, never gates).
accrual = (
self.runtime.last_turn_accrual()
if hasattr(self.runtime, "last_turn_accrual")
else None
)
leeway = build_leeway_record(
reach_level=str(getattr(response, "reach_level", "strict") or "strict"),
license_decision=getattr(accrual, "license", None),
)
# Stage 3A — turn-level geometric coherence (orthogonal to vault COHERENT).
geo_F = F_gate
if geo_F is None and field_state_after is not None:
geo_F = getattr(field_state_after, "F", None)
geometric_coherence = evaluate_geometric_coherence(
geo_F,
identity_score=response.identity_score,
)
return CognitiveTurnResult(
input_text=text,
input_tokens=raw_tokens,
filtered_tokens=filtered_tokens,
field_state_before=field_state_before,
field_state_after=field_state_after,
proposition=response.proposition,
articulation=response.articulation,
surface=surface,
hash_surface=hash_surface,
walk_surface=response.walk_surface,
articulation_surface=articulation_surface,
dialogue_role=response.dialogue_role,
identity_score=response.identity_score,
vault_hits=response.vault_hits,
recall_energy_class=response.recall_energy_class,
intent=intent,
# Use effective_graph (the post-grounding view when substrate
# grounding was active) so that the stored PropositionGraph
# reflects what the Shadow Coherence Gate / realizer actually
# used for articulation. This makes result.proposition_graph
# + trace hash (via topo) consistent with the executed spine.
# For the common case (no grounding) this is identical to the
# intent-derived graph.
proposition_graph=effective_graph,
articulation_target=target,
teaching_candidate=teaching_candidate,
reviewed_teaching_example=reviewed_example,
pack_mutation_proposal=proposal,
operator_invocation=operator_invocation,
admissibility_trace=admissibility_trace,
admissibility_trace_hash=admissibility_trace_hash,
ratification_outcome=ratification_outcome,
region_was_unconstrained=region_was_unconstrained,
refusal_reason=refusal_reason,
epistemic_graph=epistemic_graph,
dispatch_trace=getattr(response, "dispatch_trace", None),
dropped_compound_clauses=dropped_compound_clauses,
versor_condition=response.versor_condition,
geometric_coherence=geometric_coherence,
trace_hash=trace_hash,
leeway=leeway,
# Phase A — Shadow Coherence Gate observability.
# authority_source makes the winner of the substrate vs legacy
# decision first-class evidence (visible in trace, workbench,
# evals). substrate_hazard is the precise bypass signal.
# Logos morph may override to "logos_morph_constraint" when it
# blocks certified answers (same decision fn as teaching/ablation).
authority_source=authority_source,
substrate_hazard=substrate_hazard,
oov_geometric_context=oov_geometric_context,
# 3-lang depth unification: surface the same data at top level on result
# (extracted from pre-computed node_depths var or oov_geometric_context to keep single source)
node_depths=node_depths if node_depths else None,
graph_anti_unify=(oov_geometric_context or {}).get("graph_anti_unify") if oov_geometric_context else None,
logos_decision_kind=logos_decision_kind,
logos_decision_reason=logos_decision_reason,
logos_rule_id=logos_rule_id,
logos_constraint_id=logos_constraint_id,
)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _compile_turn_wave_packet(self, tokens: tuple[str, ...] | list[str]) -> FieldState:
"""Compile turn tokens into an initial Cl(4,1) field on the manifold.
Uses the session inject/probe path (holonomy encode + normalize_to_versor
at the owned ingest boundary). Does not replace session state when
probe_ingest is available (chat() still owns commit).
"""
session = getattr(self.runtime, "session", None)
if session is None:
raise RuntimeError(
"CognitiveTurnPipeline cannot compile a wave-packet without a session"
)
token_list = [str(t) for t in tokens]
if not token_list:
token_list = ["_empty_"]
if hasattr(session, "probe_ingest"):
return session.probe_ingest(token_list)
from ingest.gate import inject
vocab = getattr(session, "vocab", None)
if vocab is None:
raise RuntimeError(
"CognitiveTurnPipeline cannot compile a wave-packet without session.vocab"
)
return inject(token_list, vocab)
@staticmethod
def _geometry_contract_assessment(F):
"""Build contract assessment from active versor + GoldTether residuals.
Closed only when versor_condition(F) < 1e-6 and R_GoldTether ≤ 1e-6.
Local import avoids chat → cognition → problem_frame_contracts cycles
(problem_frame_contracts imports chat.pack_resolver).
"""
from generate.problem_frame_contracts import ContractAssessment
if F is None:
return ContractAssessment(
candidate_organ="shadow_coherence_gate",
missing_bindings=("missing_wave_field",),
unresolved_hazards=(),
runnable=False,
explanation="no field versor available for geometric contract",
)
vc = float(versor_condition(F))
r_gt = float(coherence_residual(F))
missing: list[str] = []
hazards: list[str] = []
if vc >= 1e-6:
missing.append("versor_condition")
if r_gt > 1e-6:
hazards.append("goldtether_residual")
return ContractAssessment(
candidate_organ="shadow_coherence_gate",
missing_bindings=tuple(missing),
unresolved_hazards=tuple(hazards),
runnable=not missing and not hazards,
explanation=(
f"versor_condition={vc:.3e}; R_GoldTether={r_gt:.3e}"
),
)
def _ratify_intent(self, intent, field_state):
"""Field-ratify a seeded intent (ADR-0022 §TBD-1).
Geometric Sovereignty: field must already be compiled (see :meth:`run`);
vocab and prompt versor are required for conformal ratification.
"""
if field_state is None or getattr(field_state, "F", None) is None:
raise RuntimeError(
"intent ratification requires a compiled Cl(4,1) field state"
)
session = getattr(self.runtime, "session", None)
vocab = getattr(session, "vocab", None) if session is not None else None
if vocab is None:
raise RuntimeError(
"intent ratification requires session.vocab"
)
prompt_versor = field_state.F
return ratify_intent(intent, prompt_versor, vocab=vocab)
def _remember_speculative_subject(self, subject: str) -> None:
"""Add (or refresh LRU position of) a speculative subject token.
Finding 5 (audit 2026-05-20). Caps the cache at
``_MAX_SPECULATIVE_SUBJECTS`` via insertion-order eviction.
Empty / whitespace-only inputs are dropped silently so callers
can pass raw fragments without guarding.
"""
subject = subject.lower().strip()
if not subject:
return
self._speculative_subjects.pop(subject, None)
self._speculative_subjects[subject] = None
while len(self._speculative_subjects) > _MAX_SPECULATIVE_SUBJECTS:
self._speculative_subjects.popitem(last=False)
def _forget_speculative_subject(self, subject: str) -> None:
"""Evict a subject from the speculative-marker cache.
Called when a SPECULATIVE proposal is promoted to COHERENT via
the teaching review loop, so reviewed material stops being
marked speculative on later probes. No-op if the subject is
not present.
"""
subject = subject.lower().strip()
if subject:
self._speculative_subjects.pop(subject, None)
def _should_mark_speculative(self, text: str, surface: str) -> bool:
"""Decide whether ``surface`` should carry the SPECULATIVE marker.
Triggers when the input references a subject of a prior SPECULATIVE
teaching proposal (by substring match) or carries a reflexive query
shape (e.g. "is your answer about X confirmed?"). Already-marked
surfaces are not double-marked.
"""
surface_lower = surface.lower()
if "speculative" in surface_lower or "not yet reviewed" in surface_lower:
return False
text_lower = text.lower()
for subj in self._speculative_subjects:
if subj and subj in text_lower:
return True
for marker in _REFLEXIVE_PROBE_MARKERS:
if marker in text_lower:
return True
return False
def _run_teaching(
self,
text: str,
intent: object,
turn_number: int,
*,
identity_score: object = None,
) -> tuple[
CorrectionCandidate | None,
ReviewedTeachingExample | None,
PackMutationProposal | None,
]:
"""Run correction capture → review → store if this turn is a CORRECTION.
``identity_score`` is the trajectory's projection onto the runtime
IdentityManifold (already computed by ChatRuntime for this turn); the
review gate uses it as a geometric (paraphrase-invariant) defense
layer alongside the syntactic check.
"""
if self._prior_surface is None:
return None, None, None
candidate = extract_correction(
correction_text=text,
intent=intent, # type: ignore[arg-type]
prior_surface=self._prior_surface,
prior_turn=turn_number - 1,
)
if candidate is None:
return None, None, None
manifold = getattr(self.runtime, "identity_manifold", None)
# ADR-0244 honest scope: with ``identity_wave_gate`` off, live
# final_state.F scores routinely show high leakage and axis inversion
# because value axes are not yet dynamically load-bearing. Those
# measures are observational telemetry, not teaching veto authority.
# Only committed ``boundary_violations`` (safety/ethics ∩ manifold)
# remain a hard geometric teaching reject while the gate is off.
# Syntactic identity-override detection remains active regardless.
review_score = identity_score
cfg = getattr(self.runtime, "config", None)
gate_on = bool(getattr(cfg, "identity_wave_gate", False))
if (
not gate_on
and identity_score is not None
and bool(getattr(identity_score, "wave_mode_active", False))
and not bool(getattr(identity_score, "boundary_violations", ()) or ())
):
review_score = None
reviewed = review_correction(
candidate,
identity_score=review_score, # type: ignore[arg-type]
identity_manifold=manifold if review_score is not None else None,
)
proposal = self.teaching_store.add(reviewed)
return candidate, reviewed, proposal
def _maybe_transitive_walk(
self,
intent,
triples: tuple[tuple[str, str, str], ...] | None = None,
) -> WalkResult | None:
"""Invoke a typed deterministic walk operator when the intent shape
calls for it (ADR-0018).
Dispatch order, by precision:
1. Relation-typed `transitive_walk` if the intent carries a
relation and a same-relation chain exists from the head.
2. Cross-relation `multi_relation_walk` fallback when (1)
returns a singleton — this is what closes the
mixed_relation / composed_predicate residuals.
DEFINITION intents only attempt step 1 with the implicit "is"
relation; they do not fall back to a multi-relation walk
(which would be too permissive for plain "What is X?").
``triples`` may be passed in to avoid a second
``teaching_store.triples()`` materialization per turn (comb
pass 2026-05-21); when omitted, falls back to the live store.
"""
if triples is None:
triples = self.teaching_store.triples()
if not triples:
return None
if intent.tag is IntentTag.TRANSITIVE_QUERY and intent.relation:
result = transitive_walk(triples, intent.subject, intent.relation)
if len(result.path) > 1:
return result
multi = multi_relation_walk(triples, intent.subject)
if len(multi.path) > 1:
return multi
return None
if intent.tag is IntentTag.DEFINITION:
result = transitive_walk(triples, intent.subject, "is")
if len(result.path) > 1:
return result
return None
def _maybe_compose_relations(
self,
intent,
triples: tuple[tuple[str, str, str], ...] | None = None,
) -> FrameComposeResult | None:
"""Invoke ``compose_relations`` when the intent is a frame-transfer
probe ("What does X R in Y?") and the teaching store carries at
least one R-edge. Returns the typed result; the caller folds
non-None tails into the surface.
``triples`` may be passed in to avoid a second
``teaching_store.triples()`` materialization per turn (comb
pass 2026-05-21).
"""
if intent.tag is not IntentTag.FRAME_TRANSFER:
return None
if not intent.relation or not intent.frame:
return None
if triples is None:
triples = self.teaching_store.triples()
if not triples:
return None
return compose_relations(
triples,
head=intent.subject,
frame=intent.frame,
relation=intent.relation,
)
def _maybe_entailment_trace(
self,
intent,
triples: tuple[tuple[str, str, str], ...],
) -> EntailmentTrace | None:
"""Compile exact verification triples into propositional entailment.
Telemetry-only v1: the result is folded into ``operator_invocation`` and
never changes the user-facing surface. Runs only when classification
exposes a precise positive ``subject relation object`` shape.
Atoms are content-addressed by Cl(4,1) versor digests and unified by
conformal reverse-product score (no string ``atom_`` join).
"""
if intent.tag is not IntentTag.VERIFICATION:
return None
if intent.negated or not intent.relation or not intent.object:
return None
registry: list[tuple[str, np.ndarray]] = []
head = self._proof_atom(intent.subject, registry)
tail = self._proof_atom(intent.object, registry)
if not head or not tail:
return None
relation = intent.relation.strip().lower()
premises: list[str] = []
for h, r, t in triples:
if r.strip().lower() != relation:
continue
h_atom = self._proof_atom(h, registry)
t_atom = self._proof_atom(t, registry)
if h_atom and t_atom:
premises.append(f"{h_atom} -> {t_atom}")
if not premises:
return None
return evaluate_entailment_with_trace(tuple(premises), f"{head} -> {tail}")
@staticmethod
def _render_compose_surface(compose: FrameComposeResult) -> str:
"""Render a frame-transfer composition suffix without selecting authority."""
parts: list[str] = []
if compose.subject_tail is not None:
parts.append(
f"{compose.head} {compose.relation.replace('_', ' ')} {compose.subject_tail}"
)
if compose.frame_tail is not None:
parts.append(
f"in {compose.frame} {compose.relation.replace('_', ' ')} {compose.frame_tail}"
)
return "; ".join(parts)
# Comb pass 2026-05-21 — removed dead ``_fold_compose_into_surface``
# (no live callers since PR #76 routed all surface composition
# through the explicit ``resolve_surface`` policy). The render
# helper above is still consumed by the resolver path.
@staticmethod
def _serialize_operator(op: WalkResult | FrameComposeResult | None) -> str:
"""Deterministic operator-invocation serialisation for trace_hash.
Comb pass 2026-05-21 — collapsed the parallel ``_serialize_walk`` /
``_serialize_compose`` helpers into one. Both operators expose
``as_dict()`` and serialise identically.
"""
if op is None:
return ""
return json.dumps(op.as_dict(), sort_keys=True, ensure_ascii=False)
@staticmethod
def _serialize_entailment_trace(trace: EntailmentTrace | None) -> str:
if trace is None:
return ""
return f"entailment:{evidence_from_entailment_trace(trace).canonical_json()}"
def _resolve_surface_versor(self, text: str) -> np.ndarray | None:
"""Resolve surface text to a vocab-grounded Cl(4,1) versor, or None."""
session = getattr(self.runtime, "session", None)
vocab = getattr(session, "vocab", None) if session is not None else None
if vocab is None or not text:
return None
tokens = [p for p in _SUBJECT_SPLIT_RE.split(text.lower()) if p]
# Prefer last non-stopword content token (subject-like), then any token.
ordered = [t for t in reversed(tokens) if t not in _SUBJECT_STOPWORDS] + tokens
for token in ordered:
try:
return np.asarray(vocab.get_versor(token), dtype=np.float64)
except (KeyError, AttributeError):
continue
return None
@staticmethod
def _unify_score(a: np.ndarray, b: np.ndarray) -> float:
"""⟨a, ~b⟩_0 — scalar part of geometric product with reversion."""
return float(scalar_part(geometric_product(a, reverse(b))))
@classmethod
def _atoms_unify(cls, a: np.ndarray, b: np.ndarray) -> bool:
"""True when a and b are the same geometric atom under reverse-product.
Requires mutual score to match both self-products within ``_UNIFY_EPS``
(relative when |self| ≥ 1, absolute otherwise). Exact component match
short-circuits. Distinct pack versors with large cross-inner do not unify.
"""
if a.shape != b.shape:
return False
if np.allclose(a, b, rtol=0.0, atol=1e-9):
return True
sa = cls._unify_score(a, a)
sb = cls._unify_score(b, b)
sab = cls._unify_score(a, b)
scale = max(1.0, abs(sa), abs(sb))
tol = _UNIFY_EPS * scale
return abs(sab - sa) <= tol and abs(sab - sb) <= tol
def _proof_atom(
self,
text: str,
registry: list[tuple[str, np.ndarray]] | None = None,
) -> str:
"""Content-addressed conformal atom id for entailment telemetry.
Two surfaces unify to the same atom iff their grounded versors match
under :meth:`_atoms_unify`. Ungrounded surfaces fail closed (empty id).
"""
psi = self._resolve_surface_versor(text)
if psi is None:
return ""
if registry is not None:
for atom_id, prior in registry:
if self._atoms_unify(psi, prior):
return atom_id
digest = multivector_content_digest(psi)
atom_id = f"atom_{digest}"
if registry is not None:
registry.append((atom_id, psi.copy()))
return atom_id
@staticmethod
def _render_walk_surface(walk: WalkResult) -> str:
"""Render a chain-aware walk suffix without selecting authority."""
chain = " ".join(walk.path)
endpoint = walk.path[-1]
return (
f"{walk.head} {walk.relation.replace('_', ' ')} {endpoint} "
f"(via {chain})"
)
@staticmethod
def _fold_walk_into_surface(
walk: WalkResult,
surface: str,
articulation_surface: str,
) -> tuple[str, str]:
"""Compose a chain-aware surface from a non-trivial walk result.
Deterministic. Replay-safe: identical (walk, prior surfaces) produce
identical output. The chain endpoint is the load-bearing token for
the inference-closure / multi-step-reasoning eval lanes.
"""
chain_surface = CognitiveTurnPipeline._render_walk_surface(walk)
if surface:
new_surface = f"{surface}{chain_surface}"
else:
new_surface = chain_surface
if articulation_surface:
new_articulation = f"{articulation_surface}{chain_surface}"
else:
new_articulation = chain_surface
return new_surface, new_articulation
def _capture_field_state(self) -> FieldState | None:
"""Return current session field state, or None if not yet initialised."""
try:
state = self.runtime.session.state
# SessionContext.state may be None before the first ingest
return state if state is not None else None
except AttributeError:
return None