core/generate/operators.py
Shay b5d6ad6510 feat(compositionality): compose_relations operator lifts lane 68.8% → 100%
Closes the residual `novel_pair_under_seen_relation` pattern that
neither `transitive_walk` nor `multi_relation_walk` could synthesise.

- new `compose_relations(triples, head, frame, relation)` operator —
  pure lookup, returns both `R(head, ?)` and `R(frame, ?)` tails
- new `FRAME_TRANSFER` intent + `_FRAME_TRANSFER_RE` regex tried
  before generic TRANSITIVE_QUERY so "in Y" isn't truncated; handles
  "X belong to in Y" → belongs_to normalisation
- pipeline wiring: `_maybe_compose_relations`, `_fold_compose_into_surface`,
  `_serialize_compose` (folded into operator_invocation so trace_hash
  stays bit-identical across replay)
- regression: inference_closure, multi_step_reasoning,
  cross_domain_transfer all still 100% on public + holdouts

discourse_paragraph v2:
- per-sentence grammar rubric (length, capitalization, subject
  alignment) gated on `require_per_sentence_grammar`
- scaling cases at 10 / 20 / 50 sentences — 3/3 pass, 100% per-sentence
- 3 runtime round-trip cases (`mode: runtime_roundtrip`) that prime
  vault, ask question, verify bit-identical across two fresh runtimes
- new `per_sentence_grammar_pass_rate` lane metric

Long-form replay benchmark (benchmarks/replay_vs_llm.py):
- `replay_determinism_report(prompts, runs, priming)` — CORE-only
- `compare_to_llm(prompts, llm_callable)` — BYO API client, no
  provider lock-in; reports per-prompt determinism on both sides
- ships with default cognition-pack prompts; 100% bit-identical at runs=3

Lanes green: cognition 121/121, runtime 19/19, teaching 17/17,
packs 6/6, compositionality 16/16 + 10/10, inference_closure 20/20 +
12/12, multi_step_reasoning 15/15 + 10/10, cross_domain_transfer
10/10 + 8/8, discourse_paragraph v1 12/12 + v2 6/6.
2026-05-16 22:44:06 -07:00

278 lines
8.9 KiB
Python

