feat(self_modify): port to FULL Rust — dichotomy wave

Full name-preserving port of src/rune/self_modify.py to
src/rust/self_modify (RUST verdict, #2479 dichotomy tranche — 7/7 fns
incl. the _run subprocess seam, dichotomy audit match clean):
- _run: the subprocess seam is kernel-owned — Command spawn with piped
  reader threads, poll-based timeout→kill (TimeoutExpired raise-parity
  via the envelope), strict-UTF-8 text=True, universal-newline
  translation, signal exit codes
- propose_change: rev-parse gate ('unknown base' branch), mkdtemp
  0700 'selfmod-' + 8 chars of Python's tempfile alphabet, worktree
  add with err.strip()[:200] char-parity failure branch, no-clock `at` stamp as an argument
- run_gates: default pytest argv pinned, falsy/empty/null cmd falls
  back, (stdout+stderr)[-2000:] CHARACTER tail parity
- compare_baseline: full Python truthiness lattice on passed (missing
  key, null, 0/1 coercion), the three verdict reasons verbatim
- promote_or_discard: lazy rollback_ref read (KeyError fires only
  after merge/remove side effects, like the oracle), merge-fail
  err[:200] (no strip — oracle quirk preserved), discard ignores both
  git failures
- rollback: bad-ref err[:200] verbatim; monitor: .get(key, "")
  passes ANY present value through verbatim (int/null included)
- raise parity at the TYPE level: smod_loader._raise_like_oracle
  re-raises FileNotFoundError/PermissionError/TimeoutExpired/
  KeyError/TypeError from the envelope err encodings
- smod_loader mirrors broker_loader (doc-envelope FFI, cs_free_string,
  SMOD_RUST_LIB override; RUNE_SM_CORE belongs to shard_manager so the
  pin is RUNE_SMOD_CORE); facade delegation with python-oracle
  fallback preserved
- 32 frozen truth rows vs the LIVE oracle (clock frozen, tempfile
  pinned, deterministic git fixtures with literal seed SHA; setup as
  data + PATH-stubbed default gate command), deterministic across two
  generator runs; battery replays VERBATIM (cargo test 8/8) and live
  replay is 32/32 at BOTH facade and kernel level; pytest 6/6 both
  through the Rust core and with RUNE_SMOD_CORE=python
This commit is contained in:
Nick committed 2026-10-12 02:50:56 +01:00
1 parent 4f3d1f7fc1
commit 1af27dc5d4
8 files changed
+2394

No files matched your search

