Closes the three open Phase-1b items of #000025; every §10 closure criterion is now met, so the ticket flips to closed. §10.14 — ForkScore threshold-calibration handoff to #000012. bench/scripts/fivef_threshold_calibration.py (make bench-5f-threshold- calibration) runs the canonical 5S/5T/5F packs + the 5F live packs and reports baseline rates, observability granularity (1/n), and fork_score verdicts on the parent vs synthetic child perturbations → bench/results/5f-threshold-calibration-2026-05-11.md. Findings written into ticket-000012 §8: keep SIGNAL_FLOOR / HARD_REGRESSION_FLOOR at 0.05; the small 5S packs (syntax n=10, semantics n=8) are coarser than the floors so any regression there trips hard-reject (intended zero- tolerance); the 5x averaging dilution in _delta_*; ceiling saturation (every pack at 1.0 -> delta-rate terms <= 0). No constant change shipped. 6 tests in tests/test_fivef_threshold_calibration.py. §10.13 — feedback latency / efficiency on real workload. run_feedback_loop now computes feedback_latency (listed in §5.5 since Phase 1a, never implemented) — wall-clock seconds to apply a live chain against its temp shard, surfaced per-task (feedback_latency_seconds) + battery (feedback_latency_mean_seconds, feedback_live_task_count). For live chains feedback_efficiency's cost denominator switched from len(chain) (count of requested ops) to the persisted footprint _persisted_cost = audit-event rows the chain actually wrote + their body bytes / 1e6. Embedded chains keep len(chain) and report feedback_latency_seconds = None. Latency is a wall-clock field (run-to-run variable, like BatteryResult.timestamp) and is not a fork_score input. 3 tests in tests/test_bench_batteries.py. §10.11 — real selfmodel finetuning chains. bench/scripts/selfmodel_chain_snapshot.py (make bench-5f-selfmodel- snapshot) appends one chained SelfModel snapshot per run to a persistent shard (~/.arborist/shards/selfmodel-chain.db, override via ARBORIST_SELFMODEL_CHAIN_DB) with one CapabilityClaim per sub-battery (metric = "5S-syntax" etc., measured_value = that pack's rate, eval_digest = the pack's fixture digest, threshold = SIGNAL_FLOOR). snapshot() auto-parents, so each snapshot is a distinct root and the lineage grows by one per run. run_finetuning gains a third dispatch mode — shard-chain (gated on a task's selfmodel_shard key) — via _chain_finetuning_measure: reads the two most-recent snapshots (latest() = child, its parent_selfmodel_root = parent) and measures improvement on target_capability between them. This is the real lineage replacing Phase-1a's synthetic parent->child pairs; the chained delta reflects genuine cross-run drift (0.0 today — the embedded packs are at ceiling). Operator pack bench/fixtures/5f/finetuning-shardchain-v1.jsonl (6 tasks) + make bench-5f-finetuning-shardchain; not in `make bench-5f`, `make test`, or a fresh checkout (a missing/too-short chain fails honestly). The real chain shard was bootstrapped 2-deep on 2026-05-11; make chain-check-shards reports 0 breaks on it (and all other shards). 10 tests in tests/test_selfmodel_chain.py. Full suite: 2311 passed, 28 skipped.
994 lines
40 KiB
Python
994 lines
40 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
|
|
|
|
import time
|
|
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 _chain_finetuning_measure(task: dict) -> dict:
|
|
"""Read the two most-recent SelfModel snapshots from a *persistent*
|
|
chain shard (#000025 §10.11) and return the parent/child measured
|
|
values for ``target_capability``.
|
|
|
|
The shard is the one ``bench/scripts/selfmodel_chain_snapshot.py``
|
|
appends to (default ``~/.arborist/shards/selfmodel-chain.db``,
|
|
overridable per-task via ``selfmodel_shard`` and via the
|
|
``ARBORIST_SELFMODEL_CHAIN_DB`` env var). Unlike the Phase-1b.2
|
|
"live" path — which fabricates a parent + child in a fresh temp
|
|
shard every run — this reads a genuine lineage that outlives the
|
|
process: ``latest()`` is the child, its ``parent_selfmodel_root``
|
|
is the parent.
|
|
|
|
Raises if the shard is absent, has < 2 snapshots, or neither
|
|
snapshot carries a claim for ``target_capability`` — the caller
|
|
turns that into an honest task failure (this pack is meant to run
|
|
*after* the chain has been bootstrapped, not on a fresh checkout).
|
|
"""
|
|
import os as _os
|
|
|
|
from arborist.selfmodel.store import claims_for, latest, load
|
|
from arborist.store import connect
|
|
|
|
metric = task["target_capability"]
|
|
# Resolution order: ARBORIST_SELFMODEL_CHAIN_DB env (the test /
|
|
# operator override) → the fixture's ``selfmodel_shard`` value
|
|
# (which is also the dispatch gate) → the conventional default.
|
|
raw_path = (
|
|
_os.environ.get("ARBORIST_SELFMODEL_CHAIN_DB")
|
|
or task.get("selfmodel_shard")
|
|
or "~/.arborist/shards/selfmodel-chain.db"
|
|
)
|
|
shard = Path(_os.path.expandvars(str(raw_path))).expanduser()
|
|
if not shard.exists():
|
|
raise FileNotFoundError(
|
|
f"selfmodel chain shard not found: {shard} — run "
|
|
"`make bench-5f-selfmodel-snapshot` (twice) to bootstrap it"
|
|
)
|
|
conn = connect(shard)
|
|
try:
|
|
child_row = latest(conn)
|
|
if child_row is None:
|
|
raise ValueError(f"no SelfModel snapshots in {shard}")
|
|
parent_root = child_row.get("parent_selfmodel_root")
|
|
if not parent_root:
|
|
raise ValueError(
|
|
f"chain shard {shard} has only one snapshot; need >= 2 "
|
|
"(run the bootstrapper again to extend the lineage)"
|
|
)
|
|
parent_row = load(conn, parent_root)
|
|
if parent_row is None:
|
|
raise ValueError(f"parent snapshot {parent_root[:12]}… missing from {shard}")
|
|
|
|
def _measured(root: str) -> float:
|
|
for c in claims_for(conn, root):
|
|
if c["metric"] == metric and c["measured_value"] is not None:
|
|
return float(c["measured_value"])
|
|
raise ValueError(f"no claim for {metric!r} on snapshot {root[:12]}…")
|
|
|
|
return {
|
|
"parent_root": parent_root,
|
|
"child_root": child_row["selfmodel_root"],
|
|
"parent_measured": _measured(parent_root),
|
|
"child_measured": _measured(child_row["selfmodel_root"]),
|
|
}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
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``.
|
|
|
|
Three fixture modes (selected per-task):
|
|
|
|
- **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.
|
|
- **Shard-chain** (§10.11 — gate via ``"selfmodel_shard"``):
|
|
runner reads the two most-recent snapshots from a *persistent*
|
|
chain shard (the one ``bench/scripts/selfmodel_chain_snapshot.py``
|
|
grows, one snapshot per run) and measures improvement on
|
|
``target_capability`` between them. This is the real lineage —
|
|
not a pair fabricated for the test — so the chained Δ reflects
|
|
genuine cross-run drift (0.0 today; the packs are at ceiling).
|
|
A task whose shard is absent / too short fails honestly: the
|
|
pack runs *after* the bootstrapper, not on a fresh checkout.
|
|
|
|
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("selfmodel_shard"):
|
|
source = "shard-chain"
|
|
chain = _chain_finetuning_measure(task)
|
|
parent = float(chain["parent_measured"])
|
|
child = float(chain["child_measured"])
|
|
elif 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,
|
|
}
|
|
if source == "shard-chain":
|
|
detail["chain_parent_root"] = chain["parent_root"]
|
|
detail["chain_child_root"] = chain["child_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
|
|
# 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 _persisted_cost(state: dict) -> float:
|
|
"""Real-workload cost of a live feedback chain: the persisted
|
|
footprint it actually produced, not the count of *requested* ops.
|
|
|
|
``audit_event`` rows the chain wrote (count) + their body bytes /
|
|
1e6 (matching :func:`_capital_cost_delta`'s 1 MB = 1 cost-unit
|
|
convention). Every non-empty live chain writes ≥ 1 audit event
|
|
(the ``append_audit`` op, the ``memory_snapshot_landed`` event,
|
|
etc.), so this is > 0 for any chain that did work — :func:`_efficiency`
|
|
still handles the cost-0 edge as documented.
|
|
"""
|
|
n_audit = len(state.get("audit_event_types", []))
|
|
body_bytes = sum(len(b) for b in state.get("audit_event_bodies", []) if b)
|
|
return float(n_audit) + body_bytes / 1e6
|
|
|
|
|
|
def run_feedback_loop(fixtures_path: Path) -> BatteryResult:
|
|
"""Integration coverage + feedback_efficiency + feedback_latency.
|
|
|
|
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. No real work happens, so latency is
|
|
``None`` and the cost-proxy stays ``len(chain)``.
|
|
- **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`).
|
|
|
|
**§10.13 (2026-05-11) — calibrated against real workload.** For a
|
|
live chain the cost is the *persisted footprint* :func:`_persisted_cost`
|
|
— the audit-event rows the chain actually wrote plus their body
|
|
bytes — instead of the Phase-1a ``len(chain)`` proxy (which
|
|
counted *requested* ops, not work done). §10.13 also computes
|
|
``feedback_latency`` — listed in §5.5 since Phase 1a but never
|
|
implemented — as the wall-clock seconds to apply a live chain
|
|
against its temp shard. Latency is a wall-clock field
|
|
(non-deterministic run-to-run, like ``BatteryResult.timestamp``)
|
|
and is **not** consumed by ``fork_score`` — it's surfaced for
|
|
operational observability + the capital ledger (#000020), not for
|
|
fork selection. Embedded tasks keep ``len(chain)`` as cost and
|
|
report ``feedback_latency_seconds = None``.
|
|
"""
|
|
per_task: list[TaskResult] = []
|
|
integrated = 0
|
|
total_obs = 0
|
|
efficiency_values: list[float] = []
|
|
finite_efficiency_values: list[float] = []
|
|
live_latencies: 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"]
|
|
_t0 = time.perf_counter()
|
|
state = _live_feedback_chain(live_chain)
|
|
latency_s = time.perf_counter() - _t0
|
|
live_latencies.append(latency_s)
|
|
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 = _persisted_cost(state) # §10.13: real footprint
|
|
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,
|
|
"feedback_latency_seconds": latency_s,
|
|
"persisted_cost": cost,
|
|
"persisted_audit_rows": len(state.get("audit_event_types", [])),
|
|
"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,
|
|
"feedback_latency_seconds": None, # no real work
|
|
}
|
|
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)
|
|
# §10.13: mean wall-clock latency over the live tasks (0.0 when a
|
|
# pack has no live_chain tasks). Wall-clock, run-to-run variable —
|
|
# not a fork_score input.
|
|
feedback_latency_mean = (
|
|
sum(live_latencies) / len(live_latencies) if live_latencies else 0.0
|
|
)
|
|
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),
|
|
"feedback_latency_mean_seconds": feedback_latency_mean,
|
|
"feedback_live_task_count": float(len(live_latencies)),
|
|
},
|
|
)
|
|
|
|
|
|
SUB_BATTERIES = {
|
|
"function": run_function,
|
|
"finetuning": run_finetuning,
|
|
"falsification": run_falsification,
|
|
"formulate": run_formulate,
|
|
"feedback-loop": run_feedback_loop,
|
|
}
|