Completes the bridge: every 5F sub-battery now has an embedded
(Phase 1a) AND a live (Phase 1b.2) path. Per-task detail.source
distinguishes synthetic from live signal.
| Sub-battery | Live surface | Live fixtures |
|---|---|---|
| Formulate | qa.parse_claims.parse_pointer_claims | 15 (prior) |
| Feedback Loop | store.append_audit + memory.snapshot (temp shard) | 12 (prior) |
| Function | parse_pointer_claims + shape evaluators | 12 |
| Finetuning | selfmodel.store_snapshot + claims_for round-trip | 10 |
| Falsification | qa.verify.verify_quotes (real verifier) | 12 |
Function live: input_text routes through real parse_pointer_claims;
the same Phase-1a shape evaluators (shape_match / pointer_set_match
/ threshold_on_metric) run on the live-parsed output. Tests
parser→shape pipeline against actual organism behavior.
Finetuning live: gated via fixture's "live": true flag. Runner
writes parent + child SelfModel + capability_claim to a fresh temp
shard via real arborist.selfmodel.store_snapshot, reads back via
claims_for, runs the same improvement check. Tests the SelfModel
persistence + claim-attach surface, not just static fixture data.
Falsification live: answer_text + context routes through real
arborist.qa.verify.verify_quotes. Live signals translate the
verifier's audit_mode + verifier_method + unverified_quotes into
flat tags (UNGROUNDED, STRICT_<method>, HYBRID_<method>,
UNVERIFIED_QUOTE) the runner matches against expected_reason. One
fixture (5f-fal-live-003) deliberately documents a known soft-
signal gap — the entity strategy treats Insulin/Penicillin claims
as equivalent because both share "Alexander Fleming". Production
catches it via title-relevance + claim-lattice verifier; the
fixture pins the soft-path limit so future verifier changes
re-trigger review.
Surface:
- bench/batteries/b_5f.py — _live_function_produced,
_live_finetuning_measure, _live_falsification_violations
helpers + two-mode dispatch in run_function / run_finetuning /
run_falsification.
- bench/fixtures/5f/{function,finetuning,falsification}-live-v1.jsonl
(12 + 10 + 12 fixtures).
- Makefile: bench-5f-{function,finetuning,falsification}-live
targets; bench-5f-live aggregate now covers all five sub-batteries.
- Tests: 9 new in tests/test_bench_batteries.py
- live + embedded paths for each sub-battery (6 tests)
- direct helper tests verifying the real arborist surfaces are
invoked, not stubs (3 tests)
Phase 1a fixture digests unchanged. _DEFAULT_FIXTURES still points
at synthetic Phase-1a fixtures so `runner --all` behavior is
identical and deterministic; live mode invoked via explicit
--fixtures path or make targets.
Full suite: 1210 passed, 36 skipped.
5F battery is now the first to bridge synthetic→live across every
sub-battery. The pattern + live fixture format are reusable for
5R Phase 1b.2 (when SelfModel-backed workspace ops want live
verification).
861 lines
33 KiB
Python
861 lines
33 KiB
Python
"""5F battery — Function / Finetuning / Falsification / Formulate /
|
|
Feedback Loop (ticket #000025).
|
|
|
|
Operational-quality axis from Dav1DPrometheus's framework. 5S
|
|
evaluates linguistic/sign-structure precision; 5T evaluates
|
|
temporal/cross-reasoning/logical coverage; 5F evaluates whether the
|
|
organism *acts, adapts, falsifies, formulates, and integrates
|
|
feedback* — the dimensions that turn a state-space into an adaptive
|
|
state-space.
|
|
|
|
All runners deterministic, no LLM-as-judge. Phase 1a fixtures use
|
|
text / claim_lattice / providence_record / selfmodel_snapshot /
|
|
memory_snapshot carriers; future carriers fail/skip explicitly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from bench.batteries.base import (
|
|
BatteryResult,
|
|
TaskResult,
|
|
fixture_digest,
|
|
fixture_meta,
|
|
iter_tasks,
|
|
validate_carrier,
|
|
)
|
|
from bench.batteries.b_5s import _content_tokens, _runtime_digest
|
|
|
|
|
|
def _carrier_check(task: dict) -> tuple[bool, str]:
|
|
reason = validate_carrier(task)
|
|
if reason is not None:
|
|
return False, reason
|
|
return True, ""
|
|
|
|
|
|
def _build_result(
|
|
sub_battery: str,
|
|
fixtures_path: Path,
|
|
per_task: list[TaskResult],
|
|
metrics: dict[str, float],
|
|
) -> BatteryResult:
|
|
pass_count = sum(1 for t in per_task if t.passed)
|
|
fail_count = len(per_task) - pass_count
|
|
return BatteryResult(
|
|
battery="5f",
|
|
sub_battery=sub_battery,
|
|
fixture_path=str(fixtures_path),
|
|
fixture_digest=fixture_digest(fixtures_path),
|
|
pass_count=pass_count,
|
|
fail_count=fail_count,
|
|
metrics=metrics,
|
|
per_task=per_task,
|
|
runtime_digest=_runtime_digest(),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Function — does the system perform the designated operation?
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
def _eval_shape_match(task: dict) -> tuple[bool, str]:
|
|
"""Check that the produced output shape matches the expected
|
|
shape declaration.
|
|
|
|
Phase 1a fixtures embed both the produced output and the
|
|
expected_shape. Real production wiring (Phase 1b) will call
|
|
arborist.qa.runner.ask and use the response shape.
|
|
"""
|
|
produced = task.get("produced_output", {})
|
|
shape = task["expected_shape"]
|
|
claim_count_ok = len(produced.get("claims", [])) == shape.get("claim_count")
|
|
pointers_ok = True
|
|
if shape.get("pointers_required"):
|
|
pointers_ok = all(
|
|
isinstance(c.get("pointer_ids", []), list)
|
|
and len(c.get("pointer_ids", [])) > 0
|
|
for c in produced.get("claims", [])
|
|
)
|
|
return claim_count_ok and pointers_ok, "shape_match"
|
|
|
|
|
|
def _eval_pointer_set_match(task: dict) -> tuple[bool, str]:
|
|
produced = set()
|
|
for c in task.get("produced_output", {}).get("claims", []):
|
|
for p in c.get("pointer_ids", []):
|
|
produced.add(p)
|
|
expected = set(task["expected_shape"]["pointer_set"])
|
|
return produced == expected, f"pointer_set_match expected={sorted(expected)} got={sorted(produced)}"
|
|
|
|
|
|
def _eval_threshold_on_metric(task: dict) -> tuple[bool, str]:
|
|
metric = task["expected_shape"]["metric"]
|
|
threshold = task["expected_shape"]["threshold"]
|
|
measured = task.get("produced_output", {}).get(metric)
|
|
if measured is None:
|
|
return False, f"metric {metric!r} not in produced_output"
|
|
return measured >= threshold, f"{metric}={measured} threshold={threshold}"
|
|
|
|
|
|
_FUNCTION_EVALUATORS = {
|
|
"shape_match": _eval_shape_match,
|
|
"pointer_set_match": _eval_pointer_set_match,
|
|
"threshold_on_metric": _eval_threshold_on_metric,
|
|
}
|
|
|
|
|
|
def _live_function_produced(task: dict) -> dict:
|
|
"""Derive ``produced_output`` by routing ``input_text`` through
|
|
arborist.qa.parse_claims.parse_pointer_claims. Live mode
|
|
primary surface: tests the actual claim-extraction pipeline
|
|
against the same shape evaluators Phase 1a uses."""
|
|
from arborist.qa.parse_claims import parse_pointer_claims
|
|
|
|
parsed = parse_pointer_claims(task["input_text"])
|
|
return {
|
|
"claims": [
|
|
{"claim_text": c.claim_text, "pointer_ids": list(c.pointer_ids)}
|
|
for c in parsed
|
|
],
|
|
}
|
|
|
|
|
|
def run_function(fixtures_path: Path) -> BatteryResult:
|
|
"""Function pass-rate over deterministic evaluator types.
|
|
|
|
Two fixture modes:
|
|
|
|
- **Embedded** (Phase 1a): ``produced_output`` in the fixture.
|
|
- **Live** (Phase 1b.2): ``input_text`` in the fixture; runner
|
|
derives ``produced_output`` via real
|
|
:func:`arborist.qa.parse_claims.parse_pointer_claims` and
|
|
then applies the same shape evaluator. Tests parser → shape
|
|
pipeline against actual organism behavior.
|
|
"""
|
|
per_task: list[TaskResult] = []
|
|
for task in iter_tasks(fixtures_path):
|
|
task_id = task["id"]
|
|
ok, reason = _carrier_check(task)
|
|
if not ok:
|
|
per_task.append(
|
|
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
|
|
)
|
|
continue
|
|
evaluator = task.get("evaluator")
|
|
fn = _FUNCTION_EVALUATORS.get(evaluator)
|
|
if fn is None:
|
|
per_task.append(
|
|
TaskResult(task_id=task_id, passed=False,
|
|
detail={"reason": f"unknown_evaluator: {evaluator!r}"})
|
|
)
|
|
continue
|
|
try:
|
|
if "produced_output" in task:
|
|
source = "embedded"
|
|
effective_task = task
|
|
elif "input_text" in task:
|
|
source = "live"
|
|
effective_task = {**task, "produced_output": _live_function_produced(task)}
|
|
else:
|
|
raise KeyError(
|
|
"task needs 'produced_output' (embedded) or "
|
|
"'input_text' (live)"
|
|
)
|
|
valid, why = fn(effective_task)
|
|
expected = task.get("expected", "pass")
|
|
observed = "pass" if valid else "fail"
|
|
passed = observed == expected
|
|
except Exception as exc: # noqa: BLE001
|
|
passed = False
|
|
source = "error"
|
|
why = f"{type(exc).__name__}: {exc}"
|
|
per_task.append(TaskResult(
|
|
task_id=task_id, passed=passed,
|
|
detail={"source": source, "evaluator": evaluator, "reason": why},
|
|
))
|
|
|
|
total = len(per_task)
|
|
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
|
|
meta = fixture_meta(fixtures_path)
|
|
return _build_result(
|
|
meta.get("sub_battery", "function"),
|
|
fixtures_path,
|
|
per_task,
|
|
{"function_pass_rate": rate},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Finetuning — does the organism improve parent→child?
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
def _capital_cost_delta(task: dict) -> float:
|
|
"""Sum the resource_budget deltas as a single scalar capital cost.
|
|
|
|
Phase 1a uses budget-as-cost: v1 fixtures don't carry actual
|
|
measured costs, only budget caps. Phase 1b.2 will replace this
|
|
with real values from the capital_ledger (#000020).
|
|
|
|
Sums ``max_compute_ms_delta`` (treated 1 ms = 1e-3 cost units) +
|
|
``max_storage_delta_bytes`` (1 MB = 1 cost unit). Tunable later.
|
|
"""
|
|
budget = task.get("resource_budget", {})
|
|
compute_ms = float(budget.get("max_compute_ms_delta", 0))
|
|
storage_bytes = float(budget.get("max_storage_delta_bytes", 0))
|
|
return compute_ms * 1e-3 + storage_bytes / 1e6
|
|
|
|
|
|
# Sentinels for division-by-zero efficiency metrics. Per fox's
|
|
# 2026-05-08 review of #fbd99a8: explicit values, not Python NaN/inf
|
|
# accidents. We use Python ``float('inf')`` when cost=0 and gain>0 so
|
|
# downstream consumers can compare; JSON serialization uses
|
|
# ``default=str`` and renders inf as ``"inf"`` — callers checking
|
|
# numeric ordering must handle both representations.
|
|
EFFICIENCY_INFINITE = float("inf")
|
|
EFFICIENCY_UNDEFINED = 0.0 # zero gain at zero cost = no efficiency observed
|
|
|
|
|
|
def _efficiency(gain: float, cost: float) -> float:
|
|
"""Compute gain/cost with explicit zero-cost guards.
|
|
|
|
- cost > 0 → gain / cost (standard ratio)
|
|
- cost == 0, gain > 0 → EFFICIENCY_INFINITE (free improvement)
|
|
- cost == 0, gain == 0 → EFFICIENCY_UNDEFINED (no signal)
|
|
- cost == 0, gain < 0 → -EFFICIENCY_INFINITE (free regression — bad)
|
|
"""
|
|
if cost > 0:
|
|
return gain / cost
|
|
if gain > 0:
|
|
return EFFICIENCY_INFINITE
|
|
if gain < 0:
|
|
return -EFFICIENCY_INFINITE
|
|
return EFFICIENCY_UNDEFINED
|
|
|
|
|
|
def _live_finetuning_measure(task: dict) -> dict:
|
|
"""Round-trip parent/child SelfModel + capability_claim through a
|
|
fresh temp shard; read back measured values from the persisted
|
|
rows. Tests the SelfModel persistence + claim-attach surface,
|
|
not just static fixture data.
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
from arborist.selfmodel import (
|
|
CapabilityClaim,
|
|
snapshot,
|
|
store_snapshot,
|
|
)
|
|
from arborist.selfmodel.canonical import with_claims
|
|
from arborist.selfmodel.store import claims_for
|
|
from arborist.store import connect, transaction
|
|
|
|
metric = task["target_capability"]
|
|
parent_val = float(task["parent_measured_value"])
|
|
child_val = float(task["child_measured_value"])
|
|
threshold = float(task.get("expected_improvement_min", 0.05))
|
|
|
|
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
|
tmp.close()
|
|
try:
|
|
conn = connect(tmp.name)
|
|
try:
|
|
with transaction(conn):
|
|
parent_claim = CapabilityClaim(
|
|
metric=metric,
|
|
threshold=threshold,
|
|
eval_digest="live-finetuning-eval",
|
|
measured_value=parent_val,
|
|
measured_at=1700000000,
|
|
)
|
|
sm_parent = with_claims(snapshot(conn), [parent_claim])
|
|
parent_root = store_snapshot(
|
|
conn, sm_parent, claims=[parent_claim], ts=1700000000
|
|
)
|
|
with transaction(conn):
|
|
child_claim = CapabilityClaim(
|
|
metric=metric,
|
|
threshold=threshold,
|
|
eval_digest="live-finetuning-eval",
|
|
measured_value=child_val,
|
|
measured_at=1700000100,
|
|
)
|
|
sm_child = with_claims(snapshot(conn), [child_claim])
|
|
child_root = store_snapshot(
|
|
conn, sm_child, claims=[child_claim], ts=1700000100
|
|
)
|
|
parent_rows = claims_for(conn, parent_root)
|
|
child_rows = claims_for(conn, child_root)
|
|
return {
|
|
"parent_root": parent_root,
|
|
"child_root": child_root,
|
|
"parent_measured": parent_rows[0]["measured_value"],
|
|
"child_measured": child_rows[0]["measured_value"],
|
|
}
|
|
finally:
|
|
conn.close()
|
|
finally:
|
|
try:
|
|
os.unlink(tmp.name)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def run_finetuning(fixtures_path: Path) -> BatteryResult:
|
|
"""Adaptation improvement check + adaptation_efficiency metric.
|
|
|
|
Each task asserts that ``child_measured_value >=
|
|
parent_measured_value + expected_improvement_min``.
|
|
|
|
Two fixture modes:
|
|
|
|
- **Embedded** (Phase 1a): parent/child measured values come
|
|
directly from the fixture; runner does the math.
|
|
- **Live** (Phase 1b.2 — gate via ``"live": true``): runner
|
|
writes parent + child SelfModels to a fresh temp shard via
|
|
real :func:`arborist.selfmodel.store_snapshot`, attaches a
|
|
:class:`arborist.selfmodel.CapabilityClaim` to each, reads
|
|
the persisted measured values back from
|
|
:func:`arborist.selfmodel.store.claims_for`, then runs the
|
|
same improvement check. Tests the SelfModel persistence
|
|
surface, not just the fixture data.
|
|
|
|
Per #000025 §5.2 and the 2026-05-08 fbd99a8 review:
|
|
``adaptation_efficiency = Δimprovement / Δcapital_cost`` with
|
|
explicit zero-cost guards (see :func:`_efficiency`).
|
|
"""
|
|
per_task: list[TaskResult] = []
|
|
efficiency_values: list[float] = []
|
|
finite_efficiency_values: list[float] = []
|
|
for task in iter_tasks(fixtures_path):
|
|
task_id = task["id"]
|
|
ok, reason = _carrier_check(task)
|
|
if not ok:
|
|
per_task.append(
|
|
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
|
|
)
|
|
continue
|
|
try:
|
|
if task.get("live"):
|
|
source = "live"
|
|
live = _live_finetuning_measure(task)
|
|
parent = float(live["parent_measured"])
|
|
child = float(live["child_measured"])
|
|
else:
|
|
source = "embedded"
|
|
parent = float(task["parent_measured_value"])
|
|
child = float(task["child_measured_value"])
|
|
min_improvement = float(task.get("expected_improvement_min", 0.05))
|
|
improvement = child - parent
|
|
improved = improvement >= min_improvement
|
|
expected = task.get("expected", "pass")
|
|
observed = "pass" if improved else "fail"
|
|
passed = observed == expected
|
|
cost = _capital_cost_delta(task)
|
|
efficiency = _efficiency(improvement, cost)
|
|
efficiency_values.append(efficiency)
|
|
if efficiency not in (EFFICIENCY_INFINITE, -EFFICIENCY_INFINITE):
|
|
finite_efficiency_values.append(efficiency)
|
|
detail = {
|
|
"source": source,
|
|
"parent_measured_value": parent,
|
|
"child_measured_value": child,
|
|
"improvement": improvement,
|
|
"min_improvement": min_improvement,
|
|
"capital_cost_delta": cost,
|
|
"adaptation_efficiency": efficiency,
|
|
"expected": expected,
|
|
}
|
|
except Exception as exc: # noqa: BLE001
|
|
passed = False
|
|
detail = {"reason": f"{type(exc).__name__}: {exc}"}
|
|
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
|
|
|
|
total = len(per_task)
|
|
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
|
|
# Mean efficiency reported only over the finite tasks; +inf and
|
|
# -inf participants are counted in their own buckets so the mean
|
|
# stays a meaningful summary.
|
|
mean_finite_efficiency = (
|
|
sum(finite_efficiency_values) / len(finite_efficiency_values)
|
|
if finite_efficiency_values else 0.0
|
|
)
|
|
inf_count = sum(1 for e in efficiency_values if e == EFFICIENCY_INFINITE)
|
|
neg_inf_count = sum(1 for e in efficiency_values if e == -EFFICIENCY_INFINITE)
|
|
meta = fixture_meta(fixtures_path)
|
|
return _build_result(
|
|
meta.get("sub_battery", "finetuning"),
|
|
fixtures_path,
|
|
per_task,
|
|
{
|
|
"adaptation_improvement_rate": rate,
|
|
"adaptation_efficiency_mean_finite": mean_finite_efficiency,
|
|
"adaptation_efficiency_infinite_count": float(inf_count),
|
|
"adaptation_efficiency_neg_infinite_count": float(neg_inf_count),
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Falsification — does the organism detect and correct errors?
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
def _live_falsification_violations(task: dict) -> list[str]:
|
|
"""Run real :func:`arborist.qa.verify.verify_quotes` against the
|
|
fixture's (answer_text, context) pair; surface the violation
|
|
signals as a flat list the runner can match against
|
|
``expected_reason``.
|
|
|
|
Live mode signals that map to fixture ``expected_reason`` tags:
|
|
|
|
- ``UNGROUNDED`` audit_mode → emit ``"UNGROUNDED"``.
|
|
- Any unverified_quotes → emit ``"UNVERIFIED_QUOTE"`` for each.
|
|
- HYBRID with quote-strategy resolution → emit ``"HYBRID_QUOTE"``.
|
|
|
|
The full violation taxonomy (TITLE_MISMATCH, WARRANT_MISSING,
|
|
LAZY_ANCHOR_DEMOTED, etc.) lives in the claim-lattice verifier;
|
|
Phase 1b.2 wires only the quote-path verifier — the broader
|
|
surface is reserved for Phase 1b.3.
|
|
"""
|
|
from arborist.qa.verify import verify_quotes
|
|
|
|
verdict = verify_quotes(task["answer_text"], task["context"])
|
|
out: list[str] = []
|
|
if verdict.get("audit_mode") == "UNGROUNDED":
|
|
out.append("UNGROUNDED")
|
|
if verdict.get("audit_mode") == "HYBRID":
|
|
out.append(f"HYBRID_{verdict.get('verifier_method', 'unknown').upper()}")
|
|
if verdict.get("audit_mode") == "STRICT":
|
|
out.append(f"STRICT_{verdict.get('verifier_method', 'unknown').upper()}")
|
|
if verdict.get("unverified_quotes"):
|
|
out.append("UNVERIFIED_QUOTE")
|
|
return out
|
|
|
|
|
|
def run_falsification(fixtures_path: Path) -> BatteryResult:
|
|
"""Planted-error detection rate.
|
|
|
|
Two fixture modes:
|
|
|
|
- **Embedded** (Phase 1a): fixture lists ``observed_violations``
|
|
directly; runner checks ``expected_reason`` is present.
|
|
- **Live** (Phase 1b.2): fixture provides
|
|
``answer_text`` + ``context``; runner calls real
|
|
:func:`arborist.qa.verify.verify_quotes` and translates the
|
|
verdict into violation signals (``UNGROUNDED``,
|
|
``UNVERIFIED_QUOTE``, ``HYBRID_<method>``, ``STRICT_<method>``).
|
|
Tests the live verifier surface against planted (claim,
|
|
evidence) pairs.
|
|
|
|
Each fixture pinned to ``verifier_method_root`` so verifier-
|
|
shape changes warn rather than false-fail (per ticket #000025
|
|
risk #3).
|
|
"""
|
|
per_task: list[TaskResult] = []
|
|
for task in iter_tasks(fixtures_path):
|
|
task_id = task["id"]
|
|
ok, reason = _carrier_check(task)
|
|
if not ok:
|
|
per_task.append(
|
|
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
|
|
)
|
|
continue
|
|
try:
|
|
if "answer_text" in task and "context" in task:
|
|
source = "live"
|
|
observed = set(_live_falsification_violations(task))
|
|
else:
|
|
source = "embedded"
|
|
observed = set(task.get("observed_violations", []))
|
|
expected_reason = task["expected_reason"]
|
|
expected_outcome = task.get("expected", "pass")
|
|
detected = expected_reason in observed
|
|
observed_outcome = "pass" if detected else "fail"
|
|
passed = observed_outcome == expected_outcome
|
|
detail = {
|
|
"source": source,
|
|
"expected_reason": expected_reason,
|
|
"observed_violations": sorted(observed),
|
|
"detected": detected,
|
|
"verifier_method_root": task.get("verifier_method_root"),
|
|
}
|
|
except Exception as exc: # noqa: BLE001
|
|
passed = False
|
|
detail = {"reason": f"{type(exc).__name__}: {exc}"}
|
|
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
|
|
|
|
total = len(per_task)
|
|
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
|
|
meta = fixture_meta(fixtures_path)
|
|
return _build_result(
|
|
meta.get("sub_battery", "falsification"),
|
|
fixtures_path,
|
|
per_task,
|
|
{"error_detection_rate": rate},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Formulate — turn unstructured input into structured output.
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
def _live_produced_lattice(input_text: str) -> dict:
|
|
"""Derive a ``produced_lattice`` by routing ``input_text`` through
|
|
arborist's actual claim-lattice parser. Phase 1b.2 entry point —
|
|
moves the runner from synthetic gold to live organism output.
|
|
|
|
Returns the same shape Phase 1a fixtures embed:
|
|
``{"claims": [{"claim_text": ..., "pointer_ids": [...]}, ...]}``.
|
|
"""
|
|
from arborist.qa.parse_claims import parse_pointer_claims
|
|
|
|
parsed = parse_pointer_claims(input_text)
|
|
return {
|
|
"claims": [
|
|
{"claim_text": c.claim_text, "pointer_ids": list(c.pointer_ids)}
|
|
for c in parsed
|
|
],
|
|
}
|
|
|
|
|
|
def run_formulate(fixtures_path: Path) -> BatteryResult:
|
|
"""Structural match against expected claim lattice.
|
|
|
|
Match policy: claim count + sorted-approx claim text + exact
|
|
pointer-ID-set. NOT exact-string.
|
|
|
|
Two fixture modes (selected per-task):
|
|
|
|
- **Embedded** (Phase 1a): fixture provides ``produced_lattice``
|
|
directly. Used for the 10 seed fixtures landed under #000025.
|
|
- **Live** (Phase 1b.2): fixture provides ``input_text`` only;
|
|
runner calls
|
|
:func:`arborist.qa.parse_claims.parse_pointer_claims` and
|
|
tests the actual organism's parser against
|
|
``expected_lattice``.
|
|
|
|
Embedded takes precedence if both fields are present.
|
|
Per-task ``detail.source`` reports which path ran, so the bench
|
|
output distinguishes synthetic from live signal.
|
|
"""
|
|
per_task: list[TaskResult] = []
|
|
for task in iter_tasks(fixtures_path):
|
|
task_id = task["id"]
|
|
ok, reason = _carrier_check(task)
|
|
if not ok:
|
|
per_task.append(
|
|
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
|
|
)
|
|
continue
|
|
try:
|
|
if "produced_lattice" in task:
|
|
produced = task["produced_lattice"]
|
|
source = "embedded"
|
|
elif "input_text" in task:
|
|
produced = _live_produced_lattice(task["input_text"])
|
|
source = "live"
|
|
else:
|
|
raise KeyError(
|
|
"task needs 'produced_lattice' (embedded) or "
|
|
"'input_text' (live derivation)"
|
|
)
|
|
expected = task["expected_lattice"]
|
|
count_ok = len(produced.get("claims", [])) == expected.get("claim_count")
|
|
order_ok = True
|
|
pointer_ok = True
|
|
text_ok = True
|
|
for i, exp in enumerate(expected.get("claims", [])):
|
|
if i >= len(produced.get("claims", [])):
|
|
count_ok = False
|
|
break
|
|
got = produced["claims"][i]
|
|
# Pointer-set exact match.
|
|
if set(got.get("pointer_ids", [])) != set(exp.get("pointer_ids", [])):
|
|
pointer_ok = False
|
|
# Sorted-approx claim text — content-token overlap ≥ 0.7.
|
|
got_t = _content_tokens(got.get("claim_text", ""))
|
|
exp_t = _content_tokens(exp.get("claim_text", ""))
|
|
if not exp_t or len(got_t & exp_t) / len(exp_t) < 0.7:
|
|
text_ok = False
|
|
passed_all = count_ok and order_ok and pointer_ok and text_ok
|
|
expected_outcome = task.get("expected", "pass")
|
|
observed = "pass" if passed_all else "fail"
|
|
passed = observed == expected_outcome
|
|
detail = {
|
|
"source": source,
|
|
"count_ok": count_ok,
|
|
"pointer_ok": pointer_ok,
|
|
"text_ok": text_ok,
|
|
}
|
|
except Exception as exc: # noqa: BLE001
|
|
passed = False
|
|
detail = {"reason": f"{type(exc).__name__}: {exc}"}
|
|
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
|
|
|
|
total = len(per_task)
|
|
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
|
|
meta = fixture_meta(fixtures_path)
|
|
return _build_result(
|
|
meta.get("sub_battery", "formulate"),
|
|
fixtures_path,
|
|
per_task,
|
|
{"structural_match_rate": rate},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Feedback Loop — does observation at t affect state at t+1?
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
def _live_feedback_chain(live_chain: list[dict]) -> dict:
|
|
"""Apply ``live_chain`` ops against a fresh temp arborist shard;
|
|
return the final state for the runner to assert against.
|
|
|
|
Supported ops:
|
|
|
|
- ``append_audit`` — calls :func:`arborist.store.append_audit`
|
|
with the fixture-supplied ``event_type``, ``subject_root``,
|
|
and ``body``.
|
|
- ``memory_snapshot`` — calls
|
|
:func:`arborist.memory.snapshot` +
|
|
:func:`arborist.memory.store_snapshot` to persist a memory
|
|
snapshot.
|
|
- ``selfmodel_snapshot`` — same for SelfModel.
|
|
|
|
Returns ``{"audit_event_types": [...], "memory_branch_ids":
|
|
[...], "memory_event_bodies": [...]}`` — enough for the runner
|
|
to check whether `expected_delta` propagated into downstream
|
|
state.
|
|
"""
|
|
import os
|
|
import tempfile
|
|
import json as _json
|
|
|
|
from arborist.memory import (
|
|
snapshot as memory_snapshot,
|
|
store_snapshot as memory_store,
|
|
)
|
|
from arborist.selfmodel import (
|
|
snapshot as selfmodel_snapshot,
|
|
store_snapshot as selfmodel_store,
|
|
)
|
|
from arborist.store import append_audit, connect, transaction
|
|
|
|
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
|
tmp.close()
|
|
try:
|
|
conn = connect(tmp.name)
|
|
try:
|
|
with transaction(conn):
|
|
for step in live_chain:
|
|
op = step["op"]
|
|
if op == "append_audit":
|
|
append_audit(
|
|
conn,
|
|
event_type=step["event_type"],
|
|
subject_root=step.get("subject_root"),
|
|
body=step.get("body", {}),
|
|
)
|
|
elif op == "memory_snapshot":
|
|
ms = memory_snapshot(conn)
|
|
memory_store(conn, ms)
|
|
elif op == "selfmodel_snapshot":
|
|
sm = selfmodel_snapshot(conn)
|
|
selfmodel_store(conn, sm)
|
|
else:
|
|
raise ValueError(f"unknown live op: {op!r}")
|
|
# Read final state.
|
|
audit_types = [
|
|
row["event_type"] for row in conn.execute(
|
|
"SELECT event_type FROM audit_events ORDER BY seq"
|
|
)
|
|
]
|
|
memory_branches = [
|
|
row["branch_id"] for row in conn.execute(
|
|
"SELECT DISTINCT branch_id FROM memory_branch_summaries"
|
|
)
|
|
]
|
|
event_bodies = [
|
|
row["body"] for row in conn.execute(
|
|
"SELECT body FROM audit_events ORDER BY seq"
|
|
)
|
|
]
|
|
return {
|
|
"audit_event_types": audit_types,
|
|
"memory_branch_ids": memory_branches,
|
|
"audit_event_bodies": event_bodies,
|
|
}
|
|
finally:
|
|
conn.close()
|
|
finally:
|
|
try:
|
|
os.unlink(tmp.name)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _live_delta_satisfied(state: dict, expected: dict) -> tuple[bool, str]:
|
|
"""Check expected_delta predicates against live final state.
|
|
|
|
Supported predicates:
|
|
|
|
- ``audit_event_type_present`` — the named event_type appears at
|
|
least once in the audit chain.
|
|
- ``memory_branch_present`` — the named branch_id appears in
|
|
memory_branch_summaries.
|
|
- ``body_substring_present`` — the substring appears in at least
|
|
one audit event body (loose check).
|
|
"""
|
|
for et in expected.get("audit_event_type_present", []):
|
|
if et not in state["audit_event_types"]:
|
|
return False, f"expected audit event_type {et!r} absent"
|
|
for bid in expected.get("memory_branch_present", []):
|
|
if bid not in state["memory_branch_ids"]:
|
|
return False, f"expected memory branch {bid!r} absent"
|
|
for needle in expected.get("body_substring_present", []):
|
|
if not any(needle in body for body in state["audit_event_bodies"]):
|
|
return False, f"substring {needle!r} absent from audit bodies"
|
|
return True, "all live deltas satisfied"
|
|
|
|
|
|
def run_feedback_loop(fixtures_path: Path) -> BatteryResult:
|
|
"""Integration coverage + feedback_efficiency metric.
|
|
|
|
Two fixture modes (selected per-task):
|
|
|
|
- **Embedded** (Phase 1a): fixture provides a ``chain`` of
|
|
``(operation, observation)`` pairs; runner aggregates the
|
|
observation text and checks ``expected_delta`` (a string
|
|
marker) appears in it.
|
|
- **Live** (Phase 1b.2): fixture provides ``live_chain`` —
|
|
a sequence of ops applied to a fresh temp arborist shard via
|
|
:func:`_live_feedback_chain` — plus an ``expected_delta``
|
|
dict with predicates against the resulting audit chain /
|
|
memory branches. Tests whether observations actually
|
|
propagate into downstream arborist state.
|
|
|
|
Per #000025 §5.5 and the 2026-05-08 fbd99a8 review:
|
|
``feedback_efficiency = downstream_effect_count / Δcapital_cost``
|
|
with explicit zero-cost guards (see :func:`_efficiency`). For
|
|
Phase 1a the per-task cost is the chain length (one cost-unit
|
|
per operation); Phase 1b.2 will use the capital_ledger directly.
|
|
"""
|
|
per_task: list[TaskResult] = []
|
|
integrated = 0
|
|
total_obs = 0
|
|
efficiency_values: list[float] = []
|
|
finite_efficiency_values: list[float] = []
|
|
for task in iter_tasks(fixtures_path):
|
|
task_id = task["id"]
|
|
ok, reason = _carrier_check(task)
|
|
if not ok:
|
|
per_task.append(
|
|
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
|
|
)
|
|
continue
|
|
try:
|
|
if "live_chain" in task:
|
|
source = "live"
|
|
live_chain = task["live_chain"]
|
|
state = _live_feedback_chain(live_chain)
|
|
ok_live, why = _live_delta_satisfied(
|
|
state, task.get("expected_delta", {}) or {}
|
|
)
|
|
integrated_ok = ok_live
|
|
chain_length = len(live_chain)
|
|
total_obs += chain_length
|
|
if integrated_ok:
|
|
integrated += 1
|
|
expected_outcome = task.get("expected", "pass")
|
|
observed = "pass" if integrated_ok else "fail"
|
|
passed = observed == expected_outcome
|
|
downstream_effect = 1.0 if integrated_ok else 0.0
|
|
cost = float(chain_length)
|
|
efficiency = _efficiency(downstream_effect, cost)
|
|
efficiency_values.append(efficiency)
|
|
if efficiency not in (
|
|
EFFICIENCY_INFINITE, -EFFICIENCY_INFINITE
|
|
):
|
|
finite_efficiency_values.append(efficiency)
|
|
detail = {
|
|
"source": source,
|
|
"integrated": integrated_ok,
|
|
"reason": why,
|
|
"chain_length": chain_length,
|
|
"feedback_efficiency": efficiency,
|
|
"live_state": {
|
|
"audit_event_count": len(state["audit_event_types"]),
|
|
"memory_branch_count": len(state["memory_branch_ids"]),
|
|
},
|
|
}
|
|
else:
|
|
source = "embedded"
|
|
chain = task["chain"]
|
|
total_obs += len(chain)
|
|
observed_text = " ".join(
|
|
step.get("observation", "") for step in chain
|
|
).lower()
|
|
final = chain[-1]
|
|
delta_marker = (final.get("expected_delta") or "").lower()
|
|
integrated_ok = bool(delta_marker) and delta_marker in observed_text
|
|
if integrated_ok:
|
|
integrated += 1
|
|
expected_outcome = task.get("expected", "pass")
|
|
observed = "pass" if integrated_ok else "fail"
|
|
passed = observed == expected_outcome
|
|
downstream_effect = 1.0 if integrated_ok else 0.0
|
|
cost = float(len(chain))
|
|
efficiency = _efficiency(downstream_effect, cost)
|
|
efficiency_values.append(efficiency)
|
|
if efficiency not in (
|
|
EFFICIENCY_INFINITE, -EFFICIENCY_INFINITE
|
|
):
|
|
finite_efficiency_values.append(efficiency)
|
|
detail = {
|
|
"source": source,
|
|
"integrated": integrated_ok,
|
|
"delta_marker": delta_marker,
|
|
"chain_length": len(chain),
|
|
"feedback_efficiency": efficiency,
|
|
}
|
|
except Exception as exc: # noqa: BLE001
|
|
passed = False
|
|
detail = {"reason": f"{type(exc).__name__}: {exc}"}
|
|
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
|
|
|
|
total = len(per_task)
|
|
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
|
|
integration_rate = integrated / total_obs if total_obs else 0.0
|
|
mean_finite_efficiency = (
|
|
sum(finite_efficiency_values) / len(finite_efficiency_values)
|
|
if finite_efficiency_values else 0.0
|
|
)
|
|
inf_count = sum(1 for e in efficiency_values if e == EFFICIENCY_INFINITE)
|
|
meta = fixture_meta(fixtures_path)
|
|
return _build_result(
|
|
meta.get("sub_battery", "feedback-loop"),
|
|
fixtures_path,
|
|
per_task,
|
|
{
|
|
"integration_coverage_rate": rate,
|
|
"downstream_effect_rate": integration_rate,
|
|
"feedback_efficiency_mean_finite": mean_finite_efficiency,
|
|
"feedback_efficiency_infinite_count": float(inf_count),
|
|
},
|
|
)
|
|
|
|
|
|
SUB_BATTERIES = {
|
|
"function": run_function,
|
|
"finetuning": run_finetuning,
|
|
"falsification": run_falsification,
|
|
"formulate": run_formulate,
|
|
"feedback-loop": run_feedback_loop,
|
|
}
|