Wires observational telemetry on the composer-vs-graph atom-set
relationship. Phase 1 is strictly observational: no enforcement,
no surface mutation, no grounding-source change, no trace-hash impact.
New telemetry fields on TurnEvent + ChatResponse:
composer_graph_atom_status ∈ {equivalent, divergent,
graph_unconstrained,
composer_no_atoms,
not_applicable, ""}
composer_atom_set_hash SHA-256 over sorted unique atoms
graph_atom_set_hash SHA-256 over sorted unique atoms
composer_graph_atom_overlap_count int
Composer atoms come from existing pack candidate metadata
(pack_semantic_domains channel through _maybe_pack_grounded_surface).
Graph atoms come from build_graph_from_input + resolve_lemma on
node.subject/predicate/obj — no prose parsing. When a grounded
composer path lacks explicit atom provenance, status is
'composer_no_atoms'.
New pure helper:
chat/atom_equivalence.py — normalize_atoms, hash_atoms,
atoms_for_graph_nodes, compare_atom_sets
Tests (tests/test_composer_graph_atom_equivalence.py):
- Pack DEFINITION path produces observable equivalence
- Divergent atom sets produce distinct hashes
- Register invariance: atom hashes + status identical across
{neutral, terse, convivial}; trace_hash also constant (R5 axis)
- Anchor lens engaged case still ASCII-only on surface
- No prose-parsing helper symbols introduced in runtime.py
(extract_candidate_surface_lemmas, surface_lemma,
parse_surface_atoms) — enforces Phase 1 boundary
Performance note: build_graph_from_input now runs on every warm
English turn (previously only when forward_graph_constraint=True).
Phase 1 accepts this cost to make the telemetry universally
available; Phase 2+ can introduce a feature flag if needed.
Validation:
- Cognition eval byte-identical: 100/100/91.7/100
- Full lane: 2864 passed, 3 skipped, 0 failed (+5 over baseline)
- Targeted lane: 72 passed in tests/test_{graph_constraint,
pack_grounding,register_tour_demo,anchor_lens_tour_demo,
orthogonality_tour_demo,realizer_guard_holdout,
composer_graph_atom_equivalence}.py
80 lines
2.1 KiB
Python
80 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from hashlib import sha256
|
|
|
|
from chat.pack_resolver import resolve_lemma
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AtomEquivalence:
|
|
status: str
|
|
composer_atom_set_hash: str
|
|
graph_atom_set_hash: str
|
|
overlap_count: int
|
|
|
|
|
|
def normalize_atoms(atoms: tuple[str, ...]) -> tuple[str, ...]:
|
|
cleaned = sorted(
|
|
{
|
|
atom.strip()
|
|
for atom in atoms
|
|
if atom and atom.strip() and atom.strip() != "<pending>"
|
|
}
|
|
)
|
|
return tuple(cleaned)
|
|
|
|
|
|
def hash_atoms(atoms: tuple[str, ...]) -> str:
|
|
cleaned = normalize_atoms(atoms)
|
|
if not cleaned:
|
|
return ""
|
|
blob = "\n".join(cleaned).encode("utf-8")
|
|
return sha256(blob).hexdigest()
|
|
|
|
|
|
def atoms_for_graph_nodes(graph) -> tuple[str, ...]:
|
|
atoms: list[str] = []
|
|
for node in getattr(graph, "nodes", ()) or ():
|
|
for surface in (
|
|
getattr(node, "subject", ""),
|
|
getattr(node, "predicate", ""),
|
|
getattr(node, "obj", ""),
|
|
):
|
|
resolved = resolve_lemma(str(surface))
|
|
if resolved is None:
|
|
continue
|
|
_, domains = resolved
|
|
atoms.extend(domains)
|
|
return normalize_atoms(tuple(atoms))
|
|
|
|
|
|
def compare_atom_sets(
|
|
*,
|
|
composer_atoms: tuple[str, ...],
|
|
graph_atoms: tuple[str, ...],
|
|
graph_unconstrained: bool,
|
|
applicable: bool,
|
|
) -> AtomEquivalence:
|
|
composer_norm = normalize_atoms(composer_atoms)
|
|
graph_norm = normalize_atoms(graph_atoms)
|
|
composer_hash = hash_atoms(composer_norm)
|
|
graph_hash = hash_atoms(graph_norm)
|
|
overlap = len(set(composer_norm).intersection(graph_norm))
|
|
|
|
if not applicable:
|
|
status = "not_applicable"
|
|
elif not composer_norm:
|
|
status = "composer_no_atoms"
|
|
elif graph_unconstrained or not graph_norm:
|
|
status = "graph_unconstrained"
|
|
elif overlap > 0:
|
|
status = "equivalent"
|
|
else:
|
|
status = "divergent"
|
|
return AtomEquivalence(
|
|
status=status,
|
|
composer_atom_set_hash=composer_hash,
|
|
graph_atom_set_hash=graph_hash,
|
|
overlap_count=overlap,
|
|
)
|