core/evals/inference_closure/runner.py
Shay e509e0d6d6 feat(phase3): inference-closure lane v1 — foundation OK, no operator
First Phase 3 lane. Scores whether CORE can derive entailments that
were not directly asserted, given a chain of premises taught through
the correction loop. Five transitive relation patterns drawn from
en_core_cognition_v1:

  transitive_is        A is B; B is C            -> What is A?
  transitive_precedes  A precedes B; B precedes C -> What does A precede?
  transitive_grounds   A grounds B; B grounds C   -> What does A ground?
  transitive_causes    A causes B; B causes C     -> What does A cause?
  transitive_belongs_to A belongs_to B; B belongs_to C -> Where does A belong?

Pass = expected entailment token appears in probe response surface
or walk surface (M1 or M2) AND every premise stored (M3) AND
trace_hash deterministic across two fresh runs (M4).

Results:

  split        n   derived  stored  replay  overall_pass
  public/v1    20  0.0      1.0     1.0     False
  holdouts/v1  12  0.0      1.0     1.0     False

This is the expected honest failure per docs/capability_roadmap.md
Phase 3. Foundation guarantees from Phase 2 (storage + replay) hold
at this depth; the inference-closure step itself does not yet exist
in CORE. The lane scores exactly the gap.

Concrete trace recorded in gaps.md: for premises 'wisdom is light',
'light is truth', probe 'What is wisdom?' returns the template
'wisdom is defined as ...' — vault retrieves 9 entries including
both premises, but the realizer emits a definition stub instead of
a derivation.

Architectural gaps filed (evals/inference_closure/gaps.md):

  Gap 1. generate/graph_planner.py has no transitive composition —
         plan_articulation picks a single node; there is no chained
         relation walk that produces a derived node from premises.
  Gap 2. field/propagate.py has no derivable-but-not-asserted recall
         path — vault retrieval is direct CGA inner product; no
         path-recall operator over relation-typed edges.

Both gaps are v2 engineering candidates and may share an
implementation surface. The lane is permanent regression evidence
of what specifically is missing.

Includes:
  - contract.md: pass criteria, anti-overfitting note, sub-metric
    definitions, calibration approach.
  - runner.py: parallel, fresh-pipeline-per-case, M1-M4 scoring,
    two-run replay-determinism check.
  - dev/cases.jsonl (5), public/v1 (20), holdouts/v1 (12) — disjoint
    entity sets, all five patterns covered.
  - baselines/v1_structural_zero.json: frontier LLMs do not emit
    the typed signals by construction.
  - gaps.md: full architectural finding, engineering shapes for v2.

CLI suites smoke / cognition / teaching pass; no regression on
Phase 2 work.
2026-05-16 14:33:08 -07:00

174 lines
6.1 KiB
Python

