Files
rune/scripts/check_behavior_parity.py
T
Nick 4e1eee7d9e feat(behavior): port to FULL Rust — dichotomy wave
RUST verdict (dichotomy law, #2479 tranche): behavior.py's FULL decision
surface is ported to src/rust/behavior — the reinforce decision math
(habit_reinforce: activation/success/failure counting, confidence
min(1.0, 0.5 + rate*0.5), candidate→active promotion at min_activations;
correction_reinforce: success +0.1 / failure −0.3 floor/ceiling, evidence
counting, active/archived threshold at 0.3), the match_context per-row
predicates (scope-pattern regex IGNORECASE with invalid-regex skip,
domain case-insensitive substring, confidence > 0.3 gates), the
scope_tags parse fallback, the PLUGIN descriptor (frozen data), and the
db-path resolution. IO stays Python: SQLite I/O + row ordering (the
crate links no sqlite), wall-clock now, argparse CLI.

Wiring: bh_loader.py ctypes (doc-envelope, cs_free_string — no free
symbol re-export), facade delegation in behavior.py with
RUNE_BEHAVIOR_CORE=python fallback pin (honored in the loader probe);
the evidence_provenance ep_loader path remains as the pin oracle.

Truth: scripts/gen_behavior_truth.py freezes 30 live-oracle cases (add
shapes, match branch matrix, reinforce matrix with promotion/archival/
floor/ceiling, list filters, stats, PLUGIN) embedded verbatim as
bh_truth.json; battery 10 tests replay it (clippy 0).

Parity: 100% live (14/14 check groups over the frozen shapes + list/
stats leaves, rust facade vs pinned-python, 0 divergences); 13/13 port
+ shim wiring tests; full suite failure set IDENTICAL to the clean tree
(95 pre-existing, 0 new).

Contract BUILDER-2026-10-11T16:27:58Z-92bb54-factory-wave (building).
2026-10-12 00:05:38 +01:00

183 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Live parity: behavior delegating facade (Rust core) vs the Python-pin
oracle path, over the SAME case matrix the truth generator froze.
Run: python3 scripts/check_behavior_parity.py -> x/y lines, exit 1 on any
divergence.
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
_tmp = tempfile.mkdtemp(prefix="bh-parity-")
os.environ["RUNE_HOME"] = _tmp
os.environ["RUNE_MEMORY_DIR"] = os.path.join(_tmp, "memory")
from rune import behavior as bh # noqa: E402
from rune import bh_loader # noqa: E402
if not bh_loader.available():
print("0/0 — rust behavior core not built")
sys.exit(0)
PASS = 0
FAIL = 0
DIVERGENCES = []
_CLOCK_KEYS = {"created_at", "last_reinforced", "last_activated"}
def strip_clocks(obj):
"""Timestamps are pinned by the shim (`now`) — wall-clock values
differ per run by design; compare everything else."""
if isinstance(obj, dict):
return {k: strip_clocks(v) for k, v in obj.items() if k not in _CLOCK_KEYS}
if isinstance(obj, list):
return [strip_clocks(x) for x in obj]
return obj
def check(name, rust, py):
global PASS, FAIL
if strip_clocks(rust) == strip_clocks(py):
PASS += 1
else:
FAIL += 1
DIVERGENCES.append((name, rust, py))
def row(table, rid):
db = bh.connect()
try:
r = db.execute(f"SELECT * FROM {table} WHERE id=?", (rid,)).fetchone()
return dict(r) if r else None
finally:
db.close()
def both(name, fn):
"""Drive fn() twice: pinned-python first (fresh db), rust second
(fresh db). Databases are reset between runs so ids align."""
os.environ["RUNE_BEHAVIOR_CORE"] = "python"
bh.DB_PATH = os.path.join(os.environ["RUNE_MEMORY_DIR"], "behavior.db")
if os.path.exists(bh.DB_PATH):
os.remove(bh.DB_PATH)
py = fn()
os.environ["RUNE_BEHAVIOR_CORE"] = "auto"
if os.path.exists(bh.DB_PATH):
os.remove(bh.DB_PATH)
rust = fn()
os.environ["RUNE_BEHAVIOR_CORE"] = "python"
check(name, rust, py)
# ── add shapes ────────────────────────────────────────────────────────
def add_shapes():
out = [bh.add_correction("say X", "say Y"),
bh.add_correction("orig", "corr", scope_tags={"k": "v"}, domain="shell", pattern="shell"),
bh.add_habit("trig", "act"),
bh.add_habit("trig2", "act2", name="n", context_tags=["a"], min_activations=5)]
return json.loads(json.dumps(out))
both("add-shapes", add_shapes)
# ── seed the shared match fixture in BOTH modes ───────────────────────
def seed_match():
bh.add_correction("o1", "scoped corr", scope_tags={}, domain="shell", pattern="deploy")
bh.add_correction("o2", "domain corr", scope_tags={}, domain="editor")
bh.add_correction("o3", "plain corr")
bh.add_correction("o4", "bad regex corr", pattern="([unclosed")
low = bh.add_correction("o5", "low conf corr")["added"]
bh.add_habit("deploy", "run deploy script")
bh.add_habit("test (unit|e2e)", "run tests")
bh.add_habit("([broken", "bad regex habit")
hslow = bh.add_habit("zzz-no-match-ever", "quiet")["added"]
db = bh.connect()
db.execute("UPDATE corrections SET confidence=0.2 WHERE id=?", (low,))
db.execute("UPDATE habits SET confidence=0.2 WHERE id=?", (hslow,))
db.commit()
db.close()
def match_shapes():
seed_match()
cases = []
for ctx, mt in [("deploy to shell now", None), ("deploy the editor", None),
("unrelated chatter here", None), ("deploy to shell now", "habit"),
("deploy to shell now", "correction"), ("run the unit tests", None),
("zzz-no-match-ever quiet", None), ("([unclosed anything", None)]:
got = bh.match_context(ctx, match_type=mt)
cases.append({
"corrections": sorted(c["corrected_behavior"] for c in got["corrections"]),
"habits": sorted(h["action_template"] for h in got["habits"]),
"full": json.loads(json.dumps(got)),
})
return cases
both("match-matrix", match_shapes)
# ── reinforce matrix ──────────────────────────────────────────────────
def reinforce_shapes():
seed_match()
out = []
for name, item_type, outcome, seed_fn, pre, n in [
("success-promotes-at-3", "habit", "success", lambda: bh.add_habit("t1", "a1"), {}, 3),
("failure-counts", "habit", "failure", lambda: bh.add_habit("t2", "a2"), {}, 2),
("neutral-bumps-activation-only", "habit", "neutral", lambda: bh.add_habit("t3", "a3"), {}, 1),
("already-active-stays", "habit", "success", lambda: bh.add_habit("t4", "a4", min_activations=1), {}, 2),
("min5-gate", "habit", "success", lambda: bh.add_habit("t5", "a5", min_activations=5), {}, 4),
("success-plus-point-one", "correction", "success", lambda: bh.add_correction("x1", "y1"), {}, 1),
("failure-archives", "correction", "failure", lambda: bh.add_correction("x2", "y2"), {}, 1),
("double-failure-floor", "correction", "failure", lambda: bh.add_correction("x3", "y3"), {}, 3),
("ceiling-at-one", "correction", "success", lambda: bh.add_correction("x4", "y4"), {"confidence": 0.95}, 1),
("neutral-noop", "correction", "neutral", lambda: bh.add_correction("x5", "y5"), {}, 1),
]:
rid = seed_fn()["added"]
if pre:
db = bh.connect()
table = "habits" if item_type == "habit" else "corrections"
for k, v in pre.items():
db.execute(f"UPDATE {table} SET {k}=? WHERE id=?", (v, rid))
db.commit()
db.close()
results = [bh.reinforce(rid, outcome, item_type=item_type) for _ in range(n)]
out.append({"name": name, "results": json.loads(json.dumps(results)),
"final": json.loads(json.dumps(row("habits" if item_type == "habit" else "corrections", rid)))})
out.append({"name": "not-found-habit",
"results": [bh.reinforce(99991, "success", item_type="habit")], "final": None})
out.append({"name": "not-found-correction",
"results": [bh.reinforce(99992, "success", item_type="correction")], "final": None})
return out
both("reinforce-matrix", reinforce_shapes)
# ── list + stats ──────────────────────────────────────────────────────
def list_stats_shapes():
seed_match()
out = {}
check("list-keys", sorted(bh.list_items().keys()), ["corrections", "habits"])
check("limit-1", len(bh.list_items(limit=1).get("corrections", [])), 1)
check("type-correction-keys", sorted(bh.list_items(item_type="correction").keys()), ["corrections"])
check("type-habit-keys", sorted(bh.list_items(item_type="habit").keys()), ["habits"])
check("type-all-keys", sorted(bh.list_items(item_type="all").keys()), ["corrections", "habits"])
out["stats"] = bh.behavior_stats()
return json.loads(json.dumps(out))
both("list-stats", list_stats_shapes)
print(f"{PASS}/{PASS + FAIL} live parity (rust facade vs python pin)")
for name, rust, py in DIVERGENCES:
print(f"DIVERGENCE {name}:")
print(f" rust: {json.dumps(rust, sort_keys=True)[:400]}")
print(f" py: {json.dumps(py, sort_keys=True)[:400]}")
sys.exit(1 if FAIL else 0)