#000012 Phase 1c: branch-set persistence — fork_score_branches table + CLI

Phase 1a scores one (parent, child) fork at a time; Phase 1b is the
consensus paper. Neither persists multiple candidate branches at the
same checkpoint — and #000037 §12 Trigger 1 ("ForkScore regularly
receives ≥4 candidate branches per checkpoint") gates the multi-
branch path of the Prometheus-Σ controller on this data existing.
Phase 1c lands the missing seam.

Schema (arborist/store.py): _migrate_fork_score_branches creates the
sibling table with PK (branch_set_id, branch_id) + indexes on
branch_set_id and parent_root. Sibling — never enters
audit_events.event_hash preimage, so re-scoring or back-filling
cannot break the audit chain.

Helpers (arborist/substrate/fork_score.py): persist_branch_score
upserts one row via ON CONFLICT (branch_set_id, branch_id) DO UPDATE
so re-scoring the same fork under the same checkpoint is a clean
overwrite, not a duplicate. branch_set_density(conn, branch_set_id)
returns the count of distinct branches recorded under a checkpoint
— the function the #000037 §12 Trigger 1 probe reads.
ESTIMATOR_VERSION = "fork-score-v1" pins the producer generation on
every persisted row.

CLI (arborist/cli.py): arborist substrate score gains six new flags
(--branch-set, --branch-id, --parent-root, --child-root,
--persist-shard, --weights-id). Default off — --branch-set absent
preserves Phase 1a pure-function semantics for every existing
caller. When present, requires --parent-root and either --branch-id
or --child-root; missing inputs return exit code 2.

Tests (tests/test_fork_score.py, count 18 → 23): migration creates
the table + both indexes; persist writes one row carrying
parent/child roots + verdict + weights_id + estimator_version;
upsert on the PK refreshes child_root + weights_id + recorded_at
without duplicating; branch_set_density counts per-checkpoint and
ignores cross-set rows; breakdown_blob round-trips as canonical
JSON whose values sum to the persisted score.

Status sync: #000012 §7 Phase 1c flipped from "proposed, not yet
open" to "landed 2026-05-10" with the original proposal preserved
below as design log. TICKETS row 117 mirror-updated. AUTOCOUNT
counters in #000012 + cookbook bumped 18 → 23 plus the cookbook's
fork_score.py LOC row refreshed (298 → 386 module, 403 → 609
tests, density 1.35 → 1.58).

End-to-end smoke verified: arborist substrate score writes a
fork_score_branches row with the expected schema (verdict / weights_id
/ estimator_version) and the row survives a clean SQLite read.
This commit is contained in:
russell@unturf.com 2026-05-10 20:12:30 -04:00
parent 7676af8fb1
commit d53115efd7
No known key found for this signature in database
7 changed files with 460 additions and 5 deletions

View file

