feat(task_gate): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
f9df935605
commit
b20a74badb
7 files changed
+2775
-104
No files matched your search
@@ -0,0 +1,259 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,589 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_task_gate_truth.py — freeze/replay parity truth for task_gate
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/task_gate.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output, oracle_return) triples into
|
||||
src/rust/task_gate/src/frozen_tg_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle functions delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_TG_CORE=python BEFORE any import.
|
||||
No-clock law: the wall clock is frozen by patching the datetime symbol in
|
||||
task_gate, so every kernel payload carries its own `now` and replay is
|
||||
deterministic regardless of run date.
|
||||
|
||||
Boundary: the sqlite IO (connect, the row reads, UPDATE/INSERT) and the
|
||||
CLI main() stay in the Python facade — the kernel owns the verdict logic
|
||||
(pattern extraction, regex gate matching, count/threshold/status
|
||||
transitions, promotion decision, listing projection, hook echo).
|
||||
list_gates' ORDER BY is decided by sqlite in the facade; the kernel
|
||||
receives already-ordered rows.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(
|
||||
REPO, "src", "rust", "task_gate", "src", "frozen_tg_truth.json"
|
||||
)
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_TG_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import task_gate as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
import tg_loader # noqa: E402 (kernel-level replay side)
|
||||
|
||||
FROZEN_NOW = "2026-10-11T23:00:00.123456+00:00"
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
assert tz is timezone.utc or tz is None
|
||||
return datetime(2026, 10, 11, 23, 0, 0, 123456, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
oracle_mod.datetime = FrozenDatetime
|
||||
|
||||
_COLS = ("id", "instruction", "pattern", "count", "threshold", "status",
|
||||
"promoted_at", "created_at", "last_seen")
|
||||
|
||||
|
||||
def fresh_db():
|
||||
tmp = tempfile.mkdtemp(prefix="tg_truth_")
|
||||
oracle_mod.DB_PATH = os.path.join(tmp, "task_gate.db")
|
||||
return oracle_mod.connect()
|
||||
|
||||
|
||||
def seed_rows(pre_rows):
|
||||
"""Seed explicit rows (dicts with any subset of the columns; count/
|
||||
threshold/status/pattern/created_at/last_seen default like the oracle
|
||||
would for a fresh row). Returns rows as full dicts in id order."""
|
||||
db = fresh_db()
|
||||
for i, r in enumerate(pre_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 instruction %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()
|
||||
rows = [dict(r) for r in db.execute(
|
||||
"SELECT * FROM instructions ORDER BY id").fetchall()]
|
||||
db.close()
|
||||
return rows
|
||||
|
||||
|
||||
def read_rows():
|
||||
db = sqlite3.connect(oracle_mod.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 kernel_input(rows, text):
|
||||
return {"instructions": rows, "text": text, "now": FROZEN_NOW}
|
||||
|
||||
|
||||
def expected_record(rows_after, result):
|
||||
"""Derive the kernel's expected doc purely from oracle behavior."""
|
||||
if result["matched"]:
|
||||
row = next(r for r in rows_after if r["id"] == result["id"])
|
||||
return {"matched": True, "id": row["id"], "count": row["count"],
|
||||
"status": row["status"], "promoted": result["promoted"],
|
||||
"last_seen": row["last_seen"]}
|
||||
row = next(r for r in rows_after if r["id"] == result["id"])
|
||||
return {"matched": False,
|
||||
"new_row": {k: row[k] for k in
|
||||
("instruction", "pattern", "count", "threshold",
|
||||
"status", "created_at", "last_seen")}}
|
||||
|
||||
|
||||
# ── record_instruction ───────────────────────────────────────────────────────
|
||||
|
||||
def r_new_keywords(rec):
|
||||
rows = seed_rows([])
|
||||
text = "Always CHECK the benchmark before building"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-keywords-pattern",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_no_keywords(rec):
|
||||
rows = seed_rows([])
|
||||
text = "do it"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-no-keywords-fallback-escape",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_fallback_truncate50(rec):
|
||||
rows = seed_rows([])
|
||||
text = " AB CD EF GH IJ KL MN OP QR ST UV WX YZ 12 34 56 "
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-fallback-truncated-50-stripped",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_regex_special(rec):
|
||||
rows = seed_rows([])
|
||||
text = "fix the bug (now)!"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-regex-special-chars",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_unicode(rec):
|
||||
rows = seed_rows([])
|
||||
text = "café über always"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-unicode-keywords",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_underscore_digit(rec):
|
||||
rows = seed_rows([])
|
||||
text = "v1_2 test_it now"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-underscore-digit-keywords",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_instruction_verbatim(rec):
|
||||
# the INSERT stores the ORIGINAL text, not the lowered/stripped form
|
||||
rows = seed_rows([])
|
||||
text = " ALWAYS check "
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-instruction-verbatim-not-stripped",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_observing(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3,
|
||||
"status": "observing"},
|
||||
])
|
||||
text = "please CHECK everything"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-observing-increment",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_promote(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 2, "threshold": 3,
|
||||
"status": "observing"},
|
||||
])
|
||||
text = "check again"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-promotes-at-threshold",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_already_active(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 5, "threshold": 3,
|
||||
"status": "active", "promoted_at": FROZEN_NOW},
|
||||
])
|
||||
text = "check once more"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-already-active-no-promote",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_first_wins(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3},
|
||||
{"pattern": r"\bcheck|everything\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "check everything"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-first-row-wins",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_skips_retired(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3,
|
||||
"status": "retired"},
|
||||
{"pattern": r"\bbenchmark\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "check the benchmark"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-skips-retired-row",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_word_boundary(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "the checks all pass"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("no-match-word-boundary-plural",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_fallback_pattern(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"do\ it", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "Do IT now"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-fallback-escaped-space-pattern",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_digit_threshold(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bship\b", "count": 3, "threshold": 4},
|
||||
])
|
||||
text = "ship it"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-count-below-higher-threshold",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_after_nonmatch(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bdeploy\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "release the kraken"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-row-after-existing-nonmatch",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
# ── check_context ────────────────────────────────────────────────────────────
|
||||
|
||||
def c_no_active(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3},
|
||||
{"pattern": r"\bship\b", "count": 3, "threshold": 3,
|
||||
"status": "retired"},
|
||||
])
|
||||
context = "check the ship"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-no-active-gates",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_one_match(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "promoted_at": FROZEN_NOW,
|
||||
"instruction": "always check"},
|
||||
])
|
||||
context = "about to check the benchmark"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-one-match",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_multi_match_order(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
{"pattern": r"\bship\b", "count": 7, "threshold": 3,
|
||||
"status": "active", "instruction": "ship weekly"},
|
||||
{"pattern": r"\bbenchmark\b", "count": 4, "threshold": 3,
|
||||
"status": "active", "instruction": "run the benchmark"},
|
||||
])
|
||||
context = "check, ship, benchmark"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-multi-match-rowid-order",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_case_insensitive(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
])
|
||||
context = "CHECK EVERYTHING NOW"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-case-insensitive-context",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_no_match(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
])
|
||||
context = "deploy to production"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-no-match",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_word_boundary(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
])
|
||||
context = "the checks pass"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-word-boundary-plural-no-match",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
# ── list_gates ───────────────────────────────────────────────────────────────
|
||||
|
||||
def l_empty(rec):
|
||||
seed_rows([])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-empty", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
def l_order_desc(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 5, "instruction": "five"},
|
||||
{"pattern": r"\bb\b", "count": 1, "instruction": "one"},
|
||||
{"pattern": r"\bc\b", "count": 3, "instruction": "three"},
|
||||
])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-ordered-count-desc", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
def l_ties(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 2, "instruction": "first"},
|
||||
{"pattern": r"\bb\b", "count": 4, "instruction": "second"},
|
||||
{"pattern": r"\bc\b", "count": 2, "instruction": "third"},
|
||||
])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-tie-order", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
def l_null_promoted_at(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 2, "status": "active",
|
||||
"promoted_at": FROZEN_NOW, "instruction": "active one"},
|
||||
{"pattern": r"\bb\b", "count": 1, "instruction": "observing one"},
|
||||
])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-null-and-set-promoted-at", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
# ── promote_gate ─────────────────────────────────────────────────────────────
|
||||
|
||||
def p_first(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 1, "instruction": "observing"},
|
||||
])
|
||||
ret = oracle_mod.promote_gate(1)
|
||||
rec("promote-existing-id",
|
||||
{"id": 1, "now": FROZEN_NOW}, ret, read_rows(),
|
||||
{"status": "active", "promoted_at": FROZEN_NOW})
|
||||
|
||||
|
||||
def p_unknown_id(rec):
|
||||
seed_rows([])
|
||||
ret = oracle_mod.promote_gate(42)
|
||||
rec("promote-unknown-id",
|
||||
{"id": 42, "now": FROZEN_NOW}, ret, read_rows(),
|
||||
{"status": "active", "promoted_at": FROZEN_NOW})
|
||||
|
||||
|
||||
def p_overrides_retired(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 1, "status": "retired",
|
||||
"instruction": "retired one"},
|
||||
])
|
||||
ret = oracle_mod.promote_gate(1)
|
||||
rec("promote-retired-row-becomes-active",
|
||||
{"id": 1, "now": FROZEN_NOW}, ret, read_rows(),
|
||||
{"status": "active", "promoted_at": FROZEN_NOW})
|
||||
|
||||
|
||||
# ── handle_hook ──────────────────────────────────────────────────────────────
|
||||
|
||||
def h_default(rec):
|
||||
ret = oracle_mod.handle_hook("post-build")
|
||||
rec("hook-plain-event", {"event": "post-build"}, ret, None,
|
||||
{"status": "ok", "event": "post-build", "handler": "task_gate"})
|
||||
|
||||
|
||||
def h_with_data(rec):
|
||||
ret = oracle_mod.handle_hook("pre-commit", {"sha": "abc"})
|
||||
rec("hook-with-data", {"event": "pre-commit"}, ret, None,
|
||||
{"status": "ok", "event": "pre-commit", "handler": "task_gate"})
|
||||
|
||||
|
||||
def h_empty(rec):
|
||||
ret = oracle_mod.handle_hook("")
|
||||
rec("hook-empty-event", {"event": ""}, ret, None,
|
||||
{"status": "ok", "event": "", "handler": "task_gate"})
|
||||
|
||||
|
||||
GROUPS = [
|
||||
("record_instruction", [r_new_keywords, r_new_no_keywords,
|
||||
r_new_fallback_truncate50, r_new_regex_special,
|
||||
r_new_unicode, r_new_underscore_digit,
|
||||
r_new_instruction_verbatim, r_match_observing,
|
||||
r_match_promote, r_match_already_active,
|
||||
r_match_first_wins, r_match_skips_retired,
|
||||
r_match_word_boundary, r_match_fallback_pattern,
|
||||
r_match_digit_threshold, r_new_after_nonmatch]),
|
||||
("check_context", [c_no_active, c_one_match, c_multi_match_order,
|
||||
c_case_insensitive, c_no_match, c_word_boundary]),
|
||||
("list_gates", [l_empty, l_order_desc, l_ties, l_null_promoted_at]),
|
||||
("promote_gate", [p_first, p_unknown_id, p_overrides_retired]),
|
||||
("handle_hook", [h_default, h_with_data, h_empty]),
|
||||
]
|
||||
|
||||
# groups where the kernel is involved (list_gates' ordering is sqlite's)
|
||||
KERNEL_GROUPS = {"record_instruction", "check_context", "promote_gate",
|
||||
"handle_hook"}
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, kinput, oracle_ret, rows_after, kexpected, _r=recs):
|
||||
entry = {"name": name, "input": kinput, "return": oracle_ret,
|
||||
"expected": kexpected}
|
||||
if rows_after is not None:
|
||||
entry["rows_after"] = rows_after
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected", "rows_after"):
|
||||
if key not in entry and key not in old:
|
||||
continue
|
||||
got, want = entry.get(key), old.get(key)
|
||||
if json.dumps(got, sort_keys=True) != json.dumps(
|
||||
want, sort_keys=True):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_failures = sum(
|
||||
1 for v in groups.values() for c in v if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
for group, cases in frozen.items():
|
||||
if group not in KERNEL_GROUPS:
|
||||
continue
|
||||
for case in cases:
|
||||
inp = case["input"]
|
||||
if group == "record_instruction":
|
||||
got = tg_loader.record_instruction(
|
||||
inp["instructions"], inp["text"], inp["now"])
|
||||
elif group == "check_context":
|
||||
got = tg_loader.check_context(
|
||||
inp["instructions"], inp["context"])
|
||||
elif group == "promote_gate":
|
||||
got = tg_loader.promote_gate(inp["id"], inp["now"])
|
||||
else:
|
||||
got = tg_loader.handle_hook(inp["event"])
|
||||
if json.dumps(got, sort_keys=True) != json.dumps(
|
||||
case["expected"], sort_keys=True):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(
|
||||
case["expected"], sort_keys=True)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {total - failures}/{total}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/task_gate.py",
|
||||
"pin": "RUNE_TG_CORE=python",
|
||||
"note": "Frozen via scripts/gen_task_gate_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_NOW + ". "
|
||||
"Battery embeds this VERBATIM.",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1, sort_keys=True)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -31,6 +31,24 @@ import memory_env
|
||||
DB_PATH = os.path.join(memory_env.memory_dir(), "task_gate.db")
|
||||
|
||||
|
||||
def _tg_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_TG_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the verdict logic
|
||||
(pattern extraction, gate matching, count/threshold/status transitions,
|
||||
promotion, active-gate projection, hook echo); the sqlite IO and the
|
||||
CLI stay here."""
|
||||
if os.environ.get("RUNE_TG_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import tg_loader
|
||||
|
||||
if tg_loader.available():
|
||||
return tg_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
@@ -67,6 +85,36 @@ def record_instruction(text):
|
||||
"""Record an instruction. If it matches an existing pattern, increment count."""
|
||||
db = connect()
|
||||
try:
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
now = _now()
|
||||
rows = [dict(r) for r in
|
||||
db.execute("SELECT * FROM instructions").fetchall()]
|
||||
out = core.record_instruction(rows, text, now)
|
||||
if out.get("matched"):
|
||||
db.execute(
|
||||
"UPDATE instructions SET count=?, status=?, last_seen=? WHERE id=?",
|
||||
(out["count"], out["status"], out["last_seen"], out["id"]),
|
||||
)
|
||||
db.commit()
|
||||
return {
|
||||
"matched": True,
|
||||
"id": out["id"],
|
||||
"count": out["count"],
|
||||
"status": out["status"],
|
||||
"promoted": out["promoted"],
|
||||
}
|
||||
row = out["new_row"]
|
||||
cur = db.execute(
|
||||
"INSERT INTO instructions (instruction, pattern, count, threshold, status, created_at, last_seen) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(row["instruction"], row["pattern"], row["count"],
|
||||
row["threshold"], row["status"], row["created_at"],
|
||||
row["last_seen"]),
|
||||
)
|
||||
db.commit()
|
||||
return {"matched": False, "id": cur.lastrowid, "count": 1, "status": "observing"}
|
||||
|
||||
text_lower = text.lower().strip()
|
||||
|
||||
# Check if this matches an existing instruction
|
||||
@@ -112,6 +160,12 @@ def check_context(context):
|
||||
"""Check if context matches any active gates."""
|
||||
db = connect()
|
||||
try:
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
rows = [dict(r) for r in
|
||||
db.execute("SELECT * FROM instructions").fetchall()]
|
||||
return core.check_context(rows, context)["matches"]
|
||||
|
||||
context_lower = context.lower()
|
||||
|
||||
active = db.execute("SELECT * FROM instructions WHERE status='active'").fetchall()
|
||||
@@ -146,6 +200,15 @@ def promote_gate(gate_id):
|
||||
"""Manually promote a gate to active."""
|
||||
db = connect()
|
||||
try:
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
out = core.promote_gate(gate_id, _now())
|
||||
db.execute(
|
||||
"UPDATE instructions SET status='active', promoted_at=? WHERE id=?",
|
||||
(out["promoted_at"], gate_id),
|
||||
)
|
||||
db.commit()
|
||||
return {"promoted": gate_id}
|
||||
db.execute("UPDATE instructions SET status='active', promoted_at=? WHERE id=?", (_now(), gate_id))
|
||||
db.commit()
|
||||
return {"promoted": gate_id}
|
||||
@@ -155,6 +218,9 @@ def promote_gate(gate_id):
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
"""Hook handler — check for repeated instructions."""
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
return core.handle_hook(event)
|
||||
return {"status": "ok", "event": event, "handler": "task_gate"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust task_gate core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* tg_record_instruction({instructions, text, now})
|
||||
→ {matched, id, count, status, promoted, last_seen | new_row}
|
||||
* tg_check_context({instructions, context}) → {matches}
|
||||
* tg_promote_gate({id, now}) → {status, promoted_at}
|
||||
* tg_handle_hook({event}) → {status, event, handler}
|
||||
|
||||
No-clock law: `now` (aware UTC ISO-8601) is always an argument — the
|
||||
kernel never reads the wall clock.
|
||||
|
||||
Boundary: the sqlite IO (connect, the row reads, the UPDATE/INSERT) and
|
||||
the CLI main() stay in the Python facade (task_gate.py); the kernel owns
|
||||
the verdict logic (pattern extraction, gate matching, count/threshold/
|
||||
status transitions, promotion, active-gate projection, hook echo).
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char* — the default c_int truncates 64-bit pointers).
|
||||
Strings are freed via capability_scoring's cs_free_string — the kernel
|
||||
exports NO free symbol of its own (duplicate exports break the cdylib
|
||||
link when sibling crates share one process).
|
||||
|
||||
Override the .so with TG_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "task_gate"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("TG_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtask_gate.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"tg_record_instruction",
|
||||
"tg_check_context",
|
||||
"tg_promote_gate",
|
||||
"tg_handle_hook",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class TaskGateCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust task_gate core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise TaskGateCoreUnavailable(
|
||||
f"Rust task_gate core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def record_instruction(instructions, text, now) -> dict:
|
||||
return _call("tg_record_instruction", {
|
||||
"instructions": instructions, "text": text, "now": now,
|
||||
})
|
||||
|
||||
|
||||
def check_context(instructions, context) -> dict:
|
||||
return _call("tg_check_context", {
|
||||
"instructions": instructions, "context": context,
|
||||
})
|
||||
|
||||
|
||||
def promote_gate(gate_id, now) -> dict:
|
||||
return _call("tg_promote_gate", {"id": gate_id, "now": now})
|
||||
|
||||
|
||||
def handle_hook(event) -> dict:
|
||||
return _call("tg_handle_hook", {"event": event})
|
||||
@@ -0,0 +1,68 @@
|
||||
//! task_gate branch battery — parity vs the frozen Python-oracle truth
|
||||
//! (frozen_tg_truth.json: 32 cases across all four kernel-owned public
|
||||
//! functions and every branch — keyword lattice with the 5-cap, len>3
|
||||
//! codepoint test, Python-3.7 re.escape fallback with the 50-codepoint
|
||||
//! truncation and escaped-space normalization, verbatim instruction
|
||||
//! storage, observing→active promotion at threshold, already-active and
|
||||
//! retired skips, first-row-wins, word boundaries, active-gate context
|
||||
//! projection with rowid order, retired-row promotion, hook echo).
|
||||
//! Frozen 2026-10-11 via scripts/gen_task_gate_truth.py with
|
||||
//! RUNE_TG_CORE=python pinned (poisoned-truth law). Embedded VERBATIM —
|
||||
//! never transcribed.
|
||||
|
||||
use crate::{check_context, handle_hook, promote_gate, record_instruction};
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_tg_truth.json");
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][group]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
for case in cases(group) {
|
||||
let got = f(&case["input"]);
|
||||
assert_eq!(
|
||||
&got,
|
||||
&case["expected"],
|
||||
"[{}] case '{}' diverged",
|
||||
group,
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_instruction_parity_battery() {
|
||||
replay("record_instruction", |i| {
|
||||
record_instruction(
|
||||
i["instructions"].as_array().unwrap(),
|
||||
i["text"].as_str().unwrap(),
|
||||
i["now"].as_str().unwrap(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_context_parity_battery() {
|
||||
replay("check_context", |i| {
|
||||
check_context(
|
||||
i["instructions"].as_array().unwrap(),
|
||||
i["context"].as_str().unwrap(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promote_gate_parity_battery() {
|
||||
replay("promote_gate", |i| promote_gate(i["now"].as_str().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_hook_parity_battery() {
|
||||
replay("handle_hook", |i| handle_hook(i["event"].as_str().unwrap()));
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+327
-104
@@ -1,111 +1,334 @@
|
||||
//! task_gate — repeated task gate: repeated instructions become standards (W6).
|
||||
//! PARITY port of src/rune/task_gate.py record_instruction.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GateInstruction {
|
||||
pub instruction: String,
|
||||
pub pattern: String,
|
||||
pub count: i64,
|
||||
pub threshold: i64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
pub fn extract_pattern(text: &str) -> String {
|
||||
let lower = text.to_lowercase();
|
||||
let words: Vec<&str> = lower
|
||||
.split_whitespace()
|
||||
.filter(|w| w.len() > 3)
|
||||
.take(5)
|
||||
.collect();
|
||||
if words.is_empty() {
|
||||
return regex::escape(&lower[..std::cmp::min(50, lower.len())]);
|
||||
}
|
||||
words
|
||||
.iter()
|
||||
.map(|w| format!(r"\b{}\b", regex::escape(w)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
}
|
||||
|
||||
/// Record an instruction. If it matches an existing gate, increment count.
|
||||
pub fn record_instruction(
|
||||
instructions: &mut Vec<GateInstruction>,
|
||||
text: &str,
|
||||
) -> serde_json::Value {
|
||||
let lower = text.trim().to_lowercase();
|
||||
|
||||
for inst in instructions.iter_mut() {
|
||||
if inst.status == "retired" {
|
||||
continue;
|
||||
}
|
||||
if let Ok(re) = regex::Regex::new(&inst.pattern) {
|
||||
if re.is_match(&lower) {
|
||||
inst.count += 1;
|
||||
let was_active = inst.status == "active";
|
||||
inst.status = if inst.count >= inst.threshold {
|
||||
"active".to_string()
|
||||
} else {
|
||||
"observing".to_string()
|
||||
};
|
||||
return serde_json::json!({
|
||||
"matched": true, "id": inst.count,
|
||||
"count": inst.count, "status": inst.status,
|
||||
"promoted": !was_active && inst.status == "active"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match — create new instruction
|
||||
let pattern = extract_pattern(&lower);
|
||||
let inst = GateInstruction {
|
||||
instruction: text.to_string(),
|
||||
pattern,
|
||||
count: 1,
|
||||
threshold: 3,
|
||||
status: "observing".to_string(),
|
||||
};
|
||||
instructions.push(inst.clone());
|
||||
serde_json::json!({"matched": false, "count": 1, "status": "observing"})
|
||||
}
|
||||
//! task_gate — repeated task gate: repeated instructions become standards
|
||||
//! (W6). FULL parity port of src/rune/task_gate.py (dichotomy wave, issue
|
||||
//! #2479 tranche).
|
||||
//!
|
||||
//! The kernel owns the verdict logic — pattern extraction (Python
|
||||
//! `\w+`/`len > 3`/first-5 lattice with the `re.escape` fallback),
|
||||
//! case-insensitive regex gate matching (first non-retired row wins),
|
||||
//! count/threshold/status transitions, the promotion decision, the
|
||||
//! active-gate context projection, the manual promotion payload and the
|
||||
//! hook echo. The sqlite IO (connect, row reads, UPDATE/INSERT) and the
|
||||
//! CLI stay in the Python facade (task_gate.py); the kernel is a pure
|
||||
//! function of the row docs the facade reads.
|
||||
//!
|
||||
//! Python semantics preserved verbatim (parity is pinned by the frozen
|
||||
//! oracle truth, frozen_tg_truth.json + the branch battery):
|
||||
//! * `text.lower().strip()` feeds matching AND pattern extraction, but
|
||||
//! the INSERTed instruction is the ORIGINAL text (parity case
|
||||
//! new-instruction-verbatim-not-stripped).
|
||||
//! * `len(w) > 3` is a CODEPOINT test, not bytes.
|
||||
//! * the no-keyword fallback is Python 3.7+ `re.escape` of the first 50
|
||||
//! CODEPOINTS of the stripped lower text (space IS escaped → `"\ "`,
|
||||
//! which the matcher normalizes before handing to the regex crate).
|
||||
//! * no-clock law: `now` is always an argument — the kernel never reads
|
||||
//! the wall clock.
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
|
||||
//! one JSON doc `{"ok": true, "value": ...}` / `{"ok": false, "err": ...}`.
|
||||
//! Strings are freed via capability_scoring's cs_free_string — this crate
|
||||
//! exports NO free symbol of its own (duplicate exports break the cdylib
|
||||
//! link when sibling crates share one process).
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::c_char;
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
mod branch_battery;
|
||||
|
||||
#[test]
|
||||
fn test_first_record_creates_new() {
|
||||
let mut instructions = vec![];
|
||||
let r = record_instruction(&mut instructions, "always check the benchmark");
|
||||
assert_eq!(r["matched"], false);
|
||||
assert_eq!(instructions.len(), 1);
|
||||
// ── pattern extraction (name-preserving port of the oracle's inline logic) ──
|
||||
|
||||
/// Python 3.7+ `re.escape`: only characters that can have special meaning
|
||||
/// in a regular expression are escaped. Space and the other whitespace
|
||||
/// characters ARE in that set (verbose-mode guards); letters/digits/underscore
|
||||
/// never are.
|
||||
fn py_re_escape(s: &str) -> String {
|
||||
let special = |c: char| {
|
||||
matches!(
|
||||
c,
|
||||
'(' | ')'
|
||||
| '['
|
||||
| ']'
|
||||
| '{'
|
||||
| '}'
|
||||
| '?'
|
||||
| '*'
|
||||
| '+'
|
||||
| '-'
|
||||
| '|'
|
||||
| '^'
|
||||
| '$'
|
||||
| '\\'
|
||||
| '.'
|
||||
| '&'
|
||||
| '~'
|
||||
| '#'
|
||||
| ' '
|
||||
| '\t'
|
||||
| '\n'
|
||||
| '\r'
|
||||
| '\u{0b}'
|
||||
| '\u{0c}'
|
||||
)
|
||||
};
|
||||
let mut out = String::with_capacity(s.len() + 8);
|
||||
for c in s.chars() {
|
||||
if special(c) {
|
||||
out.push('\\');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repeated_record_increments() {
|
||||
let mut instructions = vec![];
|
||||
record_instruction(&mut instructions, "always check the benchmark");
|
||||
let r = record_instruction(
|
||||
&mut instructions,
|
||||
"always check the benchmark before building",
|
||||
);
|
||||
assert_eq!(r["matched"], true);
|
||||
assert_eq!(r["count"], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_promotion_at_threshold() {
|
||||
let mut instructions = vec![];
|
||||
let pattern = extract_pattern("always check the benchmark");
|
||||
instructions.push(GateInstruction {
|
||||
instruction: "always check the benchmark".into(),
|
||||
pattern,
|
||||
count: 2,
|
||||
threshold: 3,
|
||||
status: "observing".into(),
|
||||
});
|
||||
let r = record_instruction(&mut instructions, "always check the benchmark carefully");
|
||||
assert_eq!(r["promoted"], true);
|
||||
/// First 50 CODEPOINTS of `s` (Python `s[:50]`), returned as a &str slice —
|
||||
/// byte-index slicing would split UTF-8.
|
||||
fn first_n_chars(s: &str, n: usize) -> &str {
|
||||
match s.char_indices().nth(n) {
|
||||
Some((i, _)) => &s[..i],
|
||||
None => s,
|
||||
}
|
||||
}
|
||||
|
||||
/// The gate pattern for a never-seen instruction: word-boundary alternation
|
||||
/// over the first five `\w+` keywords longer than three codepoints, falling
|
||||
/// back to the escaped 50-codepoint prefix of the (already lowered/stripped)
|
||||
/// text. Name preserved from the earlier parity stub.
|
||||
pub fn extract_pattern(lowered: &str) -> String {
|
||||
let keywords: Vec<&str> = crate::word_tokens(lowered)
|
||||
.filter(|w| w.chars().count() > 3)
|
||||
.take(5)
|
||||
.collect();
|
||||
if keywords.is_empty() {
|
||||
py_re_escape(first_n_chars(lowered, 50))
|
||||
} else {
|
||||
keywords
|
||||
.iter()
|
||||
.map(|w| format!(r"\b{}\b", py_re_escape(w)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `re.findall(r"\w+", text)` — Unicode-aware word tokens, borrowed
|
||||
/// zero-copy from the input.
|
||||
fn word_tokens(text: &str) -> impl Iterator<Item = &str> {
|
||||
text.split(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.filter(|w| !w.is_empty())
|
||||
}
|
||||
|
||||
// ── gate matching ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Python `re.search(pattern, text, re.IGNORECASE)`. Python's own re.escape
|
||||
/// escapes whitespace (`"\ "`), which the regex crate rejects — an escaped
|
||||
/// whitespace char denotes exactly that literal char, so unescape before
|
||||
/// compiling. A row whose pattern cannot compile matches nothing (the
|
||||
/// oracle's DB only ever holds oracle-generated patterns).
|
||||
fn py_regex_matches(pattern: &str, text: &str) -> bool {
|
||||
let normalized = if pattern.contains("\\ ") || pattern.contains("\\\t") {
|
||||
let mut s = String::with_capacity(pattern.len());
|
||||
let mut chars = pattern.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\\' {
|
||||
match chars.peek() {
|
||||
Some(' ') => {
|
||||
chars.next();
|
||||
s.push(' ');
|
||||
continue;
|
||||
}
|
||||
Some('\t') => {
|
||||
chars.next();
|
||||
s.push('\t');
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
s.push(c);
|
||||
}
|
||||
s
|
||||
} else {
|
||||
pattern.to_string()
|
||||
};
|
||||
regex::RegexBuilder::new(&normalized)
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.map(|re| re.is_match(text))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn row_str<'a>(row: &'a Value, key: &str) -> &'a str {
|
||||
row.get(key).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
fn row_i64(row: &Value, key: &str) -> i64 {
|
||||
row.get(key).and_then(Value::as_i64).unwrap_or(0)
|
||||
}
|
||||
|
||||
// ── public functions (name-preserving ports of the oracle) ──────────────────
|
||||
|
||||
/// task_gate.record_instruction — match against every non-retired row in
|
||||
/// order; first match increments count and re-decides status; otherwise a
|
||||
/// new observing row (count 1, threshold 3) is produced for the facade to
|
||||
/// INSERT. Returns the kernel decision: the matched-branch update payload
|
||||
/// (including the `last_seen` stamp) or the full new-row doc.
|
||||
pub fn record_instruction(rows: &[Value], text: &str, now: &str) -> Value {
|
||||
let lowered = text.to_lowercase();
|
||||
let lowered = lowered.trim();
|
||||
|
||||
for row in rows {
|
||||
if row_str(row, "status") == "retired" {
|
||||
continue;
|
||||
}
|
||||
if py_regex_matches(row_str(row, "pattern"), lowered) {
|
||||
let new_count = row_i64(row, "count") + 1;
|
||||
let status = row_str(row, "status");
|
||||
let new_status = if new_count >= row_i64(row, "threshold") {
|
||||
"active"
|
||||
} else {
|
||||
"observing"
|
||||
};
|
||||
return json!({
|
||||
"matched": true,
|
||||
"id": row_i64(row, "id"),
|
||||
"count": new_count,
|
||||
"status": new_status,
|
||||
"promoted": new_status == "active" && status != "active",
|
||||
"last_seen": now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
json!({
|
||||
"matched": false,
|
||||
"new_row": {
|
||||
"instruction": text,
|
||||
"pattern": extract_pattern(lowered),
|
||||
"count": 1,
|
||||
"threshold": 3,
|
||||
"status": "observing",
|
||||
"created_at": now,
|
||||
"last_seen": now,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// task_gate.check_context — project the ACTIVE gates whose pattern matches
|
||||
/// the (lowered, NOT stripped) context, in row order. The active-status
|
||||
/// filter is part of the oracle's verdict (its SQL carries
|
||||
/// `WHERE status='active'`), so the kernel applies it to the full row set.
|
||||
pub fn check_context(rows: &[Value], context: &str) -> Value {
|
||||
let lowered = context.to_lowercase();
|
||||
let matches: Vec<Value> = rows
|
||||
.iter()
|
||||
.filter(|row| row_str(row, "status") == "active")
|
||||
.filter(|row| py_regex_matches(row_str(row, "pattern"), &lowered))
|
||||
.map(|row| {
|
||||
json!({
|
||||
"id": row_i64(row, "id"),
|
||||
"instruction": row_str(row, "instruction"),
|
||||
"count": row_i64(row, "count"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({ "matches": matches })
|
||||
}
|
||||
|
||||
/// task_gate.promote_gate — the manual-promotion update payload; the facade
|
||||
/// applies the UPDATE and owns the `{"promoted": id}` echo.
|
||||
pub fn promote_gate(now: &str) -> Value {
|
||||
json!({"status": "active", "promoted_at": now})
|
||||
}
|
||||
|
||||
/// task_gate.handle_hook — the hook echo (data is accepted and ignored by
|
||||
/// the oracle, so the kernel never sees it).
|
||||
pub fn handle_hook(event: &str) -> Value {
|
||||
json!({"status": "ok", "event": event, "handler": "task_gate"})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
|
||||
|
||||
/// Typed FFI failure lattice behind the doc-envelope Result — the
|
||||
/// caller-visible `err` string is unchanged (enum only shapes the Rust
|
||||
/// side; parity is pinned by the battery).
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for FfiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FfiError::NullPtr => write!(f, "null argument pointer"),
|
||||
FfiError::Utf8(e) => write!(f, "{e}"),
|
||||
FfiError::Json(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
|
||||
if ptr.is_null() {
|
||||
return Err(FfiError::NullPtr);
|
||||
}
|
||||
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(FfiError::Utf8)?;
|
||||
serde_json::from_str(x).map_err(FfiError::Json)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e.to_string()}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn rows_of(a: &Value) -> &[Value] {
|
||||
a.get("instructions")
|
||||
.and_then(Value::as_array)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// tg_record_instruction({instructions, text, now})
|
||||
/// → {matched, id/count/status/promoted/last_seen | new_row}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn tg_record_instruction(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
record_instruction(
|
||||
rows_of(&a),
|
||||
a.get("text").and_then(Value::as_str).unwrap_or(""),
|
||||
a.get("now").and_then(Value::as_str).unwrap_or(""),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// tg_check_context({instructions, context}) → {matches}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn tg_check_context(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
check_context(
|
||||
rows_of(&a),
|
||||
a.get("context").and_then(Value::as_str).unwrap_or(""),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// tg_promote_gate({id, now}) → {status, promoted_at}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn tg_promote_gate(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
promote_gate(a.get("now").and_then(Value::as_str).unwrap_or(""))
|
||||
}))
|
||||
}
|
||||
|
||||
/// tg_handle_hook({event}) → {status, event, handler}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn tg_handle_hook(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
handle_hook(a.get("event").and_then(Value::as_str).unwrap_or(""))
|
||||
}))
|
||||
}
|
||||
Reference in new issue
Block a user