#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:
parent
7676af8fb1
commit
d53115efd7
7 changed files with 460 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue