core/session/graph.py
Shay c1e723f185 feat: integrate 3-core-language depth into PropositionGraph spine for bidirectional unification
- Add LexicalResolution dataclass + resolve_entry() in chat/pack_resolver.py
  that returns language, root, morphology_id, gloss, semantic_domains from
  he/grc/en packs (lru-cached, first-match, full depth support).

- Extend GraphNode (generate/graph_planner.py) with optional language/root/
  morphology_id fields (defaults preserve all call sites). Update as_dict()
  to include them conditionally. ground_graph() now propagates depth.

- Generalize enrichment in core/cognition/pipeline.py:
  - Per-subject resolution map using depth packs.
  - Enrich all matching nodes before ground (subject→node map).
  - Pass depth alongside recalled_words to ground_graph().

- Consume depth on articulation side:
  - realize_semantic() and render_semantic() now accept/use language+root
    for etymological/Logos framing on Hebrew/Greek nodes (e.g. "אמת (Hebrew
    root: א-מ-ן) is defined as..."). English unchanged.

- Enrich oov_geometric_context with node_depths for future geometric
  anti-unification using roots.

- Extend recognition/connector.py to forward depth from EpistemicNode
  paths into GraphNode.

- Add full Hebrew turn test under realizer_grounded_authority flag.
- Update related tests (semantic realizer, OOV context, surface resolution).
- Cleaned legacy type() hack immediately on discovery (hard-stop rule).

All targeted tests green (52+ in slices), broad relevant suite 581 passed.
Invariants preserved: versor only at owned boundaries, exact recall,
immutable updates, no new legacy parsers. 3 pillars upheld.

Work continues tomorrow from this checkpoint.
2026-07-06 09:01:43 -07:00

177 lines
6.1 KiB
Python

"""
session/graph.py — SessionGraph
Append-only DAG of dialogue turns. Backward edges point from a turn to prior
turns whose output was consumed as a referent during ingest. Correction passes
walk those edges with true BFS distance, not traversal ordinal.
"""
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
from typing import Any, Sequence
import numpy as np
from core.array_codec import decode_array, encode_array
@dataclass(slots=True)
class TurnNode:
turn_idx: int
input_versor: np.ndarray
output_versor: np.ndarray
tokens_in: tuple[str, ...]
tokens_out: tuple[str, ...]
dialogue_role: str
referent_slots: dict[str, int]
backward_edges: list[int] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"turn_idx": int(self.turn_idx),
"input_versor": encode_array(self.input_versor),
"output_versor": encode_array(self.output_versor),
"tokens_in": list(self.tokens_in),
"tokens_out": list(self.tokens_out),
"dialogue_role": self.dialogue_role,
"referent_slots": dict(self.referent_slots),
"backward_edges": list(self.backward_edges),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "TurnNode":
return cls(
turn_idx=int(payload["turn_idx"]),
input_versor=decode_array(payload["input_versor"]),
output_versor=decode_array(payload["output_versor"]),
tokens_in=tuple(payload["tokens_in"]),
tokens_out=tuple(payload["tokens_out"]),
dialogue_role=payload["dialogue_role"],
referent_slots=dict(payload["referent_slots"]),
backward_edges=list(payload["backward_edges"]),
)
def copy_with_output(
self,
new_output_versor: np.ndarray,
new_tokens_out: tuple[str, ...] | None = None,
) -> "TurnNode":
return TurnNode(
turn_idx=self.turn_idx,
input_versor=self.input_versor.copy(),
output_versor=np.asarray(new_output_versor, dtype=np.float32).copy(),
tokens_in=self.tokens_in,
tokens_out=new_tokens_out if new_tokens_out is not None else self.tokens_out,
dialogue_role=self.dialogue_role,
referent_slots=dict(self.referent_slots),
backward_edges=list(self.backward_edges),
)
class SessionGraph:
"""Append-only directed graph of TurnNodes indexed by turn_idx."""
def __init__(self) -> None:
self._nodes: list[TurnNode] = []
def add_turn(
self,
turn_idx: int,
input_versor: np.ndarray,
output_versor: np.ndarray,
tokens_in: Sequence[str],
tokens_out: Sequence[str],
dialogue_role: str,
referent_slots: dict[str, int] | None = None,
backward_edges: list[int] | None = None,
) -> TurnNode:
clean_edges = [
int(edge)
for edge in dict.fromkeys(backward_edges or [])
if 0 <= int(edge) < turn_idx
]
node = TurnNode(
turn_idx=turn_idx,
input_versor=np.asarray(input_versor, dtype=np.float32).copy(),
output_versor=np.asarray(output_versor, dtype=np.float32).copy(),
tokens_in=tuple(tokens_in),
tokens_out=tuple(tokens_out),
dialogue_role=dialogue_role,
referent_slots=dict(referent_slots or {}),
backward_edges=clean_edges,
)
if turn_idx != len(self._nodes):
raise ValueError(
f"turn_idx must append monotonically: got {turn_idx}, expected {len(self._nodes)}"
)
self._nodes.append(node)
return node
def update_output(
self,
turn_idx: int,
new_output_versor: np.ndarray,
new_tokens_out: tuple[str, ...] | None = None,
) -> TurnNode:
node = self._nodes[turn_idx]
updated = node.copy_with_output(new_output_versor, new_tokens_out)
self._nodes[turn_idx] = updated
return updated
def node_at(self, turn_idx: int) -> TurnNode:
return self._nodes[turn_idx]
def all_nodes(self) -> list[TurnNode]:
return list(self._nodes)
def predecessors_of(self, turn_idx: int) -> list[TurnNode]:
node = self._nodes[turn_idx]
return [self._nodes[i] for i in node.backward_edges if i < len(self._nodes)]
def successors_of(self, turn_idx: int) -> list[TurnNode]:
return [node for node in self._nodes if turn_idx in node.backward_edges]
def backward_walk(
self,
from_turn: int,
max_depth: int = 16,
) -> list[tuple[int, TurnNode]]:
"""
BFS backward walk following backward_edges.
Returns ``(distance, node)`` tuples in BFS order, excluding from_turn.
Multiple nodes at the same graph depth preserve the same distance.
"""
if from_turn < 0 or from_turn >= len(self._nodes):
raise IndexError(f"from_turn out of range: {from_turn}")
visited: set[int] = {from_turn}
queue: deque[tuple[int, int]] = deque((1, idx) for idx in self._nodes[from_turn].backward_edges)
result: list[tuple[int, TurnNode]] = []
while queue:
distance, idx = queue.popleft()
if distance > max_depth:
continue
if idx in visited or idx >= len(self._nodes) or idx < 0:
continue
visited.add(idx)
node = self._nodes[idx]
result.append((distance, node))
queue.extend((distance + 1, parent) for parent in node.backward_edges)
return result
def __len__(self) -> int:
return len(self._nodes)
def __repr__(self) -> str:
return f"SessionGraph(turns={len(self._nodes)})"
def to_dict(self) -> dict[str, Any]:
return {"nodes": [n.to_dict() for n in self._nodes]}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "SessionGraph":
graph = cls()
graph._nodes = [TurnNode.from_dict(n) for n in payload["nodes"]]
return graph