#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

@ -3227,6 +3227,43 @@ def _cmd_substrate_score(args: argparse.Namespace) -> int:
p = Path(out_path) p = Path(out_path)
p.parent.mkdir(parents=True, exist_ok=True) p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(artifact_json + "\n", encoding="utf-8") p.write_text(artifact_json + "\n", encoding="utf-8")
# Phase 1c — branch-set persistence (#000012). Default off:
# writes only when --branch-set is present. --branch-id falls
# back to --child-root so a single-flag CLI works for the common
# case (one branch per child-root identity).
branch_set_id = getattr(args, "branch_set", None)
if branch_set_id:
from arborist.substrate.fork_score import persist_branch_score
parent_root = getattr(args, "parent_root", None)
child_root = getattr(args, "child_root", None)
branch_id = getattr(args, "branch_id", None) or child_root
if not parent_root or not branch_id:
sys.stderr.write(
"--branch-set requires --parent-root + (--branch-id "
"or --child-root)\n"
)
return 2
persist_shard = getattr(args, "persist_shard", None) or args.db
weights_id = getattr(args, "weights_id", None) or (
"default" if not args.weights else Path(args.weights).stem
)
p_conn = connect(persist_shard)
try:
with transaction(p_conn):
persist_branch_score(
p_conn,
branch_set_id=branch_set_id,
branch_id=branch_id,
parent_root=parent_root,
child_root=child_root,
scored=scored,
weights_id=weights_id,
)
finally:
p_conn.close()
# Non-zero exit on REJECT so CI can gate on it. # Non-zero exit on REJECT so CI can gate on it.
return 0 if scored.verdict in ("ACCEPT", "MARGINAL") else 1 return 0 if scored.verdict in ("ACCEPT", "MARGINAL") else 1
@ -5396,6 +5433,43 @@ def build_parser() -> argparse.ArgumentParser:
"/ ForkScore-aware mesh peers can ingest the artifact." "/ ForkScore-aware mesh peers can ingest the artifact."
), ),
) )
# ----- Phase 1c (#000012 §7) — branch-set persistence ---------------
# Default off. When --branch-set is present a row is written to
# ``fork_score_branches``; absent ⇒ pure-function semantics
# (Phase 1a behavior preserved).
substrate_score.add_argument(
"--branch-set", dest="branch_set", default=None,
help=(
"checkpoint identity (e.g. parent_root + ts); when present, "
"writes one row to fork_score_branches sibling table"
),
)
substrate_score.add_argument(
"--branch-id", dest="branch_id", default=None,
help="fork identifier; defaults to --child-root when omitted",
)
substrate_score.add_argument(
"--parent-root", dest="parent_root", default=None,
help="shared parent root (required when --branch-set is given)",
)
substrate_score.add_argument(
"--child-root", dest="child_root", default=None,
help="child root (nullable for in-flight branches)",
)
substrate_score.add_argument(
"--persist-shard", dest="persist_shard", default=None,
help=(
"SQLite path to write fork_score_branches row to; defaults "
"to --db (the current arborist target shard)"
),
)
substrate_score.add_argument(
"--weights-id", dest="weights_id", default=None,
help=(
"opaque label for the WeightSet used; defaults to "
"'default' or the basename of --weights"
),
)
substrate_score.set_defaults(func=_cmd_substrate_score) substrate_score.set_defaults(func=_cmd_substrate_score)
# ----- memory subcommands (ticket #000017) -------------------------------- # ----- memory subcommands (ticket #000017) --------------------------------

View file