+522
View File
@@ -0,0 +1,522 @@
#!/usr/bin/env python3
"""gen_self_modify_truth.py — freeze/replay parity truth for self_modify
(dichotomy wave, #2479 tranche).
Freeze mode (default): exercises EVERY public function of the LIVE Python
oracle (src/rune/self_modify.py) across every branch and records
(kernel_input, expected_kernel_output) pairs into
src/rust/self_modify/src/frozen_smod_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_SMOD_CORE=python BEFORE any import
(the RUNE_SM_CORE pin already belongs to shard_manager's core on master).
No-clock law: the wall clock is frozen by patching the `time` symbol in
self_modify, so every propose record carries the frozen `at` stamp and
replay is deterministic regardless of run date. The oracle's internal
tempfile.mkdtemp is likewise pinned to a case-local path.
Deterministic-fixture law: git repos are built with fixed identity and
fixed author/committer dates, so the seed commit SHA is identical across
generator runs, replay runs and the Rust battery — the SHA is frozen
LITERALLY, no placeholder. Machine-specific paths (repo dir, worktree dir,
stub dir) are frozen as @REPO@ / @WORKTREE@ / @STUBDIR@ placeholders and
substituted at replay time. Git-dependent scenarios declare their setup as
data ("worktree:<branch>", "commit_in_worktree", "dirty_repo", plus the
"stub" flag for the PATH-stubbed default gate command) so the generator,
the Python replay and the Rust battery run the SAME recipe.
Raise-parity rows freeze {"raises": "<oracle exception>"}: the Python
facade must raise the same exception; the Rust core (envelope law) must
surface an error (the loader raises RuntimeError — documented divergence
in exception TYPE only).
"""
import copy
import json
import os
import stat
import subprocess
import sys
import tempfile
import time
from datetime import datetime, timezone
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TRUTH_PATH = os.path.join(
REPO, "src", "rust", "self_modify", "src", "frozen_smod_truth.json"
)
REPLAY = "--replay" in sys.argv
os.environ["RUNE_SMOD_CORE"] = "python" # poisoned-truth law (freeze mode)
os.environ["RUNE_MEMORY_CORE"] = "python"
sys.path.insert(0, os.path.join(REPO, "src"))
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
import self_modify as oracle_mod # noqa: E402
if REPLAY:
del os.environ["RUNE_SMOD_CORE"] # facade must delegate to the Rust core
import smod_loader # noqa: E402 (kernel-level replay side)
FROZEN_AT = "2026-10-11T12:00:00Z"
FROZEN_EPOCH = int(datetime(2026, 10, 11, 12, 0, 0, tzinfo=timezone.utc).timestamp())
GIT_ENV_FIXED = {
"GIT_AUTHOR_DATE": "2026-10-11T12:00:00+00:00",
"GIT_COMMITTER_DATE": "2026-10-11T12:00:00+00:00",
}
MISSING_REPO = "/nonexistent-selfmod-truth/repo"
class FrozenTime:
"""time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) → FROZEN_AT."""
@staticmethod
def gmtime():
return time.gmtime(FROZEN_EPOCH)
@staticmethod
def strftime(fmt, t=None):
return time.strftime(fmt, t if t is not None else FrozenTime.gmtime())
oracle_mod.time = FrozenTime
class FrozenTempfile:
"""tempfile.mkdtemp(prefix="selfmod-") → a case-local, known path."""
ROOT = None
N = 0
@staticmethod
def mkdtemp(prefix=""):
FrozenTempfile.N += 1
p = os.path.join(FrozenTempfile.ROOT, f"{prefix}{FrozenTempfile.N:02d}")
os.makedirs(p)
return p
oracle_mod.tempfile = FrozenTempfile
# ── deterministic fixture + setup interpreter (shared recipe) ───────────────
def _git(root, *argv):
env = dict(os.environ, **GIT_ENV_FIXED)
return subprocess.run(["git", "-C", root, *argv], capture_output=True,
text=True, env=env)
def make_fixture(case_root):
"""The battery recipe — fixed identity, fixed dates, seed commit.
The resulting master SHA is machine-independent (frozen literally)."""
repo = os.path.join(case_root, "repo")
os.makedirs(repo)
_git(repo, "init", "-q", "-b", "master")
_git(repo, "config", "user.email", "t@t")
_git(repo, "config", "user.name", "t")
_git(repo, "config", "commit.gpgsign", "false")
with open(os.path.join(repo, "f.txt"), "w") as f:
f.write("v1\n")
_git(repo, "add", ".")
_git(repo, "commit", "-qm", "seed")
return repo
def base_sha(repo):
return _git(repo, "rev-parse", "master").stdout.strip()
def run_setup(repo, steps):
"""Declarative setup ops, executed identically by freeze, replay and
the Rust battery. Returns the worktree path (or None)."""
wt = None
for step in steps:
op, _, arg = step.partition(":")
if op == "worktree":
wt = os.path.join(os.path.dirname(repo), f"wt-{arg}")
r = _git(repo, "worktree", "add", wt, "-b", arg, "master")
assert r.returncode == 0, r.stderr
elif op == "commit_in_worktree":
with open(os.path.join(wt, "f.txt"), "w") as f:
f.write("v2\n")
r = _git(wt, "commit", "-qam", "c")
assert r.returncode == 0, r.stderr
elif op == "dirty_repo":
with open(os.path.join(repo, "f.txt"), "w") as f:
f.write("v9\n")
else:
raise ValueError(f"unknown setup op: {step}")
return wt
def make_stub_dir(case_root):
"""PATH stub: a python3 that deterministically 'runs the gate suite'
(the oracle's default gate command resolves through PATH)."""
stub = os.path.join(case_root, "stub")
os.makedirs(stub)
path = os.path.join(stub, "python3")
with open(path, "w") as f:
f.write("#!/bin/sh\necho stub-gates-green\nexit 0\n")
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
return stub
# ── scenario cases ───────────────────────────────────────────────────────────
class Case:
def __init__(self, name, group, steps, inp, stub=False):
self.name = name
self.group = group
self.steps = steps
self.inp = inp # (repo, wt, stub) → kernel input dict
self.stub = stub
def propose_cases():
return [
Case("propose-ok", "propose_change", [],
lambda r, w, s: {"repo": "@REPO@", "branch": "cand-ok",
"base": "master", "now": FROZEN_AT}),
Case("propose-base-head", "propose_change", [],
lambda r, w, s: {"repo": "@REPO@", "branch": "cand-head",
"base": "HEAD", "now": FROZEN_AT}),
Case("propose-unknown-base", "propose_change", [],
lambda r, w, s: {"repo": "@REPO@", "branch": "cand-bad",
"base": "no-such-ref", "now": FROZEN_AT}),
Case("propose-dup-branch", "propose_change", ["worktree:dup"],
lambda r, w, s: {"repo": "@REPO@", "branch": "dup",
"base": "master", "now": FROZEN_AT}),
Case("propose-missing-repo", "propose_change", [],
lambda r, w, s: {"repo": MISSING_REPO, "branch": "cand-gone",
"base": "master", "now": FROZEN_AT}),
]
def gates_cases():
return [
Case("gates-pass", "run_gates", [],
lambda r, w, s: {"worktree": "@REPO@",
"test_cmd": ["echo", "gates-green"]}),
Case("gates-fail-stderr", "run_gates", [],
lambda r, w, s: {"worktree": "@REPO@",
"test_cmd": ["sh", "-c", "echo boom 1>&2; exit 1"]}),
Case("gates-tail-2000", "run_gates", [],
lambda r, w, s: {"worktree": "@REPO@",
"test_cmd": ["sh", "-c", "seq 1 3000"]}),
Case("gates-out-then-err", "run_gates", [],
lambda r, w, s: {"worktree": "@REPO@",
"test_cmd": ["sh", "-c",
"echo o1; echo e1 1>&2; echo o2; exit 7"]}),
Case("gates-default-cmd", "run_gates", [],
lambda r, w, s: {"worktree": "@REPO@", "test_cmd": None},
stub=True),
]
def compare_cases():
def row(name, cand, base):
return Case("cb-" + name, "compare_baseline", [],
lambda r, w, s, c=cand, b=base:
{"candidate_result": c, "baseline_result": b})
return [
row("candidate-red", {"passed": False}, {"passed": True}),
row("candidate-red-baseline-red", {"passed": False}, {"passed": False}),
row("baseline-red", {"passed": True}, {"passed": False}),
row("both-green", {"passed": True}, {"passed": True}),
row("missing-passed-key", {}, {"passed": True}),
row("truthy-coercion", {"passed": 1}, {"passed": 0}),
row("explicit-null-passed", {"passed": None}, {"passed": True}),
]
def _proposal(branch, repo):
return {"proposed": True, "branch": branch, "worktree": "@WORKTREE@",
"base": "master", "rollback_ref": base_sha(repo), "at": FROZEN_AT}
def pod_cases():
return [
Case("pod-discard", "promote_or_discard", ["worktree:discard-1"],
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("discard-1", r),
"verdict": {"promote": False, "reason": "test"}}),
Case("pod-promote-up-to-date", "promote_or_discard", ["worktree:podd-2"],
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("podd-2", r),
"verdict": {"promote": True, "reason": "test"}}),
Case("pod-promote-real-merge", "promote_or_discard",
["worktree:podd-3", "commit_in_worktree"],
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("podd-3", r),
"verdict": {"promote": True, "reason": "test"}}),
Case("pod-merge-fail", "promote_or_discard",
["worktree:podd-4", "commit_in_worktree", "dirty_repo"],
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("podd-4", r),
"verdict": {"promote": True, "reason": "test"}}),
Case("pod-discard-ghost", "promote_or_discard", [],
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("ghost-branch", r),
"verdict": {"promote": False, "reason": "test"}}),
Case("pod-missing-branch-key", "promote_or_discard", [],
lambda r, w, s: {"repo": "@REPO@",
"proposal": {k: v for k, v in _proposal("keyless", r).items()
if k != "branch"},
"verdict": {"promote": True}}),
]
def rollback_cases():
return [
Case("rb-ok-after-dirty", "rollback", ["dirty_repo"],
lambda r, w, s: {"repo": "@REPO@", "rollback_ref": base_sha(r)}),
Case("rb-ref-master", "rollback", ["dirty_repo"],
lambda r, w, s: {"repo": "@REPO@", "rollback_ref": "master"}),
Case("rb-bad-ref", "rollback", [],
lambda r, w, s: {"repo": "@REPO@", "rollback_ref": "no-such-ref"}),
Case("rb-missing-repo", "rollback", [],
lambda r, w, s: {"repo": MISSING_REPO, "rollback_ref": "master"}),
]
def monitor_cases():
def row(name, proposal):
return Case("mon-" + name, "monitor", [],
lambda r, w, s, p=proposal: {"proposal": p})
return [
row("full", {"branch": "b", "rollback_ref": "abc123"}),
row("empty", {}),
row("partial", {"branch": "only-b"}),
row("extra-keys", {"branch": "b", "rollback_ref": "r", "extra": 1}),
row("nonstring-branch", {"branch": 7, "rollback_ref": None}),
]
GROUPS = [
("propose_change", propose_cases()),
("run_gates", gates_cases()),
("compare_baseline", compare_cases()),
("promote_or_discard", pod_cases()),
("rollback", rollback_cases()),
("monitor", monitor_cases()),
]
FACADE_CALL = {
"propose_change": lambda i: oracle_mod.propose_change(
i["repo"], i["branch"], i["base"]),
"run_gates": lambda i: oracle_mod.run_gates(i["worktree"], i["test_cmd"]),
"compare_baseline": lambda i: oracle_mod.compare_baseline(
i["candidate_result"], i["baseline_result"]),
"promote_or_discard": lambda i: oracle_mod.promote_or_discard(
i["repo"], i["proposal"], i["verdict"]),
"rollback": lambda i: oracle_mod.rollback(i["repo"], i["rollback_ref"]),
"monitor": lambda i: oracle_mod.monitor(i["proposal"]),
}
KERNEL_CALL = {
"propose_change": lambda i: smod_loader.propose_change(
i["repo"], i["branch"], i["base"], i["now"]),
"run_gates": lambda i: smod_loader.run_gates(i["worktree"], i["test_cmd"]),
"compare_baseline": lambda i: smod_loader.compare_baseline(
i["candidate_result"], i["baseline_result"]),
"promote_or_discard": lambda i: smod_loader.promote_or_discard(
i["repo"], i["proposal"], i["verdict"]),
"rollback": lambda i: smod_loader.rollback(i["repo"], i["rollback_ref"]),
"monitor": lambda i: smod_loader.monitor(i["proposal"]),
}
# ── placeholder plumbing ─────────────────────────────────────────────────────
def substitute(value, repo, wt, stub):
"""Machine paths → frozen placeholders (kernel input side)."""
s = json.dumps(value)
for actual, ph in ((repo, "@REPO@"), (wt, "@WORKTREE@"), (stub, "@STUBDIR@")):
if actual:
s = s.replace(actual, ph)
return json.loads(s)
def de_substitute(value, repo, wt, stub):
"""Frozen placeholders → live paths (call side)."""
s = json.dumps(value)
for actual, ph in ((repo, "@REPO@"), (wt, "@WORKTREE@"), (stub, "@STUBDIR@")):
if actual:
s = s.replace(ph, actual)
return json.loads(s)
def normalize(out, repo, wt, stub):
"""Actual output paths → placeholders (kernel output side). The
worktree a propose call created internally is captured from the
output itself — neither side can know it in advance."""
reps = [t for t in ((wt, "@WORKTREE@") if wt else None,
(repo, "@REPO@") if repo else None,
(stub, "@STUBDIR@") if stub else None) if t]
if isinstance(out, dict) and isinstance(out.get("worktree"), str):
reps.append((out["worktree"], "@WORKTREE@"))
s = json.dumps(out, sort_keys=True)
for actual, ph in reps:
if actual and actual != "/":
s = s.replace(actual, ph)
return json.loads(s)
def _catch(fn, *a):
try:
return fn(*a)
except FileNotFoundError:
return {"__raised__": "FileNotFoundError"}
except KeyError as e:
return {"__raised__": f"KeyError: {e}"}
except RuntimeError:
return {"__raised__": "RuntimeError"}
class _PathEnv:
"""Set PATH to the stub dir for the duration of one call."""
def __init__(self, stub):
self.stub = stub
def __enter__(self):
self.old = os.environ.get("PATH")
os.environ["PATH"] = self.stub + os.pathsep + (self.old or "")
def __exit__(self, *exc):
if self.old is not None:
os.environ["PATH"] = self.old
return False
def run_scenario(case, tmp_root, call):
"""One scenario, identical for freeze and both replay tiers:
fresh fixture → setup → substituted input → call → normalized output."""
case_root = tempfile.mkdtemp(prefix=f"smod_{case.name}_", dir=tmp_root)
repo = make_fixture(case_root)
FrozenTempfile.ROOT = case_root
FrozenTempfile.N = 0
wt = run_setup(repo, case.steps)
stub = make_stub_dir(case_root) if case.stub else None
live_wt = wt or os.path.join(case_root, "wt-input")
live_stub = stub or os.path.join(case_root, "stub-unused")
inp = substitute(case.inp(repo, wt, stub), repo, wt, stub)
inp_copy = copy.deepcopy(inp) # deep-copy law: inputs frozen before calls
live = de_substitute(inp_copy, repo, live_wt, live_stub)
if case.stub:
with _PathEnv(live_stub):
out = _catch(call, live)
else:
out = _catch(call, live)
if isinstance(out, dict) and out.get("__raised__"):
return inp, {"raises": out["__raised__"]}
return inp, normalize(out, repo, live_wt, stub)
def main():
frozen = None
if REPLAY:
with open(TRUTH_PATH) as f:
frozen = json.load(f)["cases"]
tmp_root = tempfile.mkdtemp(prefix="smod_truth_")
groups = {}
failures = 0
total = 0
for group, cases in GROUPS:
recs = []
def rec(name, inp, expected, case=None, _r=recs, _g=group):
entry = {"name": name, "input": inp, "expected": expected}
if case is not None:
entry["setup"] = case.steps
entry["stub"] = case.stub
if REPLAY:
old = next(c for c in frozen[_g] if c["name"] == name)
for key in ("input", "expected"):
if json.dumps(entry[key], sort_keys=True) != json.dumps(
old[key], sort_keys=True):
print(f"FACADE DIVERGE [{_g}] {name} {key}")
print(" got: ", json.dumps(entry[key], sort_keys=True)[:600])
print(" want:", json.dumps(old[key], sort_keys=True)[:600])
_r.append({"name": name, "diverged": key})
return
_r.append(entry)
for case in cases:
total += 1
inp, want = run_scenario(case, tmp_root,
lambda i, c=case: _catch(
FACADE_CALL[c.group], i))
rec(case.name, inp, want, case)
if REPLAY:
old = next(c for c in frozen[case.group] if c["name"] == case.name)
repo, wt, stub = kernel_paths(case, tmp_root, old)
call = lambda: KERNEL_CALL[case.group](
de_substitute(copy.deepcopy(old["input"]), repo, wt, stub))
if old.get("stub"):
with _PathEnv(stub):
kout = _catch(lambda: call())
else:
kout = _catch(call)
kwant = old["expected"]
if not (isinstance(kout, dict) and kout.get("__raised__")):
kout = normalize(kout, repo, wt, stub)
if isinstance(kwant, dict) and kwant.get("raises"):
ok = kout == {"__raised__": kwant["raises"]} or (
kout == {"__raised__": "RuntimeError"})
else:
ok = json.dumps(kout, sort_keys=True) == json.dumps(
kwant, sort_keys=True)
if not ok:
failures += 1
print(f"KERNEL DIVERGE [{case.group}] {case.name}")
print(" got: ", json.dumps(kout, sort_keys=True)[:600])
print(" want:", json.dumps(kwant, sort_keys=True)[:600])
groups[group] = recs
if REPLAY:
facade_div = sum(1 for v in groups.values() for c in v if "diverged" in c)
print(f"replay: facade {total - facade_div}/{total}, "
f"kernel {total - failures}/{total}")
return 1 if (failures or facade_div) else 0
doc = {
"frozen": datetime.now(timezone.utc).isoformat(),
"oracle": "src/rune/self_modify.py",
"pin": "RUNE_SMOD_CORE=python",
"note": "Frozen via scripts/gen_self_modify_truth.py against the live "
"Python oracle; wall clock frozen at " + FROZEN_AT + "; git "
"fixtures use fixed identity+dates so the seed SHA is "
"literal. @REPO@/@WORKTREE@/@STUBDIR@ are placeholders "
"substituted at replay. 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
def kernel_paths(case, tmp_root, old_entry):
"""(repo, wt, stub) for kernel-level replay of one frozen entry."""
case_root = tempfile.mkdtemp(prefix=f"smod_kernel_{case.name}_", dir=tmp_root)
repo = make_fixture(case_root)
wt = run_setup(repo, old_entry.get("setup", []))
stub = None
if old_entry.get("stub"):
stub = make_stub_dir(case_root)
live_wt = wt or os.path.join(case_root, "wt-input")
live_stub = stub or os.path.join(case_root, "stub-unused")
return repo, live_wt, live_stub
if __name__ == "__main__":
sys.exit(main())
+47
View File
@@ -13,6 +13,14 @@ the gate suite green + no regression vs the baseline run.
Worktree isolation: proposals build on `git worktree` branches off the
live repo, so the candidate never touches the working tree until
promoted. Discard = remove worktree + delete branch (full cleanup).
Dichotomy wave #2479: the FULL Rust core (src/rust/self_modify via
smod_loader) owns the subprocess seam (_run: every git invocation and
gate command) and every proposal decision; this module remains the
parity oracle and the strangler fallback (RUNE_SMOD_CORE=python pins it
— the RUNE_SM_CORE pin belongs to shard_manager). Raise parity holds at
the type level: the loader re-raises FileNotFoundError /
TimeoutExpired / KeyError / TypeError exactly as the oracle would.
"""
from __future__ import annotations
@@ -24,6 +32,25 @@ import tempfile
import time
def _smod_core():
"""Dichotomy wave #2479: Rust core first (src/rust/self_modify via
smod_loader). RUNE_SMOD_CORE=python pins the Python fallback below —
it remains the parity oracle (strangler law). The RUNE_SM_CORE pin
belongs to shard_manager. The kernel owns the subprocess seam (_run:
every git invocation and gate command) and every proposal decision;
the wall clock enters as an argument (no-clock law)."""
if os.environ.get("RUNE_SMOD_CORE", "auto") == "python":
return None
try:
from rune import smod_loader
if smod_loader.available():
return smod_loader
except ImportError:
pass
return None
def _run(cmd, cwd, timeout=300):
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
timeout=timeout)
@@ -33,6 +60,11 @@ def _run(cmd, cwd, timeout=300):
def propose_change(repo: str, branch: str, base: str = "master") -> dict:
"""Create the isolated worktree + branch. Returns the proposal record
(with rollback_ref = the base HEAD — the recovery path)."""
core = _smod_core()
if core is not None:
return core.propose_change(
repo, branch, base,
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
rc, out, err = _run(["git", "rev-parse", base], cwd=repo)
if rc != 0:
return {"proposed": False, "error": f"unknown base: {base}"}
@@ -49,6 +81,9 @@ def propose_change(repo: str, branch: str, base: str = "master") -> dict:
def run_gates(worktree: str, test_cmd: list | None = None) -> dict:
"""Run the gate suite inside the worktree. Returns {passed, output}."""
core = _smod_core()
if core is not None:
return core.run_gates(worktree, test_cmd)
cmd = test_cmd or ["python3", "-m", "pytest", "tests/", "-q", "-x"]
rc, out, err = _run(cmd, cwd=worktree, timeout=600)
return {"passed": rc == 0,
@@ -57,6 +92,9 @@ def run_gates(worktree: str, test_cmd: list | None = None) -> dict:
def compare_baseline(candidate_result: dict, baseline_result: dict) -> dict:
"""Compare candidate vs baseline gate runs. Regression blocks."""
core = _smod_core()
if core is not None:
return core.compare_baseline(candidate_result, baseline_result)
if not candidate_result.get("passed"):
return {"promote": False,
"reason": "candidate gates red — regression blocks promotion"}
@@ -69,6 +107,9 @@ def compare_baseline(candidate_result: dict, baseline_result: dict) -> dict:
def promote_or_discard(repo: str, proposal: dict, verdict: dict) -> dict:
"""Merge (promote) or delete (discard) the proposal branch."""
core = _smod_core()
if core is not None:
return core.promote_or_discard(repo, proposal, verdict)
branch = proposal["branch"]
wt = proposal["worktree"]
if verdict.get("promote"):
@@ -88,6 +129,9 @@ def promote_or_discard(repo: str, proposal: dict, verdict: dict) -> dict:
def rollback(repo: str, rollback_ref: str) -> dict:
"""Restore the pre-change ref. The recovery path every proposal carries."""
core = _smod_core()
if core is not None:
return core.rollback(repo, rollback_ref)
rc, _out, err = _run(["git", "reset", "--hard", rollback_ref], cwd=repo)
if rc != 0:
return {"rolled_back": False, "error": err[:200]}
@@ -96,6 +140,9 @@ def rollback(repo: str, rollback_ref: str) -> dict:
def monitor(proposal: dict) -> dict:
"""The post-promotion watch record: what to observe after promotion."""
core = _smod_core()
if core is not None:
return core.monitor(proposal)
return {"monitoring": True, "branch": proposal.get("branch", ""),
"rollback_ref": proposal.get("rollback_ref", ""),
"watch": ["gate suite green on next CI run",
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust self_modify core (dichotomy wave, #2479
tranche). Mirrors broker_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* smod_propose_change({repo, branch, base, now}) -> proposal dict
* smod_run_gates({worktree, test_cmd|null}) -> {passed, output}
* smod_compare_baseline({candidate_result, baseline_result}) -> verdict
* smod_promote_or_discard({repo, proposal, verdict}) -> result dict
* smod_rollback({repo, rollback_ref}) -> result dict
* smod_monitor({proposal}) -> watch record
No-clock law: `now` (the oracle's
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())) is always an
argument — the kernel never reads the wall clock.
Boundary: the subprocess seam (_run — every git invocation and gate
command) is owned by the kernel, exactly like the oracle's _run. The
facade keeps only the wall clock and the RUNE_SMOD_CORE pin.
Raise parity: the oracle RAISES (FileNotFoundError from a missing cwd /
binary, TimeoutExpired on gate timeouts, KeyError from a missing proposal
key, TypeError at the argv seam). The kernel answers with the envelope's
err half and this loader re-raises the ORACLE'S exception TYPE via
_raise_like_oracle — raise parity holds at the type level, not just
raise-vs-return.
Free: capability_scoring's cs_free_string (shared process) — this crate
deliberately exports NO free symbol of its own (duplicate exports break
the cdylib link when sibling crates share one process). Override the .so
with SMOD_RUST_LIB (a FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
import re
import subprocess
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "self_modify"
_LIB_PATH = Path(
os.environ.get("SMOD_RUST_LIB", _CRATE_DIR / "target" / "release" / "libself_modify.so")
)
_SYMBOLS = (
"smod_propose_change",
"smod_run_gates",
"smod_compare_baseline",
"smod_promote_or_discard",
"smod_rollback",
"smod_monitor",
)
_lib = None
class SelfModifyCoreUnavailable(FileNotFoundError):
"""The Rust self-modify 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 SelfModifyCoreUnavailable(
f"Rust self_modify 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 _raise_like_oracle(err: str, sym: str):
"""Re-raise the ORACLE's exception type, not a blanket RuntimeError.
The kernel's envelope err encodes enough to reconstruct the raise
parity the Python oracle has (subprocess FileNotFoundError /
PermissionError / TimeoutExpired, dict KeyError, argv TypeError):
* "[Errno N] …" → OSError subclass by errno (spawn seam)
* "Command '…' timed out after S seconds" → subprocess.TimeoutExpired
* "KeyError: 'k'" → KeyError(k)
* "TypeError: …" → TypeError
Anything else stays a RuntimeError.
"""
if err.startswith("[Errno "):
try:
errno = int(err[7:].split("]", 1)[0])
except ValueError:
raise RuntimeError(err) from None
if errno == 2:
raise FileNotFoundError(err)
if errno == 13:
raise PermissionError(err)
if errno == 20:
raise NotADirectoryError(err)
raise OSError(errno, err)
m = re.match(r"Command '(.*)' timed out after (\d+) seconds$", err)
if m:
raise subprocess.TimeoutExpired(m.group(1), float(m.group(2)))
if err.startswith("KeyError: "):
raise KeyError(err[len("KeyError: "):].strip("'"))
if err.startswith("TypeError: "):
raise TypeError(err[len("TypeError: "):])
raise RuntimeError(err)
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 capability_scoring's cs_free_string (same process)
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_like_oracle(doc.get("err", f"{sym} failed"), sym)
return doc["value"]
def propose_change(repo: str, branch: str, base: str, now: str) -> dict:
return _call("smod_propose_change", {
"repo": repo, "branch": branch, "base": base, "now": now,
})
def run_gates(worktree: str, test_cmd: list | None) -> dict:
return _call("smod_run_gates", {
"worktree": worktree, "test_cmd": test_cmd,
})
def compare_baseline(candidate_result: dict, baseline_result: dict) -> dict:
return _call("smod_compare_baseline", {
"candidate_result": candidate_result,
"baseline_result": baseline_result,
})
def promote_or_discard(repo: str, proposal: dict, verdict: dict) -> dict:
return _call("smod_promote_or_discard", {
"repo": repo, "proposal": proposal, "verdict": verdict,
})
def rollback(repo: str, rollback_ref: str) -> dict:
return _call("smod_rollback", {
"repo": repo, "rollback_ref": rollback_ref,
})
def monitor(proposal: dict) -> dict:
return _call("smod_monitor", {"proposal": proposal})
+135
View File
@@ -0,0 +1,135 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "self_modify"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"foldhash",
"indexmap",
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "self_modify"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }
+343
View File
@@ -0,0 +1,343 @@
//! self_modify branch battery — parity vs the frozen Python-oracle truth
//! (frozen_smod_truth.json: 32 cases across all six public functions and
//! every branch — propose ok/HEAD/unknown-base/duplicate-branch/missing
//! repo, gates pass/fail-stderr/2000-char tail/out-then-err/PATH-stubbed
//! default command, the full compare_baseline truthiness lattice, promote
//! up-to-date/real-merge/merge-fail/discard/ghost/missing-key, rollback
//! ok/master/bad-ref/missing repo, monitor full/empty/partial/extra/
//! non-string pass-through). Frozen via scripts/gen_self_modify_truth.py
//! with RUNE_SMOD_CORE=python pinned (poisoned-truth law). Embedded
//! VERBATIM — never transcribed.
//!
//! Deterministic-fixture law: git fixtures use fixed identity and fixed
//! author/committer dates, so the seed SHA is identical everywhere and is
//! frozen LITERALLY; @REPO@ / @WORKTREE@ / @STUBDIR@ placeholders are
//! substituted per fresh fixture, and the declarative per-case "setup"
//! steps run through the same interpreter as the generator and the Python
//! replay. Raise-parity rows ({"raises": ...}) assert the kernel returns
//! an error (envelope law — the loader re-raises).
use crate::{
compare_baseline, monitor, propose_change, promote_or_discard, rollback, run_gates,
FfiError,
};
use serde_json::{json, Value};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
const TRUTH: &str = include_str!("frozen_smod_truth.json");
/// One gate at a time: the PATH-stub case mutates process env, and git
/// fixtures share the temp root.
static SERIAL: Mutex<()> = Mutex::new(());
fn truth() -> &'static Value {
static TRUTH_VAL: OnceLock<Value> = OnceLock::new();
TRUTH_VAL.get_or_init(|| serde_json::from_str(TRUTH).expect("frozen truth parses"))
}
fn cases(group: &str) -> &[Value] {
truth()["cases"][group]
.as_array()
.expect("frozen group exists")
}
// ── fixture + setup interpreter (same recipe as the generator) ───────────
fn battery_root(group: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"smod-battery-{}-{group}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("battery root");
root
}
fn git(root: &Path, args: &[&str]) -> (bool, String, String) {
let out = Command::new("git")
.arg("-C")
.arg(root)
.args(args)
.env("GIT_AUTHOR_DATE", "2026-10-11T12:00:00+00:00")
.env("GIT_COMMITTER_DATE", "2026-10-11T12:00:00+00:00")
.output()
.expect("git runs");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn make_fixture(case_root: &Path) -> PathBuf {
let repo = case_root.join("repo");
fs::create_dir_all(&repo).expect("fixture repo dir");
let (ok, _, err) = git(&repo, &["init", "-q", "-b", "master"]);
assert!(ok, "git init failed: {err}");
git(&repo, &["config", "user.email", "t@t"]);
git(&repo, &["config", "user.name", "t"]);
git(&repo, &["config", "commit.gpgsign", "false"]);
fs::write(repo.join("f.txt"), "v1\n").expect("seed file");
git(&repo, &["add", "."]);
let (ok, _, err) = git(&repo, &["commit", "-qm", "seed"]);
assert!(ok, "seed commit failed: {err}");
repo
}
fn base_sha(repo: &Path) -> String {
let (ok, out, _) = git(repo, &["rev-parse", "master"]);
assert!(ok, "rev-parse master failed");
out.trim().to_string()
}
fn run_setup(repo: &Path, steps: &[Value]) -> Option<PathBuf> {
let mut wt = None;
for step in steps {
let step = step.as_str().expect("setup step is a string");
let (op, arg) = match step.split_once(':') {
Some((op, arg)) => (op, arg),
None => (step, ""),
};
match op {
"worktree" => {
let path = repo.parent().unwrap().join(format!("wt-{arg}"));
let (ok, _, err) =
git(repo, &["worktree", "add", &path.to_string_lossy(), "-b", arg, "master"]);
assert!(ok, "worktree add failed: {err}");
wt = Some(path);
}
"commit_in_worktree" => {
let path = wt.clone().expect("worktree exists");
fs::write(path.join("f.txt"), "v2\n").expect("worktree file");
let (ok, _, err) = git(&path, &["commit", "-qam", "c"]);
assert!(ok, "worktree commit failed: {err}");
}
"dirty_repo" => {
fs::write(repo.join("f.txt"), "v9\n").expect("dirty file");
}
other => panic!("unknown setup op: {other}"),
}
}
wt
}
fn make_stub_dir(case_root: &Path) -> PathBuf {
let stub = case_root.join("stub");
fs::create_dir_all(&stub).expect("stub dir");
let path = stub.join("python3");
fs::write(&path, "#!/bin/sh\necho stub-gates-green\nexit 0\n").expect("stub script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&path).expect("stub stat").permissions();
perms.set_mode(perms.mode() | 0o100);
fs::set_permissions(&path, perms).expect("stub chmod");
}
stub
}
// ── placeholder plumbing ─────────────────────────────────────────────────
fn de_substitute(value: &Value, repo: &Path, wt: &Path, stub: &Path) -> Value {
let s = serde_json::to_string(value).expect("serialize");
let s = s
.replace("@REPO@", &repo.to_string_lossy())
.replace("@WORKTREE@", &wt.to_string_lossy())
.replace("@STUBDIR@", &stub.to_string_lossy());
serde_json::from_str(&s).expect("resubstituted json")
}
fn normalize(got: &Value, repo: &Path, wt: &Path, stub: &Path) -> Value {
let mut s = serde_json::to_string(got).expect("serialize got");
let reps: Vec<(String, &str)> = vec![
(
got.get("worktree")
.and_then(Value::as_str)
.map(str::to_string),
"@WORKTREE@",
),
(Some(repo.to_string_lossy().into_owned()), "@REPO@"),
(Some(stub.to_string_lossy().into_owned()), "@STUBDIR@"),
(Some(wt.to_string_lossy().into_owned()), "@WORKTREE@"),
]
.into_iter()
.filter_map(|(a, ph)| a.map(|a| (a, ph)))
.collect();
for (actual, ph) in reps {
s = s.replace(&actual, ph);
}
serde_json::from_str(&s).expect("normalized json")
}
// ── kernel dispatch ──────────────────────────────────────────────────────
fn call_kernel(group: &str, input: &Value) -> Result<Value, FfiError> {
let s = |key: &str| input.get(key).and_then(Value::as_str).unwrap_or("");
match group {
"propose_change" => propose_change(s("repo"), s("branch"), s("base"), s("now")),
"run_gates" => run_gates(
s("worktree"),
input.get("test_cmd").and_then(Value::as_array),
),
"compare_baseline" => Ok(compare_baseline(
&input["candidate_result"],
&input["baseline_result"],
)),
"promote_or_discard" => promote_or_discard(
s("repo"),
&input["proposal"],
&input["verdict"],
),
"rollback" => rollback(s("repo"), s("rollback_ref")),
"monitor" => Ok(monitor(&input["proposal"])),
other => panic!("unknown kernel group: {other}"),
}
}
fn replay_case(group: &str, case: &Value) {
let name = case["name"].as_str().expect("case name");
let root = battery_root(group);
let case_root = root.join(name);
fs::create_dir_all(&case_root).expect("case root");
let repo = make_fixture(&case_root);
let wt = run_setup(&repo, case["setup"].as_array().map(Vec::as_slice).unwrap_or(&[]))
.unwrap_or_else(|| case_root.join("wt-input"));
let stub = if case["stub"].as_bool().unwrap_or(false) {
make_stub_dir(&case_root)
} else {
case_root.join("stub-unused")
};
let live = de_substitute(&case["input"], &repo, &wt, &stub);
let got = if case["stub"].as_bool().unwrap_or(false) {
let guard = PathGuard::stub(&stub);
let r = call_kernel(group, &live);
guard.restore();
r
} else {
call_kernel(group, &live)
};
let expected = &case["expected"];
if expected.get("raises").is_some() {
assert!(
got.is_err(),
"[{group}] {name}: expected a raise ({}), kernel returned {:?}",
expected["raises"].as_str().unwrap_or(""),
got.ok()
);
return;
}
let got = got.unwrap_or_else(|e| {
panic!("[{group}] {name}: kernel raised {e}, expected {:?}", expected)
});
let got = normalize(&got, &repo, &wt, &stub);
assert_eq!(
&got,
expected,
"[{group}] {name} diverged (frozen truth, kernel-level)"
);
}
/// Sets PATH to the stub dir; restore() puts the old PATH back.
struct PathGuard {
old: Option<std::ffi::OsString>,
}
impl PathGuard {
fn stub(stub: &Path) -> PathGuard {
let old = std::env::var_os("PATH");
std::env::set_var(
"PATH",
std::env::join_paths(
std::iter::once(stub.to_path_buf()).chain(
std::env::split_paths(old.as_deref().unwrap_or_default()),
),
)
.expect("joined PATH"),
);
PathGuard { old }
}
fn restore(self) {
if let Some(old) = self.old {
std::env::set_var("PATH", old);
}
}
}
// ── the battery ──────────────────────────────────────────────────────────
fn replay_group(group: &str) -> usize {
let _serial = SERIAL.lock().expect("serial battery");
let group_cases = cases(group);
for case in group_cases {
replay_case(group, case);
}
group_cases.len()
}
#[test]
fn propose_change_parity_battery() {
assert_eq!(replay_group("propose_change"), 5);
}
#[test]
fn run_gates_parity_battery() {
assert_eq!(replay_group("run_gates"), 5);
}
#[test]
fn compare_baseline_parity_battery() {
assert_eq!(replay_group("compare_baseline"), 7);
}
#[test]
fn promote_or_discard_parity_battery() {
assert_eq!(replay_group("promote_or_discard"), 6);
}
#[test]
fn rollback_parity_battery() {
assert_eq!(replay_group("rollback"), 4);
}
#[test]
fn monitor_parity_battery() {
assert_eq!(replay_group("monitor"), 5);
}
/// The frozen propose rows pin the seed SHA literally — assert the
/// battery fixture reproduces it (the deterministic-fixture law itself).
#[test]
fn fixture_sha_is_deterministic() {
let _serial = SERIAL.lock().expect("serial battery");
let root = battery_root("sha-probe");
fs::create_dir_all(&root).expect("probe root");
let repo = make_fixture(&root);
assert_eq!(
base_sha(&repo),
cases("rollback")[0]["input"]["rollback_ref"]
.as_str()
.expect("frozen sha"),
"fixture recipe no longer reproduces the frozen seed SHA"
);
}
/// The monitor watch list is verbatim oracle vocabulary, pinned twice
/// (frozen rows + this direct assertion).
#[test]
fn monitor_watch_vocabulary() {
let m = monitor(&json!({"branch": "b", "rollback_ref": "abc123"}));
assert_eq!(
m["watch"],
json!([
"gate suite green on next CI run",
"no new failure signatures in the next 24h"
])
);
assert_eq!(m["monitoring"], json!(true));
}
@@ -0,0 +1,612 @@
{
"cases": {
"compare_baseline": [
{
"expected": {
"promote": false,
"reason": "candidate gates red \u2014 regression blocks promotion"
},
"input": {
"baseline_result": {
"passed": true
},
"candidate_result": {
"passed": false
}
},
"name": "cb-candidate-red",
"setup": [],
"stub": false
},
{
"expected": {
"promote": false,
"reason": "candidate gates red \u2014 regression blocks promotion"
},
"input": {
"baseline_result": {
"passed": false
},
"candidate_result": {
"passed": false
}
},
"name": "cb-candidate-red-baseline-red",
"setup": [],
"stub": false
},
{
"expected": {
"promote": true,
"reason": "candidate green, baseline was already red \u2014 no regression introduced"
},
"input": {
"baseline_result": {
"passed": false
},
"candidate_result": {
"passed": true
}
},
"name": "cb-baseline-red",
"setup": [],
"stub": false
},
{
"expected": {
"promote": true,
"reason": "candidate green, baseline green \u2014 no regression"
},
"input": {
"baseline_result": {
"passed": true
},
"candidate_result": {
"passed": true
}
},
"name": "cb-both-green",
"setup": [],
"stub": false
},
{
"expected": {
"promote": false,
"reason": "candidate gates red \u2014 regression blocks promotion"
},
"input": {
"baseline_result": {
"passed": true
},
"candidate_result": {}
},
"name": "cb-missing-passed-key",
"setup": [],
"stub": false
},
{
"expected": {
"promote": true,
"reason": "candidate green, baseline was already red \u2014 no regression introduced"
},
"input": {
"baseline_result": {
"passed": 0
},
"candidate_result": {
"passed": 1
}
},
"name": "cb-truthy-coercion",
"setup": [],
"stub": false
},
{
"expected": {
"promote": false,
"reason": "candidate gates red \u2014 regression blocks promotion"
},
"input": {
"baseline_result": {
"passed": true
},
"candidate_result": {
"passed": null
}
},
"name": "cb-explicit-null-passed",
"setup": [],
"stub": false
}
],
"monitor": [
{
"expected": {
"branch": "b",
"monitoring": true,
"rollback_ref": "abc123",
"watch": [
"gate suite green on next CI run",
"no new failure signatures in the next 24h"
]
},
"input": {
"proposal": {
"branch": "b",
"rollback_ref": "abc123"
}
},
"name": "mon-full",
"setup": [],
"stub": false
},
{
"expected": {
"branch": "",
"monitoring": true,
"rollback_ref": "",
"watch": [
"gate suite green on next CI run",
"no new failure signatures in the next 24h"
]
},
"input": {
"proposal": {}
},
"name": "mon-empty",
"setup": [],
"stub": false
},
{
"expected": {
"branch": "only-b",
"monitoring": true,
"rollback_ref": "",
"watch": [
"gate suite green on next CI run",
"no new failure signatures in the next 24h"
]
},
"input": {
"proposal": {
"branch": "only-b"
}
},
"name": "mon-partial",
"setup": [],
"stub": false
},
{
"expected": {
"branch": "b",
"monitoring": true,
"rollback_ref": "r",
"watch": [
"gate suite green on next CI run",
"no new failure signatures in the next 24h"
]
},
"input": {
"proposal": {
"branch": "b",
"extra": 1,
"rollback_ref": "r"
}
},
"name": "mon-extra-keys",
"setup": [],
"stub": false
},
{
"expected": {
"branch": 7,
"monitoring": true,
"rollback_ref": null,
"watch": [
"gate suite green on next CI run",
"no new failure signatures in the next 24h"
]
},
"input": {
"proposal": {
"branch": 7,
"rollback_ref": null
}
},
"name": "mon-nonstring-branch",
"setup": [],
"stub": false
}
],
"promote_or_discard": [
{
"expected": {
"branch": "discard-1",
"discarded": true,
"promoted": false,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1"
},
"input": {
"proposal": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"branch": "discard-1",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"repo": "@REPO@",
"verdict": {
"promote": false,
"reason": "test"
}
},
"name": "pod-discard",
"setup": [
"worktree:discard-1"
],
"stub": false
},
{
"expected": {
"branch": "podd-2",
"promoted": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1"
},
"input": {
"proposal": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"branch": "podd-2",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"repo": "@REPO@",
"verdict": {
"promote": true,
"reason": "test"
}
},
"name": "pod-promote-up-to-date",
"setup": [
"worktree:podd-2"
],
"stub": false
},
{
"expected": {
"branch": "podd-3",
"promoted": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1"
},
"input": {
"proposal": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"branch": "podd-3",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"repo": "@REPO@",
"verdict": {
"promote": true,
"reason": "test"
}
},
"name": "pod-promote-real-merge",
"setup": [
"worktree:podd-3",
"commit_in_worktree"
],
"stub": false
},
{
"expected": {
"error": "merge failed: error: Your local changes to the following files would be overwritten by merge:\n\tf.txt\nPlease commit your changes or stash them before you merge.\nAborting\nMerge with strategy ort failed.\n",
"promoted": false,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1"
},
"input": {
"proposal": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"branch": "podd-4",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"repo": "@REPO@",
"verdict": {
"promote": true,
"reason": "test"
}
},
"name": "pod-merge-fail",
"setup": [
"worktree:podd-4",
"commit_in_worktree",
"dirty_repo"
],
"stub": false
},
{
"expected": {
"branch": "ghost-branch",
"discarded": true,
"promoted": false,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1"
},
"input": {
"proposal": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"branch": "ghost-branch",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"repo": "@REPO@",
"verdict": {
"promote": false,
"reason": "test"
}
},
"name": "pod-discard-ghost",
"setup": [],
"stub": false
},
{
"expected": {
"raises": "KeyError: 'branch'"
},
"input": {
"proposal": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"repo": "@REPO@",
"verdict": {
"promote": true
}
},
"name": "pod-missing-branch-key",
"setup": [],
"stub": false
}
],
"propose_change": [
{
"expected": {
"at": "2026-10-11T12:00:00Z",
"base": "master",
"branch": "cand-ok",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"input": {
"base": "master",
"branch": "cand-ok",
"now": "2026-10-11T12:00:00Z",
"repo": "@REPO@"
},
"name": "propose-ok",
"setup": [],
"stub": false
},
{
"expected": {
"at": "2026-10-11T12:00:00Z",
"base": "HEAD",
"branch": "cand-head",
"proposed": true,
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"worktree": "@WORKTREE@"
},
"input": {
"base": "HEAD",
"branch": "cand-head",
"now": "2026-10-11T12:00:00Z",
"repo": "@REPO@"
},
"name": "propose-base-head",
"setup": [],
"stub": false
},
{
"expected": {
"error": "unknown base: no-such-ref",
"proposed": false
},
"input": {
"base": "no-such-ref",
"branch": "cand-bad",
"now": "2026-10-11T12:00:00Z",
"repo": "@REPO@"
},
"name": "propose-unknown-base",
"setup": [],
"stub": false
},
{
"expected": {
"error": "worktree failed: Preparing worktree (new branch 'dup')\nfatal: a branch named 'dup' already exists",
"proposed": false
},
"input": {
"base": "master",
"branch": "dup",
"now": "2026-10-11T12:00:00Z",
"repo": "@REPO@"
},
"name": "propose-dup-branch",
"setup": [
"worktree:dup"
],
"stub": false
},
{
"expected": {
"raises": "FileNotFoundError"
},
"input": {
"base": "master",
"branch": "cand-gone",
"now": "2026-10-11T12:00:00Z",
"repo": "/nonexistent-selfmod-truth/repo"
},
"name": "propose-missing-repo",
"setup": [],
"stub": false
}
],
"rollback": [
{
"expected": {
"ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1",
"rolled_back": true
},
"input": {
"repo": "@REPO@",
"rollback_ref": "16405a24f7fb0bab798f7c4cb4fb63eec407d6b1"
},
"name": "rb-ok-after-dirty",
"setup": [
"dirty_repo"
],
"stub": false
},
{
"expected": {
"ref": "master",
"rolled_back": true
},
"input": {
"repo": "@REPO@",
"rollback_ref": "master"
},
"name": "rb-ref-master",
"setup": [
"dirty_repo"
],
"stub": false
},
{
"expected": {
"error": "fatal: ambiguous argument 'no-such-ref': unknown revision or path not in the working tree.\nUse '--' to separate paths from revisions, like this:\n'git <command> [<revision>...] -- [<file>...]'\n",
"rolled_back": false
},
"input": {
"repo": "@REPO@",
"rollback_ref": "no-such-ref"
},
"name": "rb-bad-ref",
"setup": [],
"stub": false
},
{
"expected": {
"raises": "FileNotFoundError"
},
"input": {
"repo": "/nonexistent-selfmod-truth/repo",
"rollback_ref": "master"
},
"name": "rb-missing-repo",
"setup": [],
"stub": false
}
],
"run_gates": [
{
"expected": {
"output": "gates-green\n",
"passed": true
},
"input": {
"test_cmd": [
"echo",
"gates-green"
],
"worktree": "@REPO@"
},
"name": "gates-pass",
"setup": [],
"stub": false
},
{
"expected": {
"output": "boom\n",
"passed": false
},
"input": {
"test_cmd": [
"sh",
"-c",
"echo boom 1>&2; exit 1"
],
"worktree": "@REPO@"
},
"name": "gates-fail-stderr",
"setup": [],
"stub": false
},
{
"expected": {
"output": "2601\n2602\n2603\n2604\n2605\n2606\n2607\n2608\n2609\n2610\n2611\n2612\n2613\n2614\n2615\n2616\n2617\n2618\n2619\n2620\n2621\n2622\n2623\n2624\n2625\n2626\n2627\n2628\n2629\n2630\n2631\n2632\n2633\n2634\n2635\n2636\n2637\n2638\n2639\n2640\n2641\n2642\n2643\n2644\n2645\n2646\n2647\n2648\n2649\n2650\n2651\n2652\n2653\n2654\n2655\n2656\n2657\n2658\n2659\n2660\n2661\n2662\n2663\n2664\n2665\n2666\n2667\n2668\n2669\n2670\n2671\n2672\n2673\n2674\n2675\n2676\n2677\n2678\n2679\n2680\n2681\n2682\n2683\n2684\n2685\n2686\n2687\n2688\n2689\n2690\n2691\n2692\n2693\n2694\n2695\n2696\n2697\n2698\n2699\n2700\n2701\n2702\n2703\n2704\n2705\n2706\n2707\n2708\n2709\n2710\n2711\n2712\n2713\n2714\n2715\n2716\n2717\n2718\n2719\n2720\n2721\n2722\n2723\n2724\n2725\n2726\n2727\n2728\n2729\n2730\n2731\n2732\n2733\n2734\n2735\n2736\n2737\n2738\n2739\n2740\n2741\n2742\n2743\n2744\n2745\n2746\n2747\n2748\n2749\n2750\n2751\n2752\n2753\n2754\n2755\n2756\n2757\n2758\n2759\n2760\n2761\n2762\n2763\n2764\n2765\n2766\n2767\n2768\n2769\n2770\n2771\n2772\n2773\n2774\n2775\n2776\n2777\n2778\n2779\n2780\n2781\n2782\n2783\n2784\n2785\n2786\n2787\n2788\n2789\n2790\n2791\n2792\n2793\n2794\n2795\n2796\n2797\n2798\n2799\n2800\n2801\n2802\n2803\n2804\n2805\n2806\n2807\n2808\n2809\n2810\n2811\n2812\n2813\n2814\n2815\n2816\n2817\n2818\n2819\n2820\n2821\n2822\n2823\n2824\n2825\n2826\n2827\n2828\n2829\n2830\n2831\n2832\n2833\n2834\n2835\n2836\n2837\n2838\n2839\n2840\n2841\n2842\n2843\n2844\n2845\n2846\n2847\n2848\n2849\n2850\n2851\n2852\n2853\n2854\n2855\n2856\n2857\n2858\n2859\n2860\n2861\n2862\n2863\n2864\n2865\n2866\n2867\n2868\n2869\n2870\n2871\n2872\n2873\n2874\n2875\n2876\n2877\n2878\n2879\n2880\n2881\n2882\n2883\n2884\n2885\n2886\n2887\n2888\n2889\n2890\n2891\n2892\n2893\n2894\n2895\n2896\n2897\n2898\n2899\n2900\n2901\n2902\n2903\n2904\n2905\n2906\n2907\n2908\n2909\n2910\n2911\n2912\n2913\n2914\n2915\n2916\n2917\n2918\n2919\n2920\n2921\n2922\n2923\n2924\n2925\n2926\n2927\n2928\n2929\n2930\n2931\n2932\n2933\n2934\n2935\n2936\n2937\n2938\n2939\n2940\n2941\n2942\n2943\n2944\n2945\n2946\n2947\n2948\n2949\n2950\n2951\n2952\n2953\n2954\n2955\n2956\n2957\n2958\n2959\n2960\n2961\n2962\n2963\n2964\n2965\n2966\n2967\n2968\n2969\n2970\n2971\n2972\n2973\n2974\n2975\n2976\n2977\n2978\n2979\n2980\n2981\n2982\n2983\n2984\n2985\n2986\n2987\n2988\n2989\n2990\n2991\n2992\n2993\n2994\n2995\n2996\n2997\n2998\n2999\n3000\n",
"passed": true
},
"input": {
"test_cmd": [
"sh",
"-c",
"seq 1 3000"
],
"worktree": "@REPO@"
},
"name": "gates-tail-2000",
"setup": [],
"stub": false
},
{
"expected": {
"output": "o1\no2\ne1\n",
"passed": false
},
"input": {
"test_cmd": [
"sh",
"-c",
"echo o1; echo e1 1>&2; echo o2; exit 7"
],
"worktree": "@REPO@"
},
"name": "gates-out-then-err",
"setup": [],
"stub": false
},
{
"expected": {
"output": "stub-gates-green\n",
"passed": true
},
"input": {
"test_cmd": null,
"worktree": "@REPO@"
},
"name": "gates-default-cmd",
"setup": [],
"stub": true
}
]
},
"frozen": "2026-10-12T00:57:24.060736+00:00",
"note": "Frozen via scripts/gen_self_modify_truth.py against the live Python oracle; wall clock frozen at 2026-10-11T12:00:00Z; git fixtures use fixed identity+dates so the seed SHA is literal. @REPO@/@WORKTREE@/@STUBDIR@ are placeholders substituted at replay. Battery embeds this VERBATIM.",
"oracle": "src/rune/self_modify.py",
"pin": "RUNE_SMOD_CORE=python"
}
+550
View File
@@ -0,0 +1,550 @@
//! self_modify — reversible, benchmark-gated self-modification (W-sim).
//!
//! FULL parity port of src/rune/self_modify.py (dichotomy wave, #2479
//! tranche). The required loop, as code (oracle docstring, verbatim):
//!
//! ```text
//! candidate change → isolated branch/worktree → tests →
//! RuneBench subset → compare baseline → promote or discard →
//! monitor → rollback
//! ```
//!
//! No production state is modified without a recovery path: every proposal
//! carries its rollback ref (the pre-change HEAD), and promotion requires
//! the gate suite green + no regression vs the baseline run.
//!
//! Boundary: the subprocess seam (`_run` — every git invocation and gate
//! command) is OWNED by the kernel, exactly like the oracle's _run. The
//! wall clock never enters the kernel (no-clock law): `propose_change`
//! takes `now` as an argument and the facade stamps
//! time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()).
//!
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
//! {"ok": true, "value": ...} | {"ok": false, "err": "..."}:
//! * smod_propose_change({repo, branch, base, now}) → proposal dict
//! * smod_run_gates({worktree, test_cmd|null}) → {passed, output}
//! * smod_compare_baseline({candidate_result, baseline_result}) → verdict
//! * smod_promote_or_discard({repo, proposal, verdict}) → result dict
//! * smod_rollback({repo, rollback_ref}) → result dict
//! * smod_monitor({proposal}) → watch record
//!
//! Raise parity: the oracle RAISES where the kernel cannot return a dict
//! (missing cwd / binary → FileNotFoundError, gate timeout →
//! TimeoutExpired, missing proposal keys → KeyError, non-str argv →
//! TypeError). The kernel answers with the envelope's err half, encoded
//! so smod_loader re-raises the ORACLE'S exception TYPE — raise parity
//! holds at the type level, not just raise-vs-return.
//!
//! No smod_free_string export — sibling crates sharing the Python process
//! already export identical free symbols; callers free through
//! capability_scoring's cs_free_string.
use serde_json::{json, Value};
use std::fmt;
use std::io::Read;
use std::os::raw::c_char;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
pub const RUN_TIMEOUT_SECS: u64 = 300;
pub const GATES_TIMEOUT_SECS: u64 = 600;
const OUTPUT_TAIL_CHARS: usize = 2000;
const ERROR_HEAD_CHARS: usize = 200;
const DEFAULT_TEST_CMD: [&str; 6] = ["python3", "-m", "pytest", "tests/", "-q", "-x"];
const MKDTEMP_PREFIX: &str = "selfmod-";
const MKDTEMP_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789_";
const WATCH: [&str; 2] = [
"gate suite green on next CI run",
"no new failure signatures in the next 24h",
];
// ── error lattice ────────────────────────────────────────────────────────
pub enum FfiError {
/// subprocess spawn failed — the oracle's FileNotFoundError
/// (missing cwd, missing binary) propagates the same way.
Spawn(std::io::Error),
/// pipe read / wait failed (the oracle's decode errors propagate too).
Io(std::io::Error),
/// subprocess.run(timeout=…) — TimeoutExpired propagates in the oracle.
Timeout { cmd: String, secs: u64 },
/// dict[missing-key] — the oracle's KeyError.
KeyError(String),
/// a non-string reached the argv seam — the oracle's TypeError.
TypeError(&'static str),
/// tempfile.mkdtemp could not find a free name (OSError).
NoTempName,
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::Spawn(e) => write!(f, "[Errno {errno}] {e}", errno = e.raw_os_error().unwrap_or(0)),
FfiError::Io(e) => write!(f, "{e}"),
FfiError::Timeout { cmd, secs } => {
write!(f, "Command '{cmd}' timed out after {secs} seconds")
}
FfiError::KeyError(k) => write!(f, "KeyError: '{k}'"),
FfiError::TypeError(m) => write!(f, "TypeError: {m}"),
FfiError::NoTempName => write!(f, "OSError: no free temp name"),
FfiError::NullPtr => write!(f, "null argument pointer"),
FfiError::Utf8(e) => write!(f, "{e}"),
FfiError::Json(e) => write!(f, "{e}"),
}
}
}
// ── the subprocess seam (`_run`) ─────────────────────────────────────────
/// `subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
/// timeout=timeout)` → (returncode, stdout, stderr). text=True means
/// strict UTF-8 (a decode failure raises) and universal newlines.
fn _run(cmd: &[String], cwd: &str, timeout_secs: u64) -> Result<(i32, String, String), FfiError> {
let mut command = Command::new(&cmd[0]);
command
.args(&cmd[1..])
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().map_err(FfiError::Spawn)?;
let mut stdout_pipe = child.stdout.take().expect("piped stdout");
let mut stderr_pipe = child.stderr.take().expect("piped stderr");
let out_handle = thread::spawn(move || {
let mut buf = String::new();
stdout_pipe
.read_to_string(&mut buf)
.map(|_| buf)
.map_err(FfiError::Io)
});
let err_handle = thread::spawn(move || {
let mut buf = String::new();
stderr_pipe
.read_to_string(&mut buf)
.map(|_| buf)
.map_err(FfiError::Io)
});
let start = Instant::now();
let status = loop {
match child.try_wait().map_err(FfiError::Io)? {
Some(status) => break status,
None => {
if start.elapsed() >= Duration::from_secs(timeout_secs) {
let _ = child.kill();
let _ = child.wait();
return Err(FfiError::Timeout {
cmd: cmd.join(" "),
secs: timeout_secs,
});
}
thread::sleep(Duration::from_millis(10));
}
}
};
let out = join_reader(out_handle)?;
let err = join_reader(err_handle)?;
// text=True universal newlines: \r\n and lone \r both become \n.
let uni = |s: String| s.replace("\r\n", "\n").replace('\r', "\n");
Ok((status.code().unwrap_or(-1), uni(out), uni(err)))
}
fn join_reader(
handle: thread::JoinHandle<Result<String, FfiError>>,
) -> Result<String, FfiError> {
match handle.join() {
Ok(r) => r,
Err(_) => Err(FfiError::Io(std::io::Error::other(
"output reader thread panicked",
))),
}
}
// ── character-parity helpers (Python slices by characters, not bytes) ────
fn chars_head(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}
fn chars_tail(s: &str, n: usize) -> String {
let count = s.chars().count();
if count <= n {
s.to_string()
} else {
s.chars().skip(count - n).collect()
}
}
/// Python truthiness over a JSON value (`if not x.get("passed")`).
fn truthy(v: Option<&Value>) -> bool {
match v {
None | Some(Value::Null) => false,
Some(Value::Bool(b)) => *b,
Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
Some(Value::String(s)) => !s.is_empty(),
Some(Value::Array(a)) => !a.is_empty(),
Some(Value::Object(o)) => !o.is_empty(),
}
}
/// tempfile.mkdtemp(prefix="selfmod-"): a fresh 0700 dir under the
/// platform temp root with `prefix` + 8 random name characters
/// (Python's tempfile draws from lowercase letters, digits and `_`).
fn mkdtemp_selfmod() -> Result<String, FfiError> {
for _ in 0..100 {
let name = random_name();
let path = std::env::temp_dir().join(format!("{MKDTEMP_PREFIX}{name}"));
match std::fs::create_dir(&path) {
Ok(()) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(
&path,
std::fs::Permissions::from_mode(0o700),
);
}
return Ok(path.to_string_lossy().into_owned());
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(FfiError::Io(e)),
}
}
Err(FfiError::NoTempName)
}
fn random_name() -> String {
(0..8)
.map(|_| {
let byte = random_byte();
MKDTEMP_CHARS[byte as usize % MKDTEMP_CHARS.len()] as char
})
.collect()
}
fn random_byte() -> u8 {
use std::fs::File;
if let Ok(mut f) = File::open("/dev/urandom") {
let mut b = [0u8; 1];
if f.read_exact(&mut b).is_ok() {
return b[0];
}
}
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
(nanos % 256) as u8
}
// ── kernel functions (name-preserving) ───────────────────────────────────
/// `propose_change` — create the isolated worktree + branch. Returns the
/// proposal record (with rollback_ref = the base HEAD — the recovery path).
pub fn propose_change(
repo: &str,
branch: &str,
base: &str,
now: &str,
) -> Result<Value, FfiError> {
let rev_parse = vec![
"git".to_string(),
"rev-parse".to_string(),
base.to_string(),
];
let (rc, out, _err) = _run(&rev_parse, repo, RUN_TIMEOUT_SECS)?;
if rc != 0 {
return Ok(json!({"proposed": false, "error": format!("unknown base: {base}")}));
}
let base_sha = out.trim().to_string();
let wt = mkdtemp_selfmod()?;
let worktree_add = vec![
"git".to_string(),
"worktree".to_string(),
"add".to_string(),
wt.clone(),
"-b".to_string(),
branch.to_string(),
base.to_string(),
];
let (add_rc, _out, add_err) = _run(&worktree_add, repo, RUN_TIMEOUT_SECS)?;
if add_rc != 0 {
return Ok(json!({
"proposed": false,
"error": format!("worktree failed: {}", chars_head(add_err.trim(), 200)),
}));
}
Ok(json!({
"proposed": true,
"branch": branch,
"worktree": wt,
"base": base,
"rollback_ref": base_sha,
"at": now,
}))
}
/// `run_gates` — run the gate suite inside the worktree.
/// Returns {passed, output} where output is (stdout+stderr)[-2000:].
pub fn run_gates(worktree: &str, test_cmd: Option<&Vec<Value>>) -> Result<Value, FfiError> {
let cmd: Vec<String> = match test_cmd {
Some(list) if !list.is_empty() => list
.iter()
.map(|v| {
v.as_str()
.map(str::to_string)
.ok_or(FfiError::TypeError("test_cmd elements must be strings"))
})
.collect::<Result<Vec<String>, FfiError>>()?,
_ => DEFAULT_TEST_CMD.iter().map(|s| s.to_string()).collect(),
};
let (rc, out, err) = _run(&cmd, worktree, GATES_TIMEOUT_SECS)?;
let mut combined = out;
combined.push_str(&err);
Ok(json!({
"passed": rc == 0,
"output": chars_tail(&combined, OUTPUT_TAIL_CHARS),
}))
}
/// `compare_baseline` — candidate vs baseline gate runs. Regression blocks.
pub fn compare_baseline(candidate_result: &Value, baseline_result: &Value) -> Value {
if !truthy(candidate_result.get("passed")) {
return json!({
"promote": false,
"reason": "candidate gates red — regression blocks promotion",
});
}
if !truthy(baseline_result.get("passed")) {
return json!({
"promote": true,
"reason": "candidate green, baseline was already red — no regression introduced",
});
}
json!({
"promote": true,
"reason": "candidate green, baseline green — no regression",
})
}
/// dict[key] for keys that reach the argv seam: missing → KeyError
/// raise-parity; non-string → TypeError raise-parity (subprocess would
/// reject the non-str argv element).
fn argv_str<'a>(doc: &'a Value, key: &str) -> Result<&'a str, FfiError> {
match doc.get(key) {
None => Err(FfiError::KeyError(key.to_string())),
Some(Value::String(s)) => Ok(s.as_str()),
Some(_) => Err(FfiError::TypeError(
"expected str, bytes or os.PathLike object",
)),
}
}
/// dict[key] passed through verbatim (present None stays None); only a
/// MISSING key raises KeyError.
fn passthrough_field(doc: &Value, key: &str) -> Result<Value, FfiError> {
doc.get(key)
.cloned()
.ok_or_else(|| FfiError::KeyError(key.to_string()))
}
/// `promote_or_discard` — merge (promote) or delete (discard) the
/// proposal branch. rollback_ref is read lazily: in the oracle the
/// KeyError for a missing key fires only after the merge/remove side
/// effects have run.
pub fn promote_or_discard(repo: &str, proposal: &Value, verdict: &Value) -> Result<Value, FfiError> {
let branch = argv_str(proposal, "branch")?.to_string();
let wt = argv_str(proposal, "worktree")?.to_string();
let rollback_ref = || passthrough_field(proposal, "rollback_ref");
let remove_wt = vec![
"git".to_string(),
"worktree".to_string(),
"remove".to_string(),
wt,
"--force".to_string(),
];
if truthy(verdict.get("promote")) {
let msg = format!("self-modify: promote {branch}");
let merge = vec![
"git".to_string(),
"merge".to_string(),
branch.clone(),
"--no-ff".to_string(),
"-m".to_string(),
msg,
];
let (rc, _out, err) = _run(&merge, repo, RUN_TIMEOUT_SECS)?;
let _ = _run(&remove_wt, repo, RUN_TIMEOUT_SECS);
let rollback_ref = rollback_ref()?;
if rc != 0 {
return Ok(json!({
"promoted": false,
"error": format!("merge failed: {}", chars_head(&err, ERROR_HEAD_CHARS)),
"rollback_ref": rollback_ref,
}));
}
return Ok(json!({
"promoted": true,
"branch": branch,
"rollback_ref": rollback_ref,
}));
}
let _ = _run(&remove_wt, repo, RUN_TIMEOUT_SECS);
let drop_branch = vec![
"git".to_string(),
"branch".to_string(),
"-D".to_string(),
branch.clone(),
];
let _ = _run(&drop_branch, repo, RUN_TIMEOUT_SECS);
let rollback_ref = rollback_ref()?;
Ok(json!({
"promoted": false,
"discarded": true,
"branch": branch,
"rollback_ref": rollback_ref,
}))
}
/// `rollback` — restore the pre-change ref. The recovery path every
/// proposal carries.
pub fn rollback(repo: &str, rollback_ref: &str) -> Result<Value, FfiError> {
let reset = vec![
"git".to_string(),
"reset".to_string(),
"--hard".to_string(),
rollback_ref.to_string(),
];
let (rc, _out, err) = _run(&reset, repo, RUN_TIMEOUT_SECS)?;
if rc != 0 {
return Ok(json!({
"rolled_back": false,
"error": chars_head(&err, ERROR_HEAD_CHARS),
}));
}
Ok(json!({"rolled_back": true, "ref": rollback_ref}))
}
/// `monitor` — the post-promotion watch record. proposal.get(key, "")
/// passes ANY present value through verbatim (even a number or null);
/// only a MISSING key defaults to "".
pub fn monitor(proposal: &Value) -> Value {
let get_or_empty = |key: &str| {
proposal
.get(key)
.cloned()
.unwrap_or(Value::String(String::new()))
};
json!({
"monitoring": true,
"branch": get_or_empty("branch"),
"rollback_ref": get_or_empty("rollback_ref"),
"watch": WATCH,
})
}
// ── FFI (doc-envelope law) ───────────────────────────────────────────────
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
if ptr.is_null() {
return Err(FfiError::NullPtr);
}
let s = unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_str()
.map_err(FfiError::Utf8)?;
serde_json::from_str(s).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 required_str<'a>(a: &'a Value, key: &str) -> Result<&'a str, FfiError> {
a.get(key)
.and_then(Value::as_str)
.ok_or_else(|| FfiError::KeyError(key.to_string()))
}
/// smod_propose_change({repo, branch, base, now}) → proposal dict
#[no_mangle]
pub extern "C" fn smod_propose_change(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let base = a.get("base").and_then(Value::as_str).unwrap_or("master");
let now = required_str(&a, "now")?;
propose_change(
required_str(&a, "repo")?,
required_str(&a, "branch")?,
base,
now,
)
}))
}
/// smod_run_gates({worktree, test_cmd|null}) → {passed, output}
#[no_mangle]
pub extern "C" fn smod_run_gates(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let worktree = required_str(&a, "worktree")?;
let test_cmd = a.get("test_cmd").and_then(Value::as_array);
run_gates(worktree, test_cmd)
}))
}
/// smod_compare_baseline({candidate_result, baseline_result}) → verdict
#[no_mangle]
pub extern "C" fn smod_compare_baseline(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let empty = Value::Object(serde_json::Map::new());
compare_baseline(
a.get("candidate_result").unwrap_or(&empty),
a.get("baseline_result").unwrap_or(&empty),
)
}))
}
/// smod_promote_or_discard({repo, proposal, verdict}) → result dict
#[no_mangle]
pub extern "C" fn smod_promote_or_discard(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let repo = required_str(&a, "repo")?;
let proposal = a.get("proposal").ok_or_else(|| FfiError::KeyError("proposal".to_string()))?;
let verdict = a
.get("verdict")
.ok_or_else(|| FfiError::KeyError("verdict".to_string()))?;
promote_or_discard(repo, proposal, verdict)
}))
}
/// smod_rollback({repo, rollback_ref}) → result dict
#[no_mangle]
pub extern "C" fn smod_rollback(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
rollback(
required_str(&a, "repo")?,
required_str(&a, "rollback_ref")?,
)
}))
}
/// smod_monitor({proposal}) → watch record
#[no_mangle]
pub extern "C" fn smod_monitor(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let empty = Value::Object(serde_json::Map::new());
monitor(a.get("proposal").unwrap_or(&empty))
}))
}
#[cfg(test)]
mod branch_battery;