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.
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
"""Tests for the #000025 §10.11 SelfModel snapshot-chain — the
|
|
persistent lineage that replaces Phase-1a's synthetic parent→child
|
|
pairs in the Finetuning sub-battery.
|
|
|
|
Two surfaces:
|
|
- ``bench/scripts/selfmodel_chain_snapshot.py`` — the bootstrapper
|
|
that appends one chained snapshot per run (capability claims =
|
|
the 5S/5T/5F battery rates).
|
|
- ``bench.batteries.b_5f._chain_finetuning_measure`` + the
|
|
``run_finetuning`` "shard-chain" dispatch — reads the two latest
|
|
snapshots and measures improvement on ``target_capability``.
|
|
|
|
All against temp shards (``tmp_path`` / ``tmp_path_factory``) — never
|
|
the real ``~/.arborist/shards/selfmodel-chain.db``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bench.batteries import b_5f
|
|
from bench.scripts.selfmodel_chain_snapshot import append_snapshot
|
|
|
|
_F5F = Path(__file__).parent.parent / "bench" / "fixtures" / "5f"
|
|
_SHARDCHAIN_PACK = _F5F / "finetuning-shardchain-v1.jsonl"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def chain_shard(tmp_path_factory) -> Path:
|
|
"""A persistent 2-deep SelfModel chain on a temp shard."""
|
|
shard = tmp_path_factory.mktemp("smchain") / "chain.db"
|
|
s1 = append_snapshot(shard, ts=1_700_000_000)
|
|
s2 = append_snapshot(shard, ts=1_700_000_001)
|
|
assert s1["depth"] == 1 and s1["parent_root"] is None
|
|
assert s2["depth"] == 2 and s2["parent_root"] == s1["root"]
|
|
return shard
|
|
|
|
|
|
# ---- bootstrapper -------------------------------------------------------
|
|
|
|
|
|
def test_bootstrapper_grows_chain_one_per_run(tmp_path):
|
|
shard = tmp_path / "c.db"
|
|
a = append_snapshot(shard, ts=1_700_000_000)
|
|
assert a["depth"] == 1
|
|
assert a["parent_root"] is None
|
|
# 15 canonical sub-batteries → 15 capability claims.
|
|
assert len(a["claims"]) == 15
|
|
assert all(v == 1.0 for v in a["claims"].values()) # packs at ceiling
|
|
b = append_snapshot(shard, ts=1_700_000_001)
|
|
assert b["depth"] == 2
|
|
assert b["parent_root"] == a["root"]
|
|
assert b["root"] != a["root"] # distinct parent ⇒ distinct root
|
|
|
|
|
|
def test_bootstrapper_claims_carry_real_metric_names(chain_shard: Path):
|
|
a = append_snapshot(chain_shard, ts=1_700_000_002) # depth 3
|
|
assert a["depth"] == 3
|
|
assert {"5S-syntax", "5T-time", "5F-feedback-loop"} <= set(a["claims"])
|
|
|
|
|
|
# ---- _chain_finetuning_measure -----------------------------------------
|
|
|
|
|
|
def test_chain_measure_reads_two_latest(chain_shard: Path):
|
|
out = b_5f._chain_finetuning_measure(
|
|
{"target_capability": "5F-feedback-loop", "selfmodel_shard": str(chain_shard)}
|
|
)
|
|
assert out["parent_measured"] == 1.0
|
|
assert out["child_measured"] == 1.0
|
|
assert out["parent_root"] != out["child_root"]
|
|
|
|
|
|
def test_chain_measure_env_var_overrides_fixture(chain_shard: Path, monkeypatch):
|
|
monkeypatch.setenv("ARBORIST_SELFMODEL_CHAIN_DB", str(chain_shard))
|
|
out = b_5f._chain_finetuning_measure(
|
|
{"target_capability": "5S-syntax", "selfmodel_shard": "/nonexistent/ignored.db"}
|
|
)
|
|
assert out["child_measured"] == 1.0
|
|
|
|
|
|
def test_chain_measure_absent_shard_raises(tmp_path):
|
|
with pytest.raises(FileNotFoundError, match="chain shard not found"):
|
|
b_5f._chain_finetuning_measure(
|
|
{"target_capability": "5S-syntax", "selfmodel_shard": str(tmp_path / "nope.db")}
|
|
)
|
|
|
|
|
|
def test_chain_measure_single_snapshot_raises(tmp_path):
|
|
shard = tmp_path / "one.db"
|
|
append_snapshot(shard, ts=1_700_000_000) # depth 1
|
|
with pytest.raises(ValueError, match="only one snapshot"):
|
|
b_5f._chain_finetuning_measure(
|
|
{"target_capability": "5S-syntax", "selfmodel_shard": str(shard)}
|
|
)
|
|
|
|
|
|
def test_chain_measure_unknown_metric_raises(chain_shard: Path):
|
|
with pytest.raises(ValueError, match="no claim for"):
|
|
b_5f._chain_finetuning_measure(
|
|
{"target_capability": "NOT-A-REAL-METRIC", "selfmodel_shard": str(chain_shard)}
|
|
)
|
|
|
|
|
|
# ---- run_finetuning shard-chain dispatch -------------------------------
|
|
|
|
|
|
def test_run_finetuning_shardchain_pack_passes(chain_shard: Path, monkeypatch):
|
|
monkeypatch.setenv("ARBORIST_SELFMODEL_CHAIN_DB", str(chain_shard))
|
|
res = b_5f.run_finetuning(_SHARDCHAIN_PACK)
|
|
assert res.pass_count == 6
|
|
assert res.fail_count == 0
|
|
for t in res.per_task:
|
|
assert t.detail["source"] == "shard-chain"
|
|
assert t.detail["improvement"] == 0.0 # packs at ceiling ⇒ no drift
|
|
assert t.detail["chain_parent_root"] != t.detail["chain_child_root"]
|
|
|
|
|
|
def test_run_finetuning_shardchain_pack_fails_honestly_without_chain(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("ARBORIST_SELFMODEL_CHAIN_DB", str(tmp_path / "absent.db"))
|
|
res = b_5f.run_finetuning(_SHARDCHAIN_PACK)
|
|
assert res.pass_count == 0
|
|
assert res.fail_count == 6
|
|
assert "FileNotFoundError" in res.per_task[0].detail["reason"]
|
|
|
|
|
|
def test_embedded_and_live_finetuning_unaffected():
|
|
"""The shard-chain dispatch is gated on ``selfmodel_shard``; the
|
|
embedded + live packs still route through their original paths."""
|
|
emb = b_5f.run_finetuning(_F5F / "finetuning-v1.jsonl")
|
|
assert emb.pass_count == 50
|
|
for t in emb.per_task:
|
|
assert t.detail["source"] == "embedded"
|