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).
183 lines
7.6 KiB
Python
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)
|