@ -401,3 +401,209 @@ def test_signal_floor_honored():
# Score = alpha · target_delta = SIGNAL_FLOOR exactly. Verdict ACCEPT.
assert r.score == pytest.approx(SIGNAL_FLOOR, abs=1e-9)
assert r.verdict == "ACCEPT"
# ---------------------------------------------------------------------
# Phase 1c — branch-set persistence (#000012 §7 Phase 1c)
# ---------------------------------------------------------------------
def _scored():
"""Build a representative ScoredFork via the real fork_score()."""
parent = _bench_dict({
"5s": {"syntax": {"parse_pass_rate": 0.5}},
})
child = _bench_dict({
"5s": {"syntax": {"parse_pass_rate": 0.7}},
})
return fork_score(parent, child)
def test_phase1c_migration_creates_table(tmp_path):
"""Opening a connection runs the Phase 1c migration; the table
+ both indexes exist."""
from arborist.store import connect, invalidate_migration_cache
db = tmp_path / "shard.db"
invalidate_migration_cache(db)
conn = connect(db)
try:
row = conn.execute(
"SELECT name FROM sqlite_master "
"WHERE type='table' AND name='fork_score_branches'"
).fetchone()
assert row is not None
idx_names = {
r[0]
for r in conn.execute(
"SELECT name FROM sqlite_master WHERE type='index' "
"AND tbl_name='fork_score_branches'"
).fetchall()
}
assert "idx_fork_score_branches_set" in idx_names
assert "idx_fork_score_branches_parent" in idx_names
finally:
conn.close()
invalidate_migration_cache(db)
def test_phase1c_persist_branch_score_writes_one_row(tmp_path):
from arborist.store import connect, invalidate_migration_cache, transaction
from arborist.substrate.fork_score import (
ESTIMATOR_VERSION,
persist_branch_score,
)
db = tmp_path / "shard.db"
invalidate_migration_cache(db)
conn = connect(db)
try:
with transaction(conn):
persist_branch_score(
conn,
branch_set_id="cp-1",
branch_id="b-A",
parent_root="parent-root-aaaa",
child_root="child-root-A",
scored=_scored(),
weights_id="default",
)
rows = conn.execute(
"SELECT branch_set_id, branch_id, parent_root, child_root, "
" verdict, weights_id, estimator_version "
"FROM fork_score_branches"
).fetchall()
assert len(rows) == 1
r = rows[0]
assert r["branch_set_id"] == "cp-1"
assert r["branch_id"] == "b-A"
assert r["parent_root"] == "parent-root-aaaa"
assert r["child_root"] == "child-root-A"
assert r["verdict"] in ("ACCEPT", "MARGINAL", "REJECT")
assert r["estimator_version"] == ESTIMATOR_VERSION
finally:
conn.close()
invalidate_migration_cache(db)
def test_phase1c_persist_upserts_on_pk(tmp_path):
"""Re-scoring the same (branch_set_id, branch_id) is an upsert,
not a duplicate row. Fields refresh."""
from arborist.store import connect, invalidate_migration_cache, transaction
from arborist.substrate.fork_score import persist_branch_score
db = tmp_path / "shard.db"
invalidate_migration_cache(db)
conn = connect(db)
try:
s1 = _scored()
with transaction(conn):
persist_branch_score(
conn,
branch_set_id="cp-up",
branch_id="b-up",
parent_root="p1",
child_root="c1",
scored=s1,
weights_id="w1",
ts=1700000000,
)
with transaction(conn):
persist_branch_score(
conn,
branch_set_id="cp-up",
branch_id="b-up",
parent_root="p1",
child_root="c2-new",
scored=s1,
weights_id="w2-new",
ts=1700000999,
)
rows = conn.execute(
"SELECT child_root, weights_id, recorded_at "
"FROM fork_score_branches WHERE branch_set_id = 'cp-up'"
).fetchall()
assert len(rows) == 1
assert rows[0]["child_root"] == "c2-new"
assert rows[0]["weights_id"] == "w2-new"
assert rows[0]["recorded_at"] == 1700000999
finally:
conn.close()
invalidate_migration_cache(db)
def test_phase1c_branch_set_density_counts_branches(tmp_path):
"""branch_set_density returns 0 / N for the queried checkpoint;
rows under other checkpoints don't leak."""
from arborist.store import connect, invalidate_migration_cache, transaction
from arborist.substrate.fork_score import (
branch_set_density,
persist_branch_score,
)
db = tmp_path / "shard.db"
invalidate_migration_cache(db)
conn = connect(db)
try:
assert branch_set_density(conn, "missing") == 0
s = _scored()
with transaction(conn):
for i in range(4):
persist_branch_score(
conn,
branch_set_id="cp-A",
branch_id=f"branch-{i}",
parent_root="parent-root",
child_root=f"child-root-{i}",
scored=s,
)
persist_branch_score(
conn,
branch_set_id="cp-B",
branch_id="lone",
parent_root="parent-root",
child_root="child-root-Z",
scored=s,
)
# #000037 §12 Trigger 1 satisfied: ≥4 branches at "cp-A".
assert branch_set_density(conn, "cp-A") == 4
assert branch_set_density(conn, "cp-B") == 1
assert branch_set_density(conn, "cp-missing") == 0
finally:
conn.close()
invalidate_migration_cache(db)
def test_phase1c_breakdown_blob_round_trips_as_json(tmp_path):
"""breakdown_blob stores the per-term breakdown losslessly so a
downstream reader can replay the verdict."""
import json as _json
from arborist.store import connect, invalidate_migration_cache, transaction
from arborist.substrate.fork_score import persist_branch_score
db = tmp_path / "shard.db"
invalidate_migration_cache(db)
conn = connect(db)
try:
scored = _scored()
with transaction(conn):
persist_branch_score(
conn,
branch_set_id="cp-blob",
branch_id="b-blob",
parent_root="p",
child_root="c",
scored=scored,
)
row = conn.execute(
"SELECT breakdown_blob FROM fork_score_branches WHERE "
"branch_set_id='cp-blob'"
).fetchone()
recovered = _json.loads(row["breakdown_blob"])
assert sum(recovered.values()) == pytest.approx(
scored.score, abs=1e-9
)
assert set(recovered.keys()) == set(scored.breakdown.keys())
finally:
conn.close()
invalidate_migration_cache(db)