"""inference-closure eval lane runner.
Tests CORE's ability to derive entailments not directly asserted.
For each case the runner:
1. Runs the premise list on a fresh CognitiveTurnPipeline, recording
per-premise pack_mutation_proposal firings.
2. Runs the probe on that pipeline.
3. Inspects the probe response's surface / articulation surface /
vault retrieval evidence for the expected entailment token.
4. Replays the full (premises, probe) sequence on a second fresh
pipeline and checks trace_hash determinism.
Sub-metrics (per case):
M1. derived_token_in_surface — entailment token appears (case-
insensitive, token-bounded) in probe response surface
or articulation_surface.
M2. derived_token_in_vault — entailment token appears in any
vault-retrieved articulation evidence the probe produced.
M3. premises_stored — every premise emits a proposal.
M4. replay_determinism — two independent runs share trace_hash.
A case passes only when (M1 OR M2) AND M3 AND M4 hold.
Conforms to the framework interface: run_lane(cases, config=None) -> report.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Any
from chat.runtime import ChatRuntime
from core.cognition.pipeline import CognitiveTurnPipeline
from core.config import RuntimeConfig
from evals.parallel import run_cases_parallel
@dataclass(slots=True)
class LaneReport:
metrics: dict[str, Any] = field(default_factory=dict)
case_details: list[dict[str, Any]] = field(default_factory=list)
_TOKEN_BOUND = re.compile(r"\b([a-z][a-z'\-]*)\b")
def _token_set(text: str) -> set[str]:
return set(_TOKEN_BOUND.findall((text or "").lower()))
def _entailment_hit(text: str, candidates: list[str]) -> bool:
if not text:
return False
tokens = _token_set(text)
return any(c.lower() in tokens for c in candidates)
def _run_chain(premises: list[str], probe: str) -> dict[str, Any]:
"""Return per-run signals for one fresh (premises, probe) sequence."""
runtime = ChatRuntime()
pipeline = CognitiveTurnPipeline(runtime)
premise_proposal_count = 0
for premise in premises:
try:
r = pipeline.run(premise, max_tokens=8)
except ValueError:
continue
if r.pack_mutation_proposal is not None:
premise_proposal_count += 1
try:
probe_result = pipeline.run(probe, max_tokens=8)
except ValueError:
return {
"surface": "",
"articulation_surface": "",
"walk_surface": "",
"vault_hits": 0,
"trace_hash": "",
"premise_proposal_count": premise_proposal_count,
"value_error": True,
}
return {
"surface": probe_result.surface or "",
"articulation_surface": probe_result.articulation_surface or "",
"walk_surface": probe_result.walk_surface or "",
"vault_hits": int(probe_result.vault_hits),
"trace_hash": probe_result.trace_hash,
"premise_proposal_count": premise_proposal_count,
"value_error": False,
}
def _run_case(case: dict[str, Any]) -> dict[str, Any]:
premises: list[str] = list(case.get("premises", []))
probe: str = case["probe"]
entailments: list[str] = list(case.get("expected_entailment_tokens", []))
expected_proposals = int(case.get("expected_proposals", len(premises) // 2))
first = _run_chain(premises, probe)
second = _run_chain(premises, probe)
surface_blob = " ".join(
[first["surface"], first["articulation_surface"], first["walk_surface"]]
)
surface_hit = _entailment_hit(surface_blob, entailments)
# Vault evidence proxy: when the probe response references entailment
# tokens in its articulation walk, the vault retrieved them. The pipeline
# does not expose retrieved-entity text directly; we use the walk_surface
# as the closest available signal and call it a vault hit when the
# entailment token appears there.
vault_hit = _entailment_hit(first["walk_surface"], entailments)
premises_stored = first["premise_proposal_count"] >= expected_proposals
replay_pass = (
bool(first["trace_hash"])
and first["trace_hash"] == second["trace_hash"]
and first["vault_hits"] == second["vault_hits"]
and first["premise_proposal_count"] == second["premise_proposal_count"]
)
derived_recall = surface_hit or vault_hit
passed = derived_recall and premises_stored and replay_pass
return {
"id": case.get("id", ""),
"pattern": case.get("pattern", ""),
"entailment_tokens": entailments,
"vault_hits": first["vault_hits"],
"trace_hash": first["trace_hash"],
"trace_hash_replay": second["trace_hash"],
"premise_proposal_count": first["premise_proposal_count"],
"expected_proposals": expected_proposals,
"surface_hit": surface_hit,
"vault_hit": vault_hit,
"premises_stored_pass": premises_stored,
"replay_pass": replay_pass,
"derived_recall_pass": derived_recall,
"passed": passed,
}
def run_lane(
cases: list[dict[str, Any]],
*,
config: RuntimeConfig | None = None,
workers: int | None = None,
) -> LaneReport:
if not cases:
return LaneReport(metrics={}, case_details=[])
_ = config
case_details = run_cases_parallel(cases, _run_case, workers=workers)
total = len(case_details)
derived = sum(1 for d in case_details if d["derived_recall_pass"]) / total
stored = sum(1 for d in case_details if d["premises_stored_pass"]) / total
replay = sum(1 for d in case_details if d["replay_pass"]) / total
overall = sum(1 for d in case_details if d["passed"]) / total
overall_pass = derived >= 0.50 and stored >= 0.95 and replay >= 0.95
metrics: dict[str, Any] = {
"derived_recall_rate": round(derived, 4),
"premises_stored_rate": round(stored, 4),
"replay_determinism": round(replay, 4),
"all_pass_rate": round(overall, 4),
"case_count": total,
"overall_pass": overall_pass,
}
return LaneReport(metrics=metrics, case_details=case_details)