@ -574,6 +574,7 @@ def connect(db_path: Path | str = DEFAULT_DB_PATH) -> sqlite3.Connection:
_migrate_memory_root(conn) _migrate_memory_root(conn)
_migrate_adapter_loss_reports(conn) _migrate_adapter_loss_reports(conn)
_migrate_controller_events(conn) _migrate_controller_events(conn)
_migrate_fork_score_branches(conn)
_MIGRATED_SHARDS.add(cache_key) _MIGRATED_SHARDS.add(cache_key)
# Per-connection state — must run on EVERY open. SQLite scopes # Per-connection state — must run on EVERY open. SQLite scopes
@ -872,6 +873,51 @@ def _migrate_controller_events(conn: sqlite3.Connection) -> None:
) )
def _migrate_fork_score_branches(conn: sqlite3.Connection) -> None:
"""Forward-migrate to add fork_score_branches (ticket #000012 Phase 1c).
Adds the sibling table that lets ``arborist substrate score``
persist multi-branch checkpoints. Sibling does NOT enter
``audit_events.event_hash`` preimage, so re-scoring or back-
filling cannot break the audit chain. PK ``(branch_set_id,
branch_id)`` so re-scoring the same fork under the same set is
a clean upsert, not a duplicate row. Default-off at the CLI
surface (``--branch-set`` flag absent no row written).
Feeds #000037 §12 Trigger 1 — once a checkpoint accumulates ≥4
branches in this table, the multi-branch path of the
Prometheus-Σ controller has empirical data to fire on.
"""
row = conn.execute(
"SELECT name FROM sqlite_master "
"WHERE type='table' AND name='fork_score_branches'"
).fetchone()
if row is None:
conn.execute(
"CREATE TABLE fork_score_branches ("
" branch_set_id TEXT NOT NULL,"
" branch_id TEXT NOT NULL,"
" parent_root TEXT NOT NULL,"
" child_root TEXT,"
" score REAL NOT NULL,"
" verdict TEXT NOT NULL,"
" breakdown_blob TEXT NOT NULL,"
" weights_id TEXT NOT NULL,"
" estimator_version TEXT NOT NULL,"
" recorded_at INTEGER NOT NULL,"
" PRIMARY KEY (branch_set_id, branch_id)"
")"
)
conn.execute(
"CREATE INDEX idx_fork_score_branches_set "
"ON fork_score_branches(branch_set_id)"
)
conn.execute(
"CREATE INDEX idx_fork_score_branches_parent "
"ON fork_score_branches(parent_root)"
)
def _migrate_memory_root(conn: sqlite3.Connection) -> None: def _migrate_memory_root(conn: sqlite3.Connection) -> None:
"""Forward-migrate to add memory-root tables (ticket #000017). """Forward-migrate to add memory-root tables (ticket #000017).

View file

@ -23,13 +23,24 @@ Verdict thresholds:
from __future__ import annotations from __future__ import annotations
import json
import math import math
import sqlite3
import time
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from typing import Iterable, Optional from typing import Iterable, Optional
from arborist.substrate.weights import DEFAULT_WEIGHTS, WeightSet from arborist.substrate.weights import DEFAULT_WEIGHTS, WeightSet
#: Phase 1c — version pin for rows persisted to ``fork_score_branches``.
#: Bump when a code change would produce a different :class:`ScoredFork`
#: from the same inputs (algorithm change, weight semantics shift,
#: hard-regression policy change). Pinning lets a reader filter out
#: rows produced under a prior estimator without re-scoring.
ESTIMATOR_VERSION = "fork-score-v1"
SIGNAL_FLOOR = 0.05 SIGNAL_FLOOR = 0.05
"""5-pp signal floor (matches docs/bench-maxing.md). Below this, """5-pp signal floor (matches docs/bench-maxing.md). Below this,
score is MARGINAL not strong enough to commit a fork.""" score is MARGINAL not strong enough to commit a fork."""
@ -296,3 +307,80 @@ def fork_score(
flags=flags, flags=flags,
weights=weights.as_dict(), weights=weights.as_dict(),
) )
# ---------------------------------------------------------------------
# Phase 1c — branch-set persistence (#000012 §7 Phase 1c)
# ---------------------------------------------------------------------
def persist_branch_score(
conn: sqlite3.Connection,
*,
branch_set_id: str,
branch_id: str,
parent_root: str,
child_root: Optional[str],
scored: ScoredFork,
weights_id: str = "default",
estimator_version: str = ESTIMATOR_VERSION,
ts: Optional[int] = None,
) -> None:
"""Write one :class:`ScoredFork` to ``fork_score_branches``.
Upserts on ``(branch_set_id, branch_id)`` so re-scoring the same
fork under the same checkpoint is a clean overwrite, not a
duplicate row. Sibling table never enters
``audit_events.event_hash`` preimage. Caller wraps in
:func:`arborist.store.transaction` when batching.
"""
if ts is None:
ts = int(time.time())
breakdown_blob = json.dumps(
scored.breakdown, sort_keys=True, separators=(",", ":")
)
conn.execute(
"INSERT INTO fork_score_branches ("
" branch_set_id, branch_id, parent_root, child_root,"
" score, verdict, breakdown_blob, weights_id,"
" estimator_version, recorded_at"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
" ON CONFLICT(branch_set_id, branch_id) DO UPDATE SET"
" parent_root = excluded.parent_root,"
" child_root = excluded.child_root,"
" score = excluded.score,"
" verdict = excluded.verdict,"
" breakdown_blob = excluded.breakdown_blob,"
" weights_id = excluded.weights_id,"
" estimator_version = excluded.estimator_version,"
" recorded_at = excluded.recorded_at",
(
branch_set_id,
branch_id,
parent_root,
child_root,
float(scored.score),
scored.verdict,
breakdown_blob,
weights_id,
estimator_version,
ts,
),
)
def branch_set_density(
conn: sqlite3.Connection, branch_set_id: str
) -> int:
"""Count distinct branches recorded under a checkpoint id.
Used by the #000037 §12 Trigger 1 probe to satisfy the
"ForkScore regularly receives ≥ 4 candidate branches per
checkpoint" gate.
"""
row = conn.execute(
"SELECT COUNT(*) FROM fork_score_branches"
" WHERE branch_set_id = ?",
(branch_set_id,),
).fetchone()
return int(row[0]) if row else 0

