arborist/tests/test_fork_score.py
russell@unturf.com d53115efd7
#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.
2026-05-10 20:12:30 -04:00

609 lines
22 KiB
Python

"""Tests for ``arborist.substrate.fork_score`` — pure-function
ForkScore computation over (parent, child) BatteryResult bundles
(ticket #000012 Phase 1a).
Covers:
- bench_result_to_metrics adapter (BatteryResult → nested dict)
- fork_score over the three rate-deltas (5S, 5T, 5F) and the
five non-bench inputs
- hard-reject paths: NEG_INF_REGRESSION, per-battery
HARD_REGRESSION_FLOOR
- verdict band: ACCEPT (≥SIGNAL_FLOOR), MARGINAL ([0, SIGNAL_FLOOR)),
REJECT (<0 or hard-reject)
- empty inputs (zero parent, zero child) → score 0, verdict
MARGINAL
- ScoredFork.to_dict serializability
- breakdown sum identity (score ≡ Σ breakdown)
"""
from __future__ import annotations
import math
import pytest
from arborist.substrate.fork_score import (
HARD_REGRESSION_FLOOR,
INFINITE_BONUS_CAP,
SIGNAL_FLOOR,
ScoredFork,
bench_result_to_metrics,
fork_score,
)
from arborist.substrate.weights import DEFAULT_WEIGHTS, WeightSet
# --- bench_result_to_metrics adapter --------------------------------
def test_bench_result_to_metrics_groups_by_battery():
"""Adapter from runner JSON to nested-dict shape."""
payload = {
"results": [
{"battery": "5s", "sub_battery": "syntax",
"metrics": {"parse_pass_rate": 0.9}},
{"battery": "5s", "sub_battery": "semantics",
"metrics": {"equivalence_recovery_rate": 0.85}},
{"battery": "5t", "sub_battery": "transfer-learning",
"metrics": {"transfer_learning_success_rate": 0.7}},
{"battery": "5f", "sub_battery": "function",
"metrics": {"function_pass_rate": 0.95}},
],
}
out = bench_result_to_metrics(payload)
assert set(out.keys()) == {"5s", "5t", "5f"}
assert out["5s"]["syntax"]["parse_pass_rate"] == 0.9
assert out["5s"]["semantics"]["equivalence_recovery_rate"] == 0.85
assert out["5t"]["transfer-learning"]["transfer_learning_success_rate"] == 0.7
assert out["5f"]["function"]["function_pass_rate"] == 0.95
def test_bench_result_to_metrics_skips_unknown_battery():
"""Battery keys outside {5s, 5t, 5f} get dropped."""
payload = {
"results": [
{"battery": "5r", "sub_battery": "rho",
"metrics": {"rho_rate": 0.5}},
{"battery": "5s", "sub_battery": "syntax",
"metrics": {"parse_pass_rate": 0.8}},
],
}
out = bench_result_to_metrics(payload)
assert "5r" not in out
assert out["5s"]["syntax"]["parse_pass_rate"] == 0.8
def test_bench_result_to_metrics_empty_payload():
"""No `results` key → empty nested dicts (still keyed by battery)."""
out = bench_result_to_metrics({})
assert out == {"5s": {}, "5t": {}, "5f": {}}
# --- fork_score happy path ------------------------------------------
def _bench_dict(rates_by_battery_sub: dict[str, dict[str, dict[str, float]]]):
"""Helper: build a nested metrics dict directly."""
out = {"5s": {}, "5t": {}, "5f": {}}
for battery, subs in rates_by_battery_sub.items():
out[battery] = {sub: dict(metrics) for sub, metrics in subs.items()}
return out
def test_fork_score_pure_improvement_accepts():
"""Child improves on every 5S sub-battery by 10pp → score
≥ SIGNAL_FLOOR → ACCEPT verdict."""
parent = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.5},
"semantics": {"equivalence_recovery_rate": 0.5},
"syllogism": {"step_validity_rate": 0.5},
"synthesis": {"derivation_pass_rate": 0.5},
"semiotics": {"invariance_under_swap": 0.5},
},
})
child = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.6},
"semantics": {"equivalence_recovery_rate": 0.6},
"syllogism": {"step_validity_rate": 0.6},
"synthesis": {"derivation_pass_rate": 0.6},
"semiotics": {"invariance_under_swap": 0.6},
},
})
r = fork_score(parent, child)
assert r.verdict == "ACCEPT"
assert r.score >= SIGNAL_FLOOR
def test_fork_score_marginal_band():
"""Child improves by less than SIGNAL_FLOOR's mapped weight
→ MARGINAL band (score in [0, SIGNAL_FLOOR))."""
# Tiny improvement — 1pp on one sub-battery — well below the
# signal floor when averaged.
parent = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.50},
"semantics": {"equivalence_recovery_rate": 0.50},
"syllogism": {"step_validity_rate": 0.50},
"synthesis": {"derivation_pass_rate": 0.50},
"semiotics": {"invariance_under_swap": 0.50},
},
})
child = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.51},
"semantics": {"equivalence_recovery_rate": 0.50},
"syllogism": {"step_validity_rate": 0.50},
"synthesis": {"derivation_pass_rate": 0.50},
"semiotics": {"invariance_under_swap": 0.50},
},
})
r = fork_score(parent, child)
assert r.verdict == "MARGINAL"
assert 0 <= r.score < SIGNAL_FLOOR
def test_fork_score_zero_zero_yields_zero():
"""Zero parent + zero child → score 0 (no terms fire) → MARGINAL
(score in [0, SIGNAL_FLOOR))."""
r = fork_score({"5s": {}, "5t": {}, "5f": {}},
{"5s": {}, "5t": {}, "5f": {}})
assert r.score == pytest.approx(0.0, abs=1e-12)
assert r.verdict == "MARGINAL"
# --- regression / hard-reject paths ---------------------------------
def test_fork_score_hard_regression_5s_rejects():
"""Drop ≥HARD_REGRESSION_FLOOR on a 5S sub → REJECT regardless
of overall positive score."""
parent = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.9},
"semantics": {"equivalence_recovery_rate": 0.5},
"syllogism": {"step_validity_rate": 0.5},
"synthesis": {"derivation_pass_rate": 0.5},
"semiotics": {"invariance_under_swap": 0.5},
},
})
# syntax drops by 0.20 (>= 0.05 hard floor); other subs improve.
child = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.7},
"semantics": {"equivalence_recovery_rate": 0.95},
"syllogism": {"step_validity_rate": 0.95},
"synthesis": {"derivation_pass_rate": 0.95},
"semiotics": {"invariance_under_swap": 0.95},
},
})
r = fork_score(parent, child)
assert r.verdict == "REJECT"
assert any("REGRESSION_5S" in f and "syntax" in f for f in r.flags)
def test_fork_score_negative_score_rejects():
"""Score < 0 (more decreases than increases) → REJECT."""
parent = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.8},
"semantics": {"equivalence_recovery_rate": 0.8},
"syllogism": {"step_validity_rate": 0.8},
"synthesis": {"derivation_pass_rate": 0.8},
"semiotics": {"invariance_under_swap": 0.8},
},
})
# All decrease by 0.04 (less than HARD_REGRESSION_FLOOR=0.05),
# so no per-sub regression flag — but mean delta is negative
# → score < 0 → REJECT
child = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.76},
"semantics": {"equivalence_recovery_rate": 0.76},
"syllogism": {"step_validity_rate": 0.76},
"synthesis": {"derivation_pass_rate": 0.76},
"semiotics": {"invariance_under_swap": 0.76},
},
})
r = fork_score(parent, child)
assert r.verdict == "REJECT"
assert r.score < 0
def test_fork_score_neg_infinite_count_in_5f_hard_rejects():
"""`adaptation_efficiency_neg_infinite_count > 0` on child →
auto-REJECT regardless of other terms."""
parent = _bench_dict({
"5f": {
"function": {"function_pass_rate": 0.5},
"finetuning": {"adaptation_improvement_rate": 0.5},
"falsification": {"error_detection_rate": 0.5},
"formulate": {"structural_match_rate": 0.5},
"feedback-loop": {"integration_coverage_rate": 0.5},
},
})
child = _bench_dict({
"5f": {
"function": {"function_pass_rate": 0.95},
"finetuning": {
"adaptation_improvement_rate": 0.95,
"adaptation_efficiency_neg_infinite_count": 1,
},
"falsification": {"error_detection_rate": 0.95},
"formulate": {"structural_match_rate": 0.95},
"feedback-loop": {"integration_coverage_rate": 0.95},
},
})
r = fork_score(parent, child)
assert r.verdict == "REJECT"
assert any("NEG_INF_REGRESSION" in f for f in r.flags)
# --- non-bench input terms ------------------------------------------
def test_fork_score_capital_cost_penalty_subtracts():
"""capital_delta > 0 reduces the score (cost penalty)."""
parent = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
child = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
no_penalty = fork_score(parent, child, capital_delta=0.0)
with_penalty = fork_score(parent, child, capital_delta=10.0)
# capital_delta term is -theta * capital_delta — so positive
# capital_delta should REDUCE the score
assert with_penalty.score < no_penalty.score
def test_fork_score_audit_completeness_adds():
"""audit_completeness > 0 increases the score (positive term)."""
parent = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
child = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
base = fork_score(parent, child)
bonus = fork_score(parent, child, audit_completeness=1.0)
assert bonus.score > base.score
def test_fork_score_security_risk_subtracts_when_iota_positive():
"""security_risk reduces the score when the iota weight is
non-zero. Default WeightSet sets iota=0 (security risk is opt-
in for the validator), so we test with an explicit iota>0."""
parent = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
child = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
iota_on = WeightSet(
alpha=DEFAULT_WEIGHTS.alpha, beta=DEFAULT_WEIGHTS.beta,
gamma=DEFAULT_WEIGHTS.gamma, delta=DEFAULT_WEIGHTS.delta,
epsilon=DEFAULT_WEIGHTS.epsilon, zeta=DEFAULT_WEIGHTS.zeta,
eta=DEFAULT_WEIGHTS.eta, theta=DEFAULT_WEIGHTS.theta,
iota=1.0, # turn on
kappa=DEFAULT_WEIGHTS.kappa, lambda_=DEFAULT_WEIGHTS.lambda_,
)
base = fork_score(parent, child, weights=iota_on)
risky = fork_score(parent, child, weights=iota_on, security_risk=1.0)
assert risky.score < base.score
def test_fork_score_security_risk_inert_under_default_weights():
"""Honest documentation of the default behavior: with iota=0
(the default), passing security_risk does NOT change the score.
This is by design — operators must opt-in to the security-risk
penalty by setting iota>0."""
parent = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
child = _bench_dict({"5s": {}, "5t": {}, "5f": {}})
assert DEFAULT_WEIGHTS.iota == 0.0, (
"test assumes iota=0 default; update if WeightSet defaults change"
)
base = fork_score(parent, child)
with_risk = fork_score(parent, child, security_risk=1.0)
assert with_risk.score == pytest.approx(base.score, abs=1e-12)
# --- weights customization -----------------------------------------
def test_fork_score_weights_recorded_in_output():
"""Custom WeightSet flows through to the output's weights dict."""
custom = WeightSet(alpha=0.5, beta=0.5, gamma=0.5,
delta=0.1, epsilon=0.1, zeta=0.1,
eta=1.0, theta=1.0, iota=1.0,
kappa=1.0, lambda_=1.0)
r = fork_score({}, {}, weights=custom)
assert r.weights["alpha"] == pytest.approx(0.5)
assert r.weights["lambda_"] == pytest.approx(1.0)
def test_fork_score_default_weights_used_when_unspecified():
r = fork_score({}, {})
assert r.weights == DEFAULT_WEIGHTS.as_dict()
# --- ScoredFork API -------------------------------------------------
def test_scored_fork_to_dict_serializable():
"""ScoredFork.to_dict() returns JSON-serializable types."""
r = fork_score({}, {})
d = r.to_dict()
import json
json.dumps(d) # must not raise
assert "score" in d
assert "verdict" in d
assert "breakdown" in d
assert "flags" in d
assert "weights" in d
# --- score = sum(breakdown) closure ---------------------------------
def test_score_equals_sum_of_breakdown():
"""Closure check: score ≡ Σ breakdown values. Multiple
parent/child configurations to widen the cone."""
configs = [
# Plain improvement on 5S only.
(
{"5s": {"syntax": {"parse_pass_rate": 0.5}}},
{"5s": {"syntax": {"parse_pass_rate": 0.7}}},
),
# Mixed deltas across all three batteries.
(
_bench_dict({
"5s": {"syntax": {"parse_pass_rate": 0.6}},
"5t": {"transfer-learning": {"transfer_learning_success_rate": 0.6}},
"5f": {"function": {"function_pass_rate": 0.6}},
}),
_bench_dict({
"5s": {"syntax": {"parse_pass_rate": 0.7}},
"5t": {"transfer-learning": {"transfer_learning_success_rate": 0.65}},
"5f": {"function": {"function_pass_rate": 0.55}},
}),
),
# All-zero inputs → score 0.
({"5s": {}, "5t": {}, "5f": {}}, {"5s": {}, "5t": {}, "5f": {}}),
]
for parent, child in configs:
r = fork_score(parent, child)
assert r.score == pytest.approx(sum(r.breakdown.values()), abs=1e-9), (
f"score {r.score} ≠ Σbreakdown {sum(r.breakdown.values())} on "
f"parent={parent!r}, child={child!r}"
)
# --- constants honored from arborist.substrate.fork_score -----------
def test_signal_floor_honored():
"""Score exactly at SIGNAL_FLOOR is ACCEPT (≥, not >)."""
# We construct a child whose 5S delta * alpha == SIGNAL_FLOOR.
# alpha defaults to 0.30 per WeightSet (see arborist.substrate.weights).
# Solve for delta: delta = SIGNAL_FLOOR / alpha = 0.05 / 0.30 ≈ 0.1667.
# Average of 5 sub-deltas; set each sub to 0.1667 to hit it.
parent = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.5},
"semantics": {"equivalence_recovery_rate": 0.5},
"syllogism": {"step_validity_rate": 0.5},
"synthesis": {"derivation_pass_rate": 0.5},
"semiotics": {"invariance_under_swap": 0.5},
},
})
target_delta = SIGNAL_FLOOR / DEFAULT_WEIGHTS.alpha
child = _bench_dict({
"5s": {
"syntax": {"parse_pass_rate": 0.5 + target_delta},
"semantics": {"equivalence_recovery_rate": 0.5 + target_delta},
"syllogism": {"step_validity_rate": 0.5 + target_delta},
"synthesis": {"derivation_pass_rate": 0.5 + target_delta},
"semiotics": {"invariance_under_swap": 0.5 + target_delta},
},
})
r = fork_score(parent, child)
# 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)