"""Typed deterministic operators over CORE's typed state (ADR-0018).
Two operators land here as the Phase 3 v2 inference-depth bundle. Both
are pure functions; both are bounded by a ``max_hops`` cap so they
cannot diverge; both produce outputs that round-trip through the
existing pipeline (entities, vault entries).
Operator-invocation records are folded into ``trace_hash`` (see
``core/cognition/trace.py``) so any turn that calls an operator stays
bit-for-bit replay-deterministic.
"""
from __future__ import annotations
from dataclasses import dataclass
_DEFAULT_MAX_HOPS = 5
@dataclass(frozen=True, slots=True)
class WalkResult:
"""A typed relation-walk result.
``path`` is the sequence of entities visited, starting from the head
and ending at the deepest entity reachable under the requested
relation. Length 1 means no edges were found. Length > 1 means a
chain was traversed.
``relation`` and ``head`` are echoed back so the result is self-
describing for downstream wiring and trace_hash inclusion.
``truncated`` is True when the walk hit the max_hops bound before
exhausting the path; consumers should treat that as a soft signal
that a longer chain may exist in the underlying store.
"""
head: str
relation: str
path: tuple[str, ...]
truncated: bool
def as_dict(self) -> dict[str, object]:
return {
"head": self.head,
"relation": self.relation,
"path": list(self.path),
"truncated": self.truncated,
}
def _normalize(token: str) -> str:
return token.strip().lower()
def transitive_walk(
triples: tuple[tuple[str, str, str], ...],
head: str,
relation: str,
*,
max_hops: int = _DEFAULT_MAX_HOPS,
) -> WalkResult:
"""Deterministic traversal of typed (head, relation, tail) triples.
Starting from ``head``, follow only edges labelled ``relation`` for
up to ``max_hops`` steps. Returns a ``WalkResult`` whose ``path``
is the chain of visited entities.
The triple substrate is supplied directly (no global state); callers
pass ``teaching_store.triples()`` or any equivalent. Comparisons are
case-insensitive and whitespace-trimmed.
Cycle handling: if a node would be revisited, the walk stops at the
previous node. This keeps the operator total over arbitrary
teaching-store contents.
Determinism: pure function over its arguments; no hidden state.
"""
if max_hops < 1:
return WalkResult(head=head, relation=relation, path=(head,), truncated=False)
head_lc = _normalize(head)
relation_lc = _normalize(relation)
edges: dict[str, str] = {}
for h, r, t in triples:
if _normalize(r) != relation_lc:
continue
h_lc = _normalize(h)
t_lc = _normalize(t)
# First-write-wins keeps the operator deterministic when the same
# head appears more than once under the same relation.
edges.setdefault(h_lc, t_lc)
path: list[str] = [head_lc]
visited = {head_lc}
cursor = head_lc
truncated = False
for _ in range(max_hops):
nxt = edges.get(cursor)
if nxt is None:
break
if nxt in visited:
break
path.append(nxt)
visited.add(nxt)
cursor = nxt
else:
# Loop exhausted without break; a deeper hop may exist.
truncated = edges.get(cursor) is not None
return WalkResult(
head=head_lc,
relation=relation_lc,
path=tuple(path),
truncated=truncated,
)
def multi_relation_walk(
triples: tuple[tuple[str, str, str], ...],
head: str,
*,
max_hops: int = _DEFAULT_MAX_HOPS,
) -> WalkResult:
"""Walk any outgoing edge from ``head``, regardless of relation label.
Used when the probe's relation does not match any stored relation
label rooted at ``head`` — i.e. the chain in the teaching store
spans multiple relation types and the probe asks about the *end*
of the chain rather than a single relation's reach. This is the
operator the multi-step-reasoning ``mixed_relation_*`` and
compositionality ``composed_predicate`` patterns need to close.
Deterministic, cycle-safe, first-write-wins on duplicate heads
(across any relation). The returned ``relation`` field is the
sentinel ``"<mixed>"`` so the operator-invocation record makes the
cross-relation provenance explicit in trace_hash.
"""
if max_hops < 1:
return WalkResult(head=head, relation="<mixed>", path=(head,), truncated=False)
head_lc = _normalize(head)
edges: dict[str, str] = {}
for h, _r, t in triples:
edges.setdefault(_normalize(h), _normalize(t))
path: list[str] = [head_lc]
visited = {head_lc}
cursor = head_lc
truncated = False
for _ in range(max_hops):
nxt = edges.get(cursor)
if nxt is None:
break
if nxt in visited:
break
path.append(nxt)
visited.add(nxt)
cursor = nxt
else:
truncated = edges.get(cursor) is not None
return WalkResult(
head=head_lc,
relation="<mixed>",
path=tuple(path),
truncated=truncated,
)
@dataclass(frozen=True, slots=True)
class FrameComposeResult:
"""Result of a relation-frame composition (compose_relations).
``head`` and ``frame`` are the two entities the probe names.
``relation`` is the relation under which both have been instantiated
in the teaching store. ``subject_tail`` is the tail of
``R(head, ?)`` if it exists in the store, else None. ``frame_tail``
is the tail of ``R(frame, ?)``.
The compositional answer to the probe "What does HEAD R in FRAME?"
is ``frame_tail`` (the cross-instance transfer): in the frame of
FRAME, HEAD's behaviour under R aligns with FRAME's R-tail.
``subject_tail`` is returned alongside as the direct (literal)
answer so the realizer can surface both for replay evidence.
"""
head: str
frame: str
relation: str
subject_tail: str | None
frame_tail: str | None
def as_dict(self) -> dict[str, object]:
return {
"head": self.head,
"frame": self.frame,
"relation": self.relation,
"subject_tail": self.subject_tail,
"frame_tail": self.frame_tail,
}
def compose_relations(
triples: tuple[tuple[str, str, str], ...],
head: str,
frame: str,
relation: str,
) -> FrameComposeResult:
"""Frame-aligned cross-instance composition over typed triples.
Given a teaching store containing ``R(head, h_tail)`` and
``R(frame, f_tail)``, this operator answers probes of the form
"What does HEAD R in FRAME?" by reporting both tails. The
compositional reading is ``frame_tail`` — i.e. in the frame of
FRAME, HEAD's R-target aligns with FRAME's R-target.
Pure function over its arguments. First-write-wins on duplicate
``(head, relation)`` keys to preserve determinism. Case-insensitive
and whitespace-trimmed input handling, mirroring ``transitive_walk``.
Returns ``FrameComposeResult`` with ``subject_tail`` / ``frame_tail``
set to None when the corresponding edge is absent — callers can
detect "no composition possible" by checking both for None.
"""
head_lc = _normalize(head)
frame_lc = _normalize(frame)
relation_lc = _normalize(relation)
edges: dict[str, str] = {}
for h, r, t in triples:
if _normalize(r) != relation_lc:
continue
h_lc_inner = _normalize(h)
edges.setdefault(h_lc_inner, _normalize(t))
return FrameComposeResult(
head=head_lc,
frame=frame_lc,
relation=relation_lc,
subject_tail=edges.get(head_lc),
frame_tail=edges.get(frame_lc),
)
def path_recall(
triples: tuple[tuple[str, str, str], ...],
entity: str,
relation_chain: tuple[str, ...],
*,
max_hops: int = _DEFAULT_MAX_HOPS,
) -> tuple[str, ...]:
"""Recall the sequence of entities along a named relation chain.
A single-element ``relation_chain`` (e.g. ``("is",)``) reduces to
``transitive_walk``. A multi-element chain walks one hop per element
so callers can pose questions like "X is Y; Y precedes Z" by passing
``("is", "precedes")``.
Returns the path of entities visited. Empty chain returns just the
starting entity. Determinism and case-insensitivity inherit from
``transitive_walk``.
"""
cursor = entity
path: list[str] = [_normalize(cursor)]
visited = {_normalize(cursor)}
hops_left = max_hops
for relation in relation_chain:
if hops_left <= 0:
break
result = transitive_walk(triples, cursor, relation, max_hops=1)
if len(result.path) < 2:
break
next_entity = result.path[1]
if next_entity in visited:
break
path.append(next_entity)
visited.add(next_entity)
cursor = next_entity
hops_left -= 1
return tuple(path)