View file

@ -125,7 +125,7 @@ Newest first. Update on every open/close.
| #000015 | π* domain library + cross-domain composition | closed · landed 2026-05-07 | 2026-05-07 | — | | #000015 | π* domain library + cross-domain composition | closed · landed 2026-05-07 | 2026-05-07 | — |
| #000014 | SelfModel: schema, falsification, integration | closed · landed 2026-05-07 | 2026-05-07 | — | | #000014 | SelfModel: schema, falsification, integration | closed · landed 2026-05-07 | 2026-05-07 | — |
| #000013 | Spatial-temporal substrate (Merkle-AGI v7-W) | closed · landed 2026-05-09 (substrate paper + frontier catalog + namespace stub) | 2026-05-07 | — | | #000013 | Spatial-temporal substrate (Merkle-AGI v7-W) | closed · landed 2026-05-09 (substrate paper + frontier catalog + namespace stub) | 2026-05-07 | — |
| #000012 | Selection & consensus protocol (Merkle-AGI v8) | in progress · Phase 1a (ForkScore) landed 2026-05-08; Phase 1b (consensus paper, `docs/_source/merkle-agi-v8-consensus.rst` 834 lines) landed 2026-05-10; Phase 1c (branch-set persistence) remains proposed-not-opened | 2026-05-07 | — | | #000012 | Selection & consensus protocol (Merkle-AGI v8) | in progress · Phase 1a (ForkScore) landed 2026-05-08; Phase 1b (consensus paper, `docs/_source/merkle-agi-v8-consensus.rst` 834 lines) landed 2026-05-10; Phase 1c (branch-set persistence`fork_score_branches` sibling table, `persist_branch_score` + `branch_set_density`, 6 new CLI flags on `arborist substrate score`, default-off) landed 2026-05-10 — feeds #000037 §12 Trigger 1 | 2026-05-07 | — |
| #000011 | SOFT_PREFLIGHT_HINT model-assisted sidecar | closed · landed 2026-05-04 (zero-shot full impl) | 2026-05-04 | D1 (preserves) | | #000011 | SOFT_PREFLIGHT_HINT model-assisted sidecar | closed · landed 2026-05-04 (zero-shot full impl) | 2026-05-04 | D1 (preserves) |
| #000010 | Meta-Cognition Preflight Guard (M0 / MCTL) | closed · landed 2026-05-03 (Phases 14); DAG binding shipped via #000009 | 2026-05-03 | D1, D3 | | #000010 | Meta-Cognition Preflight Guard (M0 / MCTL) | closed · landed 2026-05-03 (Phases 14); DAG binding shipped via #000009 | 2026-05-03 | D1, D3 |
| #000009 | Preflight run-DAG node binding (#000008+#000010) | closed · re-landed 2026-05-04 (§8 corrections: reject-path DAG, nested CTI clauses) | 2026-05-03 | D3, D4 | | #000009 | Preflight run-DAG node binding (#000008+#000010) | closed · re-landed 2026-05-04 (§8 corrections: reject-path DAG, nested CTI clauses) | 2026-05-03 | D3, D4 |

