Files
rune/scripts/check_task_gate_parity.py
T

260 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Live parity: Rust task_gate kernel vs the Python oracle (dichotomy
wave #2479). Replays the frozen truth corpus PLUS seeded fuzz over every
public function and branch. Timestamps are frozen (no-clock law — the
kernel takes `now` as an argument), so nothing is masked.
The facade reads RUNE_TG_CORE per call, so one process can flip between
the pinned oracle (RUNE_TG_CORE=python) and the kernel (unset).
"""
from __future__ import annotations
import json
import os
import random
import sqlite3
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src" / "rune"))
import task_gate as tg # noqa: E402
FROZEN_NOW = "2026-10-11T23:00:00.123456+00:00"
class FrozenDatetime(datetime):
"""No-clock law: the facade's `now` stamps are wall clock, not logic —
freeze them so the two core runs stamp identically."""
@classmethod
def now(cls, tz=None):
return datetime(2026, 10, 11, 23, 0, 0, 123456, tzinfo=timezone.utc)
tg.datetime = FrozenDatetime
TRUTH_PATH = Path(__file__).resolve().parents[1] / (
"src/rust/task_gate/src/frozen_tg_truth.json")
def call_core(core, fn, *args, **kwargs):
old = os.environ.get("RUNE_TG_CORE")
os.environ["RUNE_TG_CORE"] = core
try:
return getattr(tg, fn)(*args, **kwargs)
finally:
if old is None:
os.environ.pop("RUNE_TG_CORE", None)
else:
os.environ["RUNE_TG_CORE"] = old
def fresh_db():
tmp = tempfile.mkdtemp(prefix="tg_parity_")
tg.DB_PATH = os.path.join(tmp, "task_gate.db")
db = tg.connect()
db.close()
def read_rows():
db = sqlite3.connect(tg.DB_PATH)
db.row_factory = sqlite3.Row
rows = [dict(r) for r in db.execute(
"SELECT * FROM instructions ORDER BY id").fetchall()]
db.close()
return rows
def seed_rows(rows):
fresh_db()
db = tg.connect()
for i, r in enumerate(rows, start=1):
db.execute(
"INSERT INTO instructions (id, instruction, pattern, count,"
" threshold, status, promoted_at, created_at, last_seen)"
" VALUES (?,?,?,?,?,?,?,?,?)",
(
r.get("id", i), r.get("instruction", "seed %d" % i),
r.get("pattern", r"\bseed\b"), r.get("count", 1),
r.get("threshold", 3), r.get("status", "observing"),
r.get("promoted_at"), r.get("created_at", FROZEN_NOW),
r.get("last_seen", FROZEN_NOW),
),
)
db.commit()
db.close()
def scenario_record(rows_before, text):
"""Run record under both cores on identical pre-state; compare
(return, rows_after)."""
seed_rows(rows_before)
py_ret = call_core("python", "record_instruction", text)
py_rows = read_rows()
seed_rows(rows_before)
rs_ret = call_core("auto", "record_instruction", text)
rs_rows = read_rows()
ok = (json.dumps(py_ret, sort_keys=True)
== json.dumps(rs_ret, sort_keys=True)
and json.dumps(py_rows, sort_keys=True)
== json.dumps(rs_rows, sort_keys=True))
return ok, py_ret, rs_ret, py_rows, rs_rows
def scenario_ctx(rows, context):
seed_rows(rows)
py = call_core("python", "check_context", context)
rs = call_core("auto", "check_context", context)
return (json.dumps(py, sort_keys=True)
== json.dumps(rs, sort_keys=True)), py, rs
def scenario_list(rows):
seed_rows(rows)
py = call_core("python", "list_gates")
rs = call_core("auto", "list_gates")
return (json.dumps(py, sort_keys=True)
== json.dumps(rs, sort_keys=True)), py, rs
def scenario_promote(rows, gate_id):
seed_rows(rows)
py = call_core("python", "promote_gate", gate_id)
py_rows = read_rows()
seed_rows(rows)
rs = call_core("auto", "promote_gate", gate_id)
rs_rows = read_rows()
return (json.dumps(py, sort_keys=True)
== json.dumps(rs, sort_keys=True)
and json.dumps(py_rows, sort_keys=True)
== json.dumps(rs_rows, sort_keys=True)), py, rs, py_rows, rs_rows
def scenario_hook(event):
py = call_core("python", "handle_hook", event, {"ignored": True})
rs = call_core("auto", "handle_hook", event, {"ignored": True})
return (json.dumps(py, sort_keys=True)
== json.dumps(rs, sort_keys=True)), py, rs
# ── fuzz pools ─────────────────────────────────────────────────────────
WORDS = ["always", "check", "the", "benchmark", "before", "building",
"ship", "it", "do", "stop", "now", "café", "über", "v1_2",
"test_it", "fix", "bug", "release", "deploy", "kraken", "ab",
"cd", "research", "first", "wash", "spin", "gate", "task"]
PATTERNS = [r"\bcheck\b", r"\bship\b", r"\bbenchmark\b", r"do\ it",
r"\balways\b|\bcheck\b", r"\bcafé\b", r"\bresearch\b",
r"\bgate\b|\btask\b", r"\bfirst\b"]
STATUSES = ["observing", "active", "retired"]
def rand_text(rng):
n = rng.randint(1, 8)
sep = rng.choice([" ", " ", " ", " ", "! ", " ("])
return sep.join(rng.choice(WORDS) for _ in range(n)) + rng.choice(
["", "!", "?", " (now)"])
def rand_rows(rng, max_rows=4):
rows = []
for _ in range(rng.randint(0, max_rows)):
rows.append({
"pattern": rng.choice(PATTERNS),
"count": rng.randint(1, 5),
"threshold": rng.randint(2, 4),
"status": rng.choice(STATUSES),
"instruction": rand_text(rng),
"promoted_at": FROZEN_NOW if rng.random() < 0.3 else None,
})
return rows
def fuzz_cases(rng):
cases = []
for _ in range(300):
cases.append(("record", rand_rows(rng), rand_text(rng)))
for _ in range(250):
cases.append(("ctx", rand_rows(rng), rand_text(rng)))
for _ in range(200):
cases.append(("list", rand_rows(rng, 5), None))
for _ in range(150):
cases.append(("promote", rand_rows(rng), rng.randint(1, 5)))
for _ in range(50):
cases.append(("hook", None, rand_text(rng)))
return cases
def main() -> int:
failures = []
def check(label, ok, detail=""):
if not ok:
failures.append((label, detail))
print(f"FAIL {label}\n{detail}")
# 1. frozen truth corpus (facade level, both cores already proven by
# gen_task_gate_truth.py --replay; re-run kernel level here live)
truth = json.loads(TRUTH_PATH.read_text())["cases"]
n_truth = 0
for group, cases in truth.items():
for case in cases:
if group == "record_instruction":
inp = case["input"]
ok, p, r, pr, rr = scenario_record(
inp["instructions"], inp["text"])
elif group == "check_context":
inp = case["input"]
ok, p, r = scenario_ctx(inp["instructions"], inp["context"])
elif group == "list_gates":
ok, p, r = scenario_list(
case["rows_after"] and _rows_before(case))
elif group == "promote_gate":
inp = case["input"]
ok, p, r, pr, rr = scenario_promote(
[{"pattern": r"\bx\b"}], inp["id"])
else: # handle_hook
ok, p, r = scenario_hook(case["input"]["event"])
n_truth += 1
check(f"truth:{group}:{case['name']}", ok,
f"py={json.dumps(p, sort_keys=True)[:300]}\n"
f"rs={json.dumps(r, sort_keys=True)[:300]}")
# 2. seeded fuzz
rng = random.Random(2479)
n_fuzz = 0
for kind, rows, arg in fuzz_cases(rng):
n_fuzz += 1
label = f"fuzz:{kind}:{n_fuzz}"
if kind == "record":
ok, p, r, pr, rr = scenario_record(rows, arg)
elif kind == "ctx":
ok, p, r = scenario_ctx(rows, arg)
elif kind == "list":
ok, p, r = scenario_list(rows)
elif kind == "promote":
ok, p, r, pr, rr = scenario_promote(rows, arg)
else:
ok, p, r = scenario_hook(arg)
check(label, ok,
f"py={json.dumps(p, sort_keys=True)[:300]}\n"
f"rs={json.dumps(r, sort_keys=True)[:300]}\n"
f"rows={json.dumps(rows, sort_keys=True)[:200]}")
total = n_truth + n_fuzz
print(f"parity: {total - len(failures)}/{total} "
f"({n_truth} truth + {n_fuzz} fuzz seed 2479)")
return 1 if failures else 0
def _rows_before(case):
"""list_gates truth carries rows_after (the full pre-state — listing
never mutates); replay it as the seed state."""
return case["rows_after"]
if __name__ == "__main__":
sys.exit(main())