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.
171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
"""Append one SelfModel snapshot to a persistent chain shard, with
|
|
capability claims drawn from a fresh 5S/5T/5F bench run (#000025 §10.11).
|
|
|
|
Why this exists: Phase 1a's Finetuning sub-battery scored *synthetic*
|
|
parent→child SelfModel pairs; Phase 1b.2's "live" path round-trips a
|
|
parent + child through a *fresh temp* shard each run. §10.11 wants the
|
|
real thing — a SelfModel lineage that *persists across runs*, so the
|
|
Finetuning runner can measure improvement between two genuine,
|
|
chained snapshots instead of a pair fabricated for the occasion.
|
|
|
|
Each invocation:
|
|
|
|
1. Runs the canonical 5S/5T/5F sub-batteries (embedded packs — the
|
|
ones `fork_score._BATTERY_RATE_KEYS` reads). Deterministic, ~1s,
|
|
no LLM call.
|
|
2. Builds one :class:`arborist.selfmodel.CapabilityClaim` per
|
|
sub-battery: ``metric = "5S-syntax"`` etc., ``measured_value`` =
|
|
that pack's rate, ``eval_digest`` = that pack's fixture digest,
|
|
``threshold`` = ``fork_score.SIGNAL_FLOOR`` (the rate floor below
|
|
which a fork-score Δ on this capability is a hard regression).
|
|
3. Calls :func:`arborist.selfmodel.snapshot` (which auto-parents to
|
|
the latest root in the shard) → :func:`with_claims` →
|
|
:func:`store_snapshot`. The new snapshot's distinct parent makes
|
|
it a distinct root, so the chain grows by exactly one per run.
|
|
|
|
Output shard defaults to ``~/.arborist/shards/selfmodel-chain.db``
|
|
(override with ``--shard`` or ``ARBORIST_SELFMODEL_CHAIN_DB``). The
|
|
shard carries an ``audit_events`` table (every ``store_snapshot``
|
|
write chains through it), so ``make chain-check-shards`` covers it.
|
|
|
|
Idempotent on content: re-running with no chain growth in between is
|
|
a no-op only if the parent root is unchanged — which it isn't after
|
|
the first append, so successive runs always extend the lineage. The
|
|
bench rates are at ceiling (1.0) today, so the chained Δ is 0.0; the
|
|
point is that the *mechanism* is real and the lineage outlives any
|
|
single process.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import time
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(_REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_REPO_ROOT))
|
|
|
|
from bench.batteries import b_5f, b_5s, b_5t # noqa: E402
|
|
from arborist.selfmodel import CapabilityClaim, snapshot, store_snapshot # noqa: E402
|
|
from arborist.selfmodel.canonical import with_claims # noqa: E402
|
|
from arborist.selfmodel.store import claims_for, latest # noqa: E402
|
|
from arborist.store import connect, transaction # noqa: E402
|
|
from arborist.substrate.fork_score import SIGNAL_FLOOR, _BATTERY_RATE_KEYS # noqa: E402
|
|
|
|
_FIX = _REPO_ROOT / "bench" / "fixtures"
|
|
|
|
# Canonical sub-battery → embedded fixture pack (the keys fork_score
|
|
# reads). Mirrors bench.batteries.runner._DEFAULT_FIXTURES minus the
|
|
# legacy 5t "transfer" the scorer ignores.
|
|
_PACKS: list[tuple[str, str, Path]] = [
|
|
("5s", "syntax", _FIX / "5s" / "syntax-v1.jsonl"),
|
|
("5s", "semantics", _FIX / "5s" / "semantics-v1.jsonl"),
|
|
("5s", "syllogism", _FIX / "5s" / "syllogism-v1.jsonl"),
|
|
("5s", "synthesis", _FIX / "5s" / "synthesis-v1.jsonl"),
|
|
("5s", "semiotics", _FIX / "5s" / "semiotics-v1.jsonl"),
|
|
("5t", "transfer-learning", _FIX / "5t" / "transfer-learning-v2.jsonl"),
|
|
("5t", "triangulation", _FIX / "5t" / "triangulation-v1.jsonl"),
|
|
("5t", "truthtables", _FIX / "5t" / "truthtables-v1.jsonl"),
|
|
("5t", "transitivity", _FIX / "5t" / "transitivity-v1.jsonl"),
|
|
("5t", "time", _FIX / "5t" / "time-v1.jsonl"),
|
|
("5f", "function", _FIX / "5f" / "function-v1.jsonl"),
|
|
("5f", "finetuning", _FIX / "5f" / "finetuning-v1.jsonl"),
|
|
("5f", "falsification", _FIX / "5f" / "falsification-v1.jsonl"),
|
|
("5f", "formulate", _FIX / "5f" / "formulate-v1.jsonl"),
|
|
("5f", "feedback-loop", _FIX / "5f" / "feedback-loop-v1.jsonl"),
|
|
]
|
|
|
|
_SUB_RUNNERS = {"5s": b_5s.SUB_BATTERIES, "5t": b_5t.SUB_BATTERIES, "5f": b_5f.SUB_BATTERIES}
|
|
|
|
|
|
def default_shard() -> Path:
|
|
env = os.environ.get("ARBORIST_SELFMODEL_CHAIN_DB")
|
|
if env:
|
|
return Path(env).expanduser()
|
|
return Path.home() / ".arborist" / "shards" / "selfmodel-chain.db"
|
|
|
|
|
|
def _battery_claims(measured_at: int) -> list[CapabilityClaim]:
|
|
claims: list[CapabilityClaim] = []
|
|
for battery, sub, path in _PACKS:
|
|
res = _SUB_RUNNERS[battery][sub](path)
|
|
metric_key = _BATTERY_RATE_KEYS[battery][sub]
|
|
rate = float(res.metrics.get(metric_key, 0.0))
|
|
claims.append(
|
|
CapabilityClaim(
|
|
metric=f"{battery.upper()}-{sub}",
|
|
threshold=float(SIGNAL_FLOOR),
|
|
eval_digest=res.fixture_digest,
|
|
measured_value=rate,
|
|
measured_at=measured_at,
|
|
validity_horizon="next-checkpoint",
|
|
claim_text=f"{battery.upper()} {sub} {metric_key} on the embedded pack",
|
|
)
|
|
)
|
|
return claims
|
|
|
|
|
|
def append_snapshot(shard: Path, *, ts: int | None = None) -> dict:
|
|
"""Run the suite, append one chained SelfModel snapshot, return a
|
|
summary dict ``{root, parent_root, depth, claims: [...]}``."""
|
|
if ts is None:
|
|
ts = int(time.time())
|
|
shard.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = connect(shard)
|
|
try:
|
|
prev = latest(conn)
|
|
claims = _battery_claims(ts)
|
|
with transaction(conn):
|
|
sm = with_claims(snapshot(conn), claims)
|
|
root = store_snapshot(conn, sm, claims=claims, ts=ts)
|
|
# Walk the parent chain to report depth.
|
|
depth = 1
|
|
cur = load_parent(conn, root)
|
|
while cur is not None:
|
|
depth += 1
|
|
cur = load_parent(conn, cur)
|
|
rows = claims_for(conn, root)
|
|
return {
|
|
"shard": str(shard),
|
|
"root": root,
|
|
"parent_root": prev["selfmodel_root"] if prev else None,
|
|
"depth": depth,
|
|
"claims": {r["metric"]: r["measured_value"] for r in rows},
|
|
}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def load_parent(conn, root: str) -> str | None:
|
|
row = conn.execute(
|
|
"SELECT parent_selfmodel_root FROM selfmodel_records WHERE selfmodel_root = ?",
|
|
(root,),
|
|
).fetchone()
|
|
if row is None:
|
|
return None
|
|
return row["parent_selfmodel_root"]
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
p.add_argument(
|
|
"--shard",
|
|
type=Path,
|
|
default=None,
|
|
help="Chain shard path (default: $ARBORIST_SELFMODEL_CHAIN_DB or ~/.arborist/shards/selfmodel-chain.db)",
|
|
)
|
|
args = p.parse_args(argv)
|
|
shard = args.shard.expanduser() if args.shard else default_shard()
|
|
summary = append_snapshot(shard)
|
|
import json as _json
|
|
|
|
print(_json.dumps(summary, indent=2, default=str))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|