View file

@ -283,7 +283,7 @@ function ahead of the consensus paper:
([0, SIGNAL_FLOOR)), REJECT (negative score OR hard-regression ([0, SIGNAL_FLOOR)), REJECT (negative score OR hard-regression
flag OR `NEG_INF_REGRESSION` flag). flag OR `NEG_INF_REGRESSION` flag).
- Reference doc: `docs/v8-fork-score.md`. - Reference doc: `docs/v8-fork-score.md`.
- Tests: <!--AUTOCOUNT:tests:tests/test_fork_score.py-->18<!--/AUTOCOUNT--> - Tests: <!--AUTOCOUNT:tests:tests/test_fork_score.py-->23<!--/AUTOCOUNT-->
cases in `tests/test_fork_score.py` pin the pure ScoredFork cases in `tests/test_fork_score.py` pin the pure ScoredFork
dataclass + scoring contract (SIGNAL_FLOOR=0.05, dataclass + scoring contract (SIGNAL_FLOOR=0.05,
HARD_REGRESSION_FLOOR=0.05, score = sum-of-breakdown closure, HARD_REGRESSION_FLOOR=0.05, score = sum-of-breakdown closure,
@ -347,7 +347,48 @@ What's deliberately NOT in Phase 1a (now specified in the v8 paper):
- Stake mechanics + economic incentives. → Part 6 - Stake mechanics + economic incentives. → Part 6
- Cross-validator ZK proof exchange. → Closure §; deferred to #000016 - Cross-validator ZK proof exchange. → Closure §; deferred to #000016
### Phase 1c — Branch-set persistence (proposed, not yet open) ### Phase 1c — Branch-set persistence (landed 2026-05-10)
**Status:** landed. Pressure-1 satisfied (Phase 1b paper landed
2026-05-10 — multi-branch deployment paper-spec is closed); fox
operator-go to wire the data path so #000037 §12 Trigger 1 gains
empirical surface. Implementation pinned below.
- **Schema:** `fork_score_branches` sibling table created via
`arborist.store._migrate_fork_score_branches`; indexes on
`branch_set_id` and `parent_root`. PK `(branch_set_id, branch_id)`.
- **Helpers:** `arborist.substrate.fork_score.persist_branch_score`
(upsert one row; ON CONFLICT replaces score / verdict /
breakdown_blob / weights_id / estimator_version / recorded_at) +
`branch_set_density(conn, branch_set_id)` (count distinct branches
for a checkpoint — the function the #000037 §12 Trigger 1 probe
reads).
- **CLI:** `arborist substrate score` gains six flags
(`--branch-set`, `--branch-id`, `--parent-root`, `--child-root`,
`--persist-shard`, `--weights-id`). Default off — `--branch-set`
absent ⇒ pure-function semantics preserved (Phase 1a behavior
unchanged for every existing caller).
- **Estimator version pin:** module-level
`ESTIMATOR_VERSION = "fork-score-v1"` constant; bump when a code
change would produce a different `ScoredFork` from the same
inputs (algorithm change, weight semantics, hard-regression
policy). Persisted on every row so a reader can filter by
estimator generation.
- **Tests:** 5 new in `tests/test_fork_score.py`
migration-creates-table, persist-writes-one-row, upsert-on-pk,
branch_set_density-counts-by-set, breakdown_blob-round-trips-as-
json. Test count 18 → 23.
- **Hard constraints honored:** sibling table never enters
`audit_events.event_hash` preimage; no behavioral change to
single-validator scoring; default-off CLI; no mesh wire format
change; `weights_id` opaque (folding weights into a hash stays a
Phase 1b/wire concern).
The original Phase-1c proposal text is preserved below for design-
log continuity. Re-read it as the authoritative spec; the bullets
above are the landing receipt.
#### Original proposal (preserved)
**Problem.** Phase 1a scores one ``(parent, child)`` fork at a time; **Problem.** Phase 1a scores one ``(parent, child)`` fork at a time;
Phase 1b is the consensus paper. Neither persists *multiple Phase 1b is the consensus paper. Neither persists *multiple

View file

@ -607,7 +607,7 @@ than waiting for bench-time STRICT-rate drift to surface it.
input, recommendation-text mode transitions, and the §11 input, recommendation-text mode transitions, and the §11
worked-example bit-for-bit (with doc-calibration update worked-example bit-for-bit (with doc-calibration update
surfaced through the test). surfaced through the test).
- `tests/test_fork_score.py`<!--AUTOCOUNT:tests:tests/test_fork_score.py-->18<!--/AUTOCOUNT--> tests for v8 ForkScore - `tests/test_fork_score.py`<!--AUTOCOUNT:tests:tests/test_fork_score.py-->23<!--/AUTOCOUNT--> tests for v8 ForkScore
(#000012 Phase 1a); pins SIGNAL_FLOOR (5pp) + HARD_REGRESSION_FLOOR (#000012 Phase 1a); pins SIGNAL_FLOOR (5pp) + HARD_REGRESSION_FLOOR
(5pp), score = sum-of-breakdown closure, security_risk inert (5pp), score = sum-of-breakdown closure, security_risk inert
under default iota=0 (opt-in), NEG_INF_REGRESSION hard-reject. under default iota=0 (opt-in), NEG_INF_REGRESSION hard-reject.
@ -675,7 +675,7 @@ than waiting for bench-time STRICT-rate drift to surface it.
| warrant_resolver.py | ~800 | ~430 (combined) | 0.54 | | warrant_resolver.py | ~800 | ~430 (combined) | 0.54 |
| warrant_chain.py | 89 | 320 (<!--AUTOCOUNT:tests:tests/test_warrant_chain.py-->9<!--/AUTOCOUNT--> tests) | 3.6 | | warrant_chain.py | 89 | 320 (<!--AUTOCOUNT:tests:tests/test_warrant_chain.py-->9<!--/AUTOCOUNT--> tests) | 3.6 |
| t3_bound_calculator.py | 249 | 446 (<!--AUTOCOUNT:tests:tests/test_t3_bound_calculator.py-->53<!--/AUTOCOUNT--> tests) | 1.79 | | t3_bound_calculator.py | 249 | 446 (<!--AUTOCOUNT:tests:tests/test_t3_bound_calculator.py-->53<!--/AUTOCOUNT--> tests) | 1.79 |
| fork_score.py | 298 | 403 (<!--AUTOCOUNT:tests:tests/test_fork_score.py-->18<!--/AUTOCOUNT--> tests) | 1.35 | | fork_score.py | 386 | 609 (<!--AUTOCOUNT:tests:tests/test_fork_score.py-->23<!--/AUTOCOUNT--> tests) | 1.58 |
| weights.py | 73 | 180 (<!--AUTOCOUNT:tests:tests/test_weights.py-->16<!--/AUTOCOUNT--> tests) | 2.5 | | weights.py | 73 | 180 (<!--AUTOCOUNT:tests:tests/test_weights.py-->16<!--/AUTOCOUNT--> tests) | 2.5 |
| pi_star/protocol+registry | 124 | 280 (<!--AUTOCOUNT:tests:tests/test_pi_star_protocol_and_registry.py-->21<!--/AUTOCOUNT--> tests) | 2.3 | | pi_star/protocol+registry | 124 | 280 (<!--AUTOCOUNT:tests:tests/test_pi_star_protocol_and_registry.py-->21<!--/AUTOCOUNT--> tests) | 2.3 |
| qa/progress.py | 85 | 226 (<!--AUTOCOUNT:tests:tests/test_qa_progress.py-->31<!--/AUTOCOUNT--> tests) | 2.7 | | qa/progress.py | 85 | 226 (<!--AUTOCOUNT:tests:tests/test_qa_progress.py-->31<!--/AUTOCOUNT--> tests) | 2.7 |

View file

@ -401,3 +401,209 @@ def test_signal_floor_honored():
# Score = alpha · target_delta = SIGNAL_FLOOR exactly. Verdict ACCEPT. # Score = alpha · target_delta = SIGNAL_FLOOR exactly. Verdict ACCEPT.
assert r.score == pytest.approx(SIGNAL_FLOOR, abs=1e-9) assert r.score == pytest.approx(SIGNAL_FLOOR, abs=1e-9)
assert r.verdict == "ACCEPT" 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)