- Remove retired HANDOFF-antigravity-2026-07-01.md stray. - Harden enrich_assessments_with_depth (remove fragile __dict__ fallback; best-effort only). - Explicit _last_node_depths in ChatRuntime; clean direct propagation from pipeline (remove hasattr/setattr dance + bare excepts). - Add clarifying propagation contract comments. - Update plan.md with cleanup section. - All targeted tests + SHA/claims checks green post-edit. - Tree clean, ready to roll. Addresses remaining MEDIUM items from code-reviewer run.
119 lines
4.4 KiB
Python
119 lines
4.4 KiB
Python
"""Pure depth canonicalization helpers for 3-lang root-aware unification.
|
|
|
|
Extracted per strategy to enable slot-precise, node_id-keyed canonicalization
|
|
without heuristics in callers. All functions pure, side-effect free, immutable.
|
|
|
|
Used by derive_recognizer, recognize (for matching), and assessment enrichment.
|
|
|
|
Connects to cognitive path: listen/comprehend (depth from packs) -> think (anti-unif canonical) -> articulate.
|
|
Preserves exact recall, no drift repair.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
from typing import Any, Sequence, Tuple
|
|
|
|
from recognition.outcome import EvidenceSpan, FeatureBundle
|
|
|
|
# For type in tests, import GraphNode when needed
|
|
try:
|
|
from generate.graph_planner import GraphNode
|
|
except ImportError:
|
|
GraphNode = Any # type: ignore
|
|
|
|
|
|
def canonicalize_token(token: str, node_id: str | None, depths: dict[str, dict] | None) -> str:
|
|
"""Wrap root_normalize keyed on node_id from depths dict.
|
|
|
|
depths: {node_id: {"language": , "root": , ...}, ...}
|
|
If node_id in depths and lang he/grc and root, return root, else token.
|
|
Caller must pass the token associated with that node_id.
|
|
"""
|
|
if not depths or not node_id:
|
|
return token
|
|
d = depths.get(node_id)
|
|
if not d:
|
|
return token
|
|
lang = d.get("language")
|
|
root = d.get("root")
|
|
if lang in ("he", "grc") and root:
|
|
return root
|
|
return token
|
|
|
|
|
|
def canonicalize_agent_slot(
|
|
tokens: Sequence[str], bundle: FeatureBundle | None, depths: dict[str, dict] | None,
|
|
*, agent_node_id: str | None = None, start_idx: int | None = None
|
|
) -> Tuple[str, ...]:
|
|
"""Return copy of tokens with agent slot canonicalized using EvidenceSpan.start + node_id if available.
|
|
|
|
Single lookup by agent_node_id (node-keyed, requires explicit nid or no-op; no first-key proxy).
|
|
If bundle use its start, else start_idx.
|
|
Pure.
|
|
"""
|
|
if not depths:
|
|
return tuple(tokens)
|
|
start = start_idx
|
|
if bundle and start is None:
|
|
agent_feat = bundle.get("agent")
|
|
if agent_feat and isinstance(agent_feat.evidence, EvidenceSpan):
|
|
start = agent_feat.evidence.start
|
|
if start is None or start < 0 or start >= len(tokens):
|
|
return tuple(tokens)
|
|
new_tokens = list(tokens)
|
|
nid = agent_node_id
|
|
if nid is None or nid not in depths:
|
|
return tuple(new_tokens) # require explicit nid, no first-key proxy
|
|
d = depths[nid]
|
|
if d.get("language") in ("he", "grc") and d.get("root"):
|
|
orig = new_tokens[start]
|
|
new_tokens[start] = canonicalize_token(orig, nid, depths)
|
|
return tuple(new_tokens)
|
|
|
|
|
|
def build_node_depths(nodes: Sequence[Any]) -> dict[str, dict]:
|
|
"""Lift the node_depths dict from list of GraphNode (or objects with .node_id, .language etc).
|
|
|
|
Pure extraction of the comprehension in pipeline.
|
|
"""
|
|
return {
|
|
n.node_id: {
|
|
k: v for k, v in {
|
|
"language": getattr(n, "language", None),
|
|
"root": getattr(n, "root", None),
|
|
"morphology_id": getattr(n, "morphology_id", None),
|
|
}.items() if v is not None
|
|
}
|
|
for n in nodes
|
|
if getattr(n, "language", None) or getattr(n, "root", None)
|
|
}
|
|
|
|
|
|
def enrich_assessments_with_depth(assessments: Tuple[Any, ...], depth: dict | None) -> Tuple[Any, ...]:
|
|
"""Immutable enrichment of assessments with root note using dataclasses.replace.
|
|
|
|
Returns a new tuple. Enrichment is best-effort: if an assessment does not
|
|
support replace (e.g. non-dataclass or frozen in an incompatible way), the
|
|
original item is kept without the depth note. No __dict__ reconstruction
|
|
is performed (avoids producing invalid objects for slots/frozen types).
|
|
"""
|
|
if not depth:
|
|
return assessments
|
|
roots = [d.get("root") for d in depth.values() if d.get("root")]
|
|
if not roots:
|
|
return assessments
|
|
note = f" [root:{roots[0]}]"
|
|
new_ass = []
|
|
for a in assessments:
|
|
if hasattr(a, "explanation") and getattr(a, "runnable", False):
|
|
try:
|
|
new_a = replace(a, explanation=(getattr(a, "explanation", "") or "") + note)
|
|
except Exception:
|
|
# Best-effort only; do not synthesize copies that could violate
|
|
# frozen/slots invariants after CGA substrate types.
|
|
new_a = a
|
|
new_ass.append(new_a)
|
|
else:
|
|
new_ass.append(a)
|
|
return tuple(new_ass)
|