Files
rune/scripts/check_microkernel_parity.py
Nick b9b089fa49 feat(microkernel): close PARTIAL gaps — dichotomy wave
Name-preserving port of the 6 unmatched fns into src/go/microkernel:
NowIso (+clock seam), ExtractManifest (brace-counted PLUGIN block +
literal_eval-subset parser), Discover, RunHook (subprocess hook runner),
LoadRegistry/SaveRegistry (renamed; corrupt-file full-default parity),
plus Fire wiring the CLI fire case to real 8-way parallel dispatch with
the bounded fire-log. Register now honors Python truthiness for hook
values (string targets index) and plumbs _file into entries.

Frozen truth: 25-case envelope via the shared harness (regen-stable,
--verify byte-identical x2); Go battery 25/25 subtests, -race clean;
live oracle-vs-Go parity 24/24 (scripts/check_microkernel_parity.py);
gofmt/vet/go build ./... clean; Python both-pins green for the module
(18 test_p1_01 failures proven pre-existing on stashed base).
Audit: microkernel 6/12 PARTIAL -> 12/12 FULL (evidence:
verification/summary-microkernel.md + microkernel-*.exit).
2026-10-14 02:30:29 +01:00

345 lines
15 KiB
Python

#!/usr/bin/env python3
"""check_microkernel_parity.py — live oracle-vs-Go parity sweep for the
microkernel gap closure (dichotomy wave #2479, worktree rune-wave-cq).
The frozen envelope (truth_microkernel_gap.json, 25 cases) pins the gap
fns byte-identically. THIS script drives the LIVE Python oracle
(src/rune/microkernel.py) and the Go binary's parity op mode across every
branch class with FRESH fixtures (not the frozen ones): extract_manifest
(prefix traps, nested literals, unicode), discover (mixed dir), registry
load/save shapes, and fire/_run_hook (ok-json / raw-stdout truncation /
rc-error / timeout / skipped / ghost-file / empty-file / two-listeners /
no-listeners).
Normalization law (shared with the rig): wall-clock durations → "<ms>",
fire results name-sorted, fixture paths → <dir>. Data keys alphabetical
(the Go map marshal is key-sorted; plugins canonicalize the payload).
Usage: python3 scripts/check_microkernel_parity.py [--bin <path>]
Exit 0 on 100% parity, 1 with the first divergence.
"""
from __future__ import annotations
import argparse
import copy
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import microkernel as mk # noqa: E402
FROZEN_NOW = "2026-10-13T11:05:00"
FROZEN_NOW_AWARE = FROZEN_NOW + "+00:00"
OK_JSON = (
"import json, sys\n"
"data = sys.argv[sys.argv.index('--data') + 1]\n"
"print(json.dumps({'echo': json.loads(data)}))\n"
)
RAW_LONG = "print('h\\u00e9llo-\\u2192ok ' * 90)\n"
BOOM = ("import sys\n"
"sys.stderr.write('\\u00e9rr\\u00f4r-' * 80)\n"
"sys.exit(3)\n")
SLOW = "import time\ntime.sleep(4)\n"
FAILS: list[str] = []
CHECKS = [0]
def check(name, oracle_val, go_val):
CHECKS[0] += 1
a = json.dumps(oracle_val, sort_keys=True, ensure_ascii=False)
b = json.dumps(go_val, sort_keys=True, ensure_ascii=False)
if a != b:
FAILS.append(f"{name}:\n oracle: {a[:300]}\n go: {b[:300]}")
# ── clock pin (aware — fire() mixes naive/aware now()) ────────────────────
def pin_clock():
base = datetime.fromisoformat(FROZEN_NOW).replace(tzinfo=timezone.utc)
class Frozen(datetime):
@classmethod
def now(cls, tz=None):
if tz is not None:
return base.replace(tzinfo=tz)
return base
old = mk.datetime
mk.datetime = Frozen
return old
# ── Go side ────────────────────────────────────────────────────────────────
def run_go(binpath, req, env_dir=None):
env = dict(os.environ)
if env_dir:
env["RUNE_MEMORY_DIR"] = env_dir
p = subprocess.run([str(binpath), "parity"], input=json.dumps(req),
capture_output=True, text=True, env=env, timeout=120)
if p.returncode != 0:
raise RuntimeError(f"go op {req.get('op')} exited {p.returncode}: "
f"{p.stdout} {p.stderr}")
return json.loads(p.stdout)
# ── normalization ──────────────────────────────────────────────────────────
def norm_fire(result):
out = copy.deepcopy(result)
if "total_duration_ms" in out:
out["total_duration_ms"] = "<ms>"
for r in out.get("results", []):
if "duration_ms" in r:
r["duration_ms"] = "<ms>"
out["results"] = sorted(out.get("results", []), key=lambda r: r.get("name", ""))
return out
def norm_logs(log):
return [{"event": e.get("event"), "timestamp": e.get("timestamp"),
"hooks_fired": e.get("hooks_fired"), "duration_ms": "<ms>"}
for e in log]
def oracle_fire_with(manifest, event, data, timeout):
"""Fire through the live oracle; the manifest's _file must already
point at an existing fixture script (or a ghost for not-found)."""
work = tempfile.mkdtemp(prefix="mk_par_fire_")
mk.REGISTRY_DB = os.path.join(work, "registry.json")
mk.register(dict(manifest))
result = mk.fire(event, data=data, timeout=timeout)
log = mk._load_registry().get("log", [])
return {"result": norm_fire(result), "log": norm_logs(log)}
def go_registry_blob(manifest):
return {
"plugins": {manifest["name"]: {
"manifest": {"name": manifest["name"]},
"registered_at": FROZEN_NOW_AWARE,
"active": bool(manifest.get("auto_activate", True)),
"file": manifest.get("_file", ""),
}},
"capabilities": {},
"hooks": {ev: [manifest["name"]] for ev, on in manifest.get("hooks", {}).items() if on},
"log": [],
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--bin", default="/tmp/mk-parity-bin")
args = ap.parse_args()
binpath = Path(args.bin)
old_clock = pin_clock()
# ── now_iso: FORMAT parity (wall clock — both must emit the shape) ──
CHECKS[0] += 1
shape = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$")
if not (shape.match(mk.now_iso()) and shape.match(run_go(binpath, {"op": "now_iso"})["result"])):
FAILS.append(f"now_iso shape: oracle {mk.now_iso()!r} go {run_go(binpath, {'op': 'now_iso'})['result']!r}")
# ── extract_manifest: content sweep ──────────────────────────────
contents = [
'PLUGIN = {\n "name": "x",\n "capabilities": ["a", "b"],\n "hooks": {"e": "handle_hook"},\n "priority": 2,\n "auto_activate": False,\n}\n',
'PLUGINX = {\n "name": "prefix-trap",\n}\n', # startswith("PLUGIN")
'PLUGIN={"name":"dense","n":0.5}\n', # no spaces
"PLUGIN = {'k': 'v', 't': (1, 2), 'l': [1, [2, 3]]}\n", # tuple literal
'PLUGIN = {\n "name": "é中",\n "s": "tab\\tnewline\\nquote\\"q",\n}\n',
'PLUGIN = {\n "name": "trail",\n "arr": [1, 2, 3,],\n}\n', # trailing comma in list
'no manifest here\n',
'PLUGIN = {\n "name": "never-closed",\n',
]
for i, content in enumerate(contents):
fd, fpath = tempfile.mkstemp(suffix=".py")
with os.fdopen(fd, "w") as f:
f.write(content)
try:
oracle = copy.deepcopy(mk._extract_manifest(fpath))
finally:
os.unlink(fpath)
go = run_go(binpath, {"op": "extract_manifest", "content": content})["result"]
check(f"extract_manifest[{i}]", oracle, go)
# ── discover: mixed fixture dir ──────────────────────────────────
work = tempfile.mkdtemp(prefix="mk_par_disc_")
fixture = Path(work) / "scripts"
fixture.mkdir()
files = {
"alpha.py": 'PLUGIN = {\n "name": "alpha",\n "capabilities": ["cap"],\n "hooks": {"evt": "run"},\n "priority": 1,\n}\n',
"beta.py": 'PLUGIN = {\n "name": "beta",\n "capabilities": ["cap"],\n "priority": 9,\n}\n',
"gamma.py": 'PLUGIN = {"version": "0"}\n', # no name → error row
"delta.py": 'def plain():\n pass\n', # no PLUGIN → skipped
"_under.py": 'PLUGIN = {"name": "under"}\n',
"microkernel.py": 'PLUGIN = {"name": "self"}\n',
}
for fname, src in files.items():
(fixture / fname).write_text(src)
real_listdir = os.listdir
old_dir, old_db = mk.SCRIPTS_DIR, mk.REGISTRY_DB
mk.SCRIPTS_DIR = str(fixture)
mk.REGISTRY_DB = os.path.join(work, "registry.json")
os.listdir = lambda p: sorted(files) if str(p) == str(fixture) else real_listdir(p)
try:
oracle_result = copy.deepcopy(mk.discover())
oracle_state = mk._load_registry()
finally:
os.listdir = real_listdir
mk.SCRIPTS_DIR, mk.REGISTRY_DB = old_dir, old_db
oracle_proj = {
"plugins": {n: {"active": p.get("active"), "registered_at": p.get("registered_at"),
"file": p.get("file", "").replace(str(fixture), "<dir>")}
for n, p in oracle_state["plugins"].items()},
"capabilities": oracle_state["capabilities"],
"hooks": oracle_state["hooks"],
}
go = run_go(binpath, {"op": "discover", "dir": str(fixture), "now": FROZEN_NOW_AWARE})
go_proj = {
"plugins": {n: {"active": p["active"], "registered_at": p["registered_at"],
"file": p["file"].replace(str(fixture), "<dir>")}
for n, p in go["plugins"].items()},
"capabilities": go["capabilities"],
"hooks": go["hooks"],
}
check("discover.result", oracle_result, go["result"])
check("discover.registry", oracle_proj, go_proj)
# ── registry load/save shapes ────────────────────────────────────
old_db = mk.REGISTRY_DB
load_dir = tempfile.mkdtemp(prefix="mk_par_load_")
full = json.dumps({
"plugins": {"q": {"manifest": {"name": "q", "priority": 3},
"registered_at": FROZEN_NOW_AWARE, "active": False,
"file": "/y/q.py"}},
"capabilities": {"c": ["q"]},
"hooks": {"e2": ["q"]},
"log": [{"event": "e2", "timestamp": FROZEN_NOW_AWARE,
"hooks_fired": 2, "duration_ms": 3.25}],
}, indent=2)
def oracle_load(content):
mk.REGISTRY_DB = os.path.join(load_dir, "registry.json")
try:
db = mk.REGISTRY_DB
if content is None:
if os.path.exists(db):
os.unlink(db)
else:
with open(db, "w") as f:
f.write(content)
reg = mk._load_registry()
return {"plugins": {n: {"active": p.get("active"),
"registered_at": p.get("registered_at"),
"file": p.get("file", "")}
for n, p in reg.get("plugins", {}).items()},
"capabilities": reg.get("capabilities", {}),
"hooks": reg.get("hooks", {}),
"log": reg.get("log", [])}
finally:
mk.REGISTRY_DB = old_db
for label, content in (("missing", None), ("corrupt", "}{ nope"), ("full", full)):
oracle = oracle_load(content)
go = run_go(binpath, {"op": "registry_load", "content": content}, env_dir=load_dir)
check(f"registry_load.{label}", oracle,
{"plugins": {n: {"active": p["active"],
"registered_at": p["registered_at"], "file": p["file"]}
for n, p in go["plugins"].items()},
"capabilities": go["capabilities"], "hooks": go["hooks"], "log": go["log"]})
# roundtrip
mk.REGISTRY_DB = os.path.join(load_dir, "registry.json")
try:
first = mk._load_registry()
mk._save_registry(first)
again = mk._load_registry()
oracle_rt = {"stable": first == again}
finally:
mk.REGISTRY_DB = old_db
go_rt = run_go(binpath, {"op": "registry_roundtrip"}, env_dir=load_dir)
check("registry_roundtrip", oracle_rt, {"stable": go_rt["stable"]})
# ── fire / _run_hook branch matrix ───────────────────────────────
evt = "evt"
fire_cases = [
("ok_json", OK_JSON, {"name": "okp", "hooks": {evt: True}}, evt, {"a": 1, "b": "x"}, 30),
("raw_text", RAW_LONG, {"name": "rawp", "hooks": {evt: True}}, evt, None, 30),
("rc_error", BOOM, {"name": "boomp", "hooks": {evt: True}}, evt, None, 30),
("timeout", SLOW, {"name": "slowp", "hooks": {evt: True}}, evt, None, 1),
("skipped", OK_JSON, {"name": "offp", "hooks": {evt: True}, "auto_activate": False}, evt, None, 30),
("ghost_file", None, {"name": "ghostp", "hooks": {evt: True}, "_file": "/nonexistent/ghost.py"}, evt, None, 30),
("empty_file", None, {"name": "nofilep", "hooks": {evt: True}}, evt, None, 30),
("no_listeners", OK_JSON, {"name": "lonely", "capabilities": ["c"]}, "nope_evt", None, 30),
]
for label, script, manifest, event, data, timeout in fire_cases:
manifest = dict(manifest)
if script is not None:
work = tempfile.mkdtemp(prefix="mk_par_fx_")
fpath = os.path.join(work, "plugin.py")
with open(fpath, "w") as f:
f.write(script)
manifest["_file"] = fpath
oracle = oracle_fire_with(manifest, event, data, timeout)
blob = go_registry_blob(manifest)
go = run_go(binpath, {"op": "fire", "registry": blob, "event": event,
"data": data, "timeout_ms": timeout * 1000,
"now": FROZEN_NOW_AWARE})
go = {"result": norm_fire(go["result"]), "log": norm_logs(go["log"])}
check(f"fire.{label}", oracle, go)
# two listeners in parallel
work2 = tempfile.mkdtemp(prefix="mk_par_two_")
tmpdb2 = os.path.join(work2, "registry.json")
old_db = mk.REGISTRY_DB
mk.REGISTRY_DB = tmpdb2
try:
for nm in ("zz", "aa"):
fpath = os.path.join(work2, nm + ".py")
with open(fpath, "w") as f:
f.write(OK_JSON)
mk.register({"name": nm, "hooks": {evt: True}, "_file": fpath})
oracle = {"result": norm_fire(mk.fire(evt, data={"z": 1}, timeout=30)),
"log": norm_logs(mk._load_registry().get("log", []))}
finally:
mk.REGISTRY_DB = old_db
blob = {
"plugins": {
"zz": {"manifest": {"name": "zz"}, "registered_at": FROZEN_NOW_AWARE,
"active": True, "file": os.path.join(work2, "zz.py")},
"aa": {"manifest": {"name": "aa"}, "registered_at": FROZEN_NOW_AWARE,
"active": True, "file": os.path.join(work2, "aa.py")},
},
"capabilities": {}, "hooks": {evt: ["zz", "aa"]}, "log": [],
}
go = run_go(binpath, {"op": "fire", "registry": blob, "event": evt,
"data": {"z": 1}, "timeout_ms": 30000, "now": FROZEN_NOW_AWARE})
go = {"result": norm_fire(go["result"]), "log": norm_logs(go["log"])}
check("fire.two_listeners", oracle, go)
mk.datetime = old_clock
print(f"{'FAIL' if FAILS else 'OK'}: {CHECKS[0] - len(FAILS)}/{CHECKS[0]} checks passed")
for f in FAILS:
print(" " + f)
return 1 if FAILS else 0
if __name__ == "__main__":
sys.exit(main())