feat(state_checkpoint): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
36db392a94
commit
b278437a77
8 files changed
+2170
-17
No files matched your search
@@ -0,0 +1,568 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_state_checkpoint_truth.py — freeze/replay parity truth for
|
||||
state_checkpoint (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/state_checkpoint.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/state_checkpoint/src/frozen_sck_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_SCK_CORE=python BEFORE any import (a
|
||||
no-op pre-port, load-bearing post-port — the facade must never delegate
|
||||
into the kernel while truth is being frozen).
|
||||
|
||||
No-clock law: the wall clock is frozen by patching the `datetime` symbol
|
||||
in state_checkpoint, so every kernel payload carries its own `ts`/`at`
|
||||
and replay is deterministic regardless of run date.
|
||||
|
||||
Boundary law: the FILE IO (sqlite open/count, shutil copy, ledger
|
||||
load/save, env resolution in _store_db/_ledger_path) stays in the Python
|
||||
facade — the kernel is a pure function of the loaded ledger doc and
|
||||
facade-computed counts. The `snapshot_io`, `restore_io`, `diff_io` and
|
||||
`load_corrupt` truth groups are facade-only (no kernel symbol) and are
|
||||
skipped by the kernel-level replay pass.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "state_checkpoint", "src",
|
||||
"frozen_sck_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
os.environ["RUNE_SCK_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 state_checkpoint as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import sck_loader # noqa: E402 (kernel-level replay side)
|
||||
# replay law: the facade pass must exercise the LIVE Rust delegation
|
||||
# path, so the pin is lifted AFTER import (the facade reads the env
|
||||
# at call time). The kernel pass calls the loader directly.
|
||||
del os.environ["RUNE_SCK_CORE"]
|
||||
|
||||
FROZEN_TS = 1791806400 # 2026-10-12T12:00:00Z
|
||||
FROZEN_TS_STR = "20261012T120000"
|
||||
FROZEN_AT = "2026-10-12T12:00:00+00:00"
|
||||
|
||||
|
||||
class _FrozenDT:
|
||||
@staticmethod
|
||||
def now(tz=None):
|
||||
return datetime.fromtimestamp(FROZEN_TS, tz=timezone.utc)
|
||||
|
||||
|
||||
oracle_mod.datetime = _FrozenDT
|
||||
|
||||
|
||||
_DIR_SEQ = [0]
|
||||
|
||||
|
||||
def fresh_dir():
|
||||
"""Deterministic per-case dir — the frozen truth embeds absolute
|
||||
paths (snapshot records carry them), so replay must see the SAME
|
||||
paths the freeze saw."""
|
||||
_DIR_SEQ[0] += 1
|
||||
d = os.path.join(tempfile.gettempdir(), f"sck_truth_frozen_{_DIR_SEQ[0]:03d}")
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
os.makedirs(d)
|
||||
return d
|
||||
|
||||
|
||||
def make_db(path, rows):
|
||||
conn = sqlite3.connect(path)
|
||||
conn.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, status TEXT)")
|
||||
conn.executemany("INSERT INTO entries VALUES (?,?)",
|
||||
list(enumerate(rows)))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def ledger_of(path):
|
||||
return copy.deepcopy(oracle_mod._load_ledger(path))
|
||||
|
||||
|
||||
def rec_kernel(rec, name, inp, expected, oracle_ret=None):
|
||||
"""Record a kernel case; inputs deep-copied BEFORE oracle mutation."""
|
||||
rec(name, copy.deepcopy(inp), oracle_ret, expected)
|
||||
|
||||
|
||||
def rec_facade(rec, name, inp, expected, oracle_ret=None):
|
||||
rec(name, inp, oracle_ret, expected)
|
||||
|
||||
|
||||
# ── snapshot (kernel: record shaping + ledger append) ────────────────────────
|
||||
|
||||
def _snapshot_case(rec, name, label, rows, prior=None, db_text=False):
|
||||
d = fresh_dir()
|
||||
db = os.path.join(d, "store.db")
|
||||
if db_text:
|
||||
Path(db).write_text("not a database\n")
|
||||
else:
|
||||
make_db(db, rows)
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
if prior is not None:
|
||||
Path(ledger).write_text(json.dumps({"snapshots": prior}, indent=1))
|
||||
data_before = ledger_of(ledger)
|
||||
ret = oracle_mod.snapshot(label, store_db=db, ledger_path=ledger)
|
||||
inp = {"ledger_doc": data_before, "ts": FROZEN_TS_STR, "at": FROZEN_AT,
|
||||
"label": label, "entries": ret["entries"], "path": ret["path"]}
|
||||
rec_kernel(rec, name, inp,
|
||||
{"record": ret, "ledger": ledger_of(ledger)}, oracle_ret=ret)
|
||||
|
||||
|
||||
def c_snapshot_first(rec):
|
||||
_snapshot_case(rec, "snapshot-first-empty-ledger", "nightly",
|
||||
["active", "active", "retired"])
|
||||
|
||||
|
||||
def c_snapshot_second(rec):
|
||||
prior = [{"id": "20261012T110000-a", "label": "a",
|
||||
"path": "/tmp/snapshot-20261012T110000-a.db", "entries": 1,
|
||||
"at": "2026-10-12T11:00:00+00:00"}]
|
||||
_snapshot_case(rec, "snapshot-second-appends", "nightly",
|
||||
["active", "active"], prior=prior)
|
||||
|
||||
|
||||
def c_snapshot_entries_unknown(rec):
|
||||
_snapshot_case(rec, "snapshot-entries-minus-one-not-a-db", "weird",
|
||||
[], db_text=True)
|
||||
|
||||
|
||||
def c_snapshot_label_chars(rec):
|
||||
_snapshot_case(rec, "snapshot-label-dashes-and-dots", "pre-restore.v2",
|
||||
["active"])
|
||||
|
||||
|
||||
# ── list_snapshots (kernel: newest-first stable sort) ────────────────────────
|
||||
|
||||
def _list_case(rec, name, snaps):
|
||||
d = fresh_dir()
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
if snaps is not None:
|
||||
Path(ledger).write_text(json.dumps({"snapshots": snaps}, indent=1))
|
||||
out = oracle_mod.list_snapshots(ledger_path=ledger)
|
||||
inp = {"snapshots": copy.deepcopy(snaps or [])}
|
||||
rec_kernel(rec, name, inp, {"snapshots": out}, oracle_ret=out)
|
||||
|
||||
|
||||
def c_list_empty(rec):
|
||||
_list_case(rec, "list-missing-ledger-empty", None)
|
||||
|
||||
|
||||
def c_list_newest_first(rec):
|
||||
_list_case(rec, "list-newest-first", [
|
||||
{"id": "s1", "at": "2026-10-12T10:00:00+00:00"},
|
||||
{"id": "s2", "at": "2026-10-12T12:00:00+00:00"},
|
||||
{"id": "s3", "at": "2026-10-12T11:00:00+00:00"},
|
||||
])
|
||||
|
||||
|
||||
def c_list_tie_stable(rec):
|
||||
_list_case(rec, "list-tie-preserves-original-order", [
|
||||
{"id": "first", "at": "2026-10-12T12:00:00+00:00"},
|
||||
{"id": "second", "at": "2026-10-12T12:00:00+00:00"},
|
||||
{"id": "third", "at": "2026-10-12T13:00:00+00:00"},
|
||||
])
|
||||
|
||||
|
||||
def c_list_missing_at(rec):
|
||||
_list_case(rec, "list-missing-at-sorts-last", [
|
||||
{"id": "no-at"},
|
||||
{"id": "dated", "at": "2026-10-12T12:00:00+00:00"},
|
||||
])
|
||||
|
||||
|
||||
# ── restore decision (kernel) ────────────────────────────────────────────────
|
||||
|
||||
def _restore_case(rec, name, snapshot_id, dry_run, snaps, make_paths_real=False):
|
||||
d = fresh_dir()
|
||||
db = os.path.join(d, "store.db")
|
||||
make_db(db, ["active"])
|
||||
if make_paths_real:
|
||||
for s in snaps:
|
||||
make_db(s["path"], ["active", "active"])
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
Path(ledger).write_text(json.dumps({"snapshots": snaps}, indent=1))
|
||||
decision = oracle_mod.restore(snapshot_id, ledger_path=ledger,
|
||||
store_db=db, dry_run=dry_run)
|
||||
inp = {"snapshots": copy.deepcopy(snaps), "snapshot_id": snapshot_id,
|
||||
"dry_run": dry_run}
|
||||
if "snapshot" in decision:
|
||||
# proceed: the oracle's return carries facade IO results (safety
|
||||
# snapshot id); the KERNEL surface is the decision only
|
||||
expected = {"record": decision["snapshot"]}
|
||||
else:
|
||||
# error doc / dry-run doc ARE the kernel decision verbatim
|
||||
expected = decision
|
||||
rec_kernel(rec, name, inp, expected, oracle_ret=decision)
|
||||
|
||||
|
||||
def c_restore_unknown(rec):
|
||||
_restore_case(rec, "restore-unknown-error-doc", "nope", False,
|
||||
[{"id": "s1", "path": "/x.db"}])
|
||||
|
||||
|
||||
def c_restore_dry_run(rec):
|
||||
snap = {"id": "s1", "label": "a", "path": "/tmp/s1.db", "entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"}
|
||||
_restore_case(rec, "restore-dry-run-would-restore", "s1", True, [snap])
|
||||
|
||||
|
||||
def c_restore_found(rec):
|
||||
d = fresh_dir()
|
||||
snap = {"id": "s1", "label": "a", "path": os.path.join(d, "s1.db"),
|
||||
"entries": 2, "at": "2026-10-12T12:00:00+00:00"}
|
||||
_restore_case(rec, "restore-found-proceed-decision", "s1", False, [snap],
|
||||
make_paths_real=True)
|
||||
|
||||
|
||||
def c_restore_first_match(rec):
|
||||
# restore uses the FIRST match (match[0]) — unlike diff's last-wins
|
||||
snaps = [{"id": "s1", "path": "/first.db"},
|
||||
{"id": "s1", "path": "/second.db"}]
|
||||
_restore_case(rec, "restore-first-match-wins", "s1", True, snaps)
|
||||
|
||||
|
||||
# ── restore_commit (kernel: final doc shaping) ───────────────────────────────
|
||||
|
||||
def _commit_case(rec, name):
|
||||
d = fresh_dir()
|
||||
db = os.path.join(d, "store.db")
|
||||
make_db(db, ["active", "active"])
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
first = oracle_mod.snapshot("target", store_db=db, ledger_path=ledger)
|
||||
ret = oracle_mod.restore(first["id"], ledger_path=ledger, store_db=db)
|
||||
# the safety snapshot is the SECOND record in the ledger
|
||||
snaps = oracle_mod._load_ledger(ledger)["snapshots"]
|
||||
rec_kernel(rec, name, {"record": first, "safety_id": ret["safety_snapshot"]},
|
||||
{"restored": True, "snapshot": first,
|
||||
"safety_snapshot": ret["safety_snapshot"]},
|
||||
oracle_ret=ret)
|
||||
|
||||
|
||||
def c_commit_shape(rec):
|
||||
_commit_case(rec, "restore-commit-final-doc")
|
||||
|
||||
|
||||
# ── diff lookup (kernel: last-wins id map) ───────────────────────────────────
|
||||
|
||||
def _lookup_case(rec, name, snaps, before_id, after_id, dbs=None):
|
||||
d = fresh_dir()
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
snaps = [dict(s, path=os.path.join(d, s["path"])) for s in snaps]
|
||||
# the oracle diff OPENS every referenced snapshot db — back each path
|
||||
for s in snaps:
|
||||
make_db(s["path"], (dbs or {}).get(os.path.basename(s["path"]),
|
||||
["active"]))
|
||||
Path(ledger).write_text(json.dumps({"snapshots": snaps}, indent=1))
|
||||
out = oracle_mod.diff(before_id, after_id, ledger_path=ledger)
|
||||
inp = {"snapshots": copy.deepcopy(snaps), "before_id": before_id,
|
||||
"after_id": after_id}
|
||||
if "error" in out:
|
||||
rec_kernel(rec, name, inp, {"error": out["error"]}, oracle_ret=out)
|
||||
else:
|
||||
# per-side sqlite counts are facade IO; the lookup doc is the
|
||||
# kernel surface (by_id is built dict-comprehension style:
|
||||
# LAST duplicate wins)
|
||||
by_id = {}
|
||||
for s in snaps:
|
||||
by_id[s["id"]] = s
|
||||
rec_kernel(rec, name, inp,
|
||||
{"before_path": by_id[before_id]["path"],
|
||||
"after_path": by_id[after_id]["path"]}, oracle_ret=out)
|
||||
|
||||
|
||||
def c_lookup_both_known(rec):
|
||||
_lookup_case(rec, "lookup-both-known", [
|
||||
{"id": "a", "path": "a.db"}, {"id": "b", "path": "b.db"}], "a", "b")
|
||||
|
||||
|
||||
def c_lookup_before_unknown(rec):
|
||||
_lookup_case(rec, "lookup-before-unknown", [
|
||||
{"id": "b", "path": "b.db"}], "ghost", "b")
|
||||
|
||||
|
||||
def c_lookup_after_unknown(rec):
|
||||
_lookup_case(rec, "lookup-after-unknown", [
|
||||
{"id": "a", "path": "a.db"}], "a", "ghost")
|
||||
|
||||
|
||||
def c_lookup_dup_last_wins(rec):
|
||||
# diff's by_id map is built dict-comprehension style: LAST wins —
|
||||
# real dbs with distinct counts make the choice observable
|
||||
_lookup_case(rec, "lookup-duplicate-id-last-wins", [
|
||||
{"id": "a", "path": "first.db"},
|
||||
{"id": "a", "path": "second.db"},
|
||||
{"id": "b", "path": "b.db"}], "a", "b",
|
||||
dbs={"first.db": ["active"],
|
||||
"second.db": ["active"] * 5,
|
||||
"b.db": ["active"] * 2})
|
||||
|
||||
|
||||
# ── diff shape (kernel: delta + per-side error tolerance) ────────────────────
|
||||
|
||||
def _shape_case(rec, name, before, after):
|
||||
d = fresh_dir()
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
snaps = []
|
||||
for sid, side, dbrows in (("a", "before", before), ("b", "after", after)):
|
||||
db = os.path.join(d, f"{sid}.db")
|
||||
if dbrows is None:
|
||||
Path(db).write_text("not a database\n")
|
||||
else:
|
||||
make_db(db, dbrows)
|
||||
snaps.append({"id": sid, "path": db})
|
||||
Path(ledger).write_text(json.dumps({"snapshots": snaps}, indent=1))
|
||||
out = oracle_mod.diff("a", "b", ledger_path=ledger)
|
||||
inp = {"before": out["before"], "after": out["after"]}
|
||||
rec_kernel(rec, name, inp, out, oracle_ret=out)
|
||||
|
||||
|
||||
def c_shape_both_ok(rec):
|
||||
_shape_case(rec, "shape-both-ok-delta",
|
||||
["active", "active", "retired"],
|
||||
["active", "active", "active", "retired"])
|
||||
|
||||
|
||||
def c_shape_before_error(rec):
|
||||
_shape_case(rec, "shape-before-error-side", None,
|
||||
["active", "active"])
|
||||
|
||||
|
||||
def c_shape_after_error(rec):
|
||||
_shape_case(rec, "shape-after-error-side",
|
||||
["active", "active"], None)
|
||||
|
||||
|
||||
def c_shape_both_error(rec):
|
||||
_shape_case(rec, "shape-both-error-sides", None, None)
|
||||
|
||||
|
||||
def c_shape_zero_delta(rec):
|
||||
_shape_case(rec, "shape-zero-delta",
|
||||
["active", "retired"], ["active", "retired"])
|
||||
|
||||
|
||||
# ── facade-only: FILE IO (sqlite, copy, ledger save, env resolution) ────────
|
||||
|
||||
def c_snapshot_io(rec):
|
||||
d = fresh_dir()
|
||||
db = os.path.join(d, "store.db")
|
||||
make_db(db, ["active"] * 3)
|
||||
ret = oracle_mod.snapshot("io", store_db=db,
|
||||
ledger_path=os.path.join(d, "checkpoints.json"))
|
||||
rec_facade(rec, "snapshot-io-real-count-and-file", {"label": "io"},
|
||||
{"entries": ret["entries"], "path_exists":
|
||||
Path(ret["path"]).is_file(),
|
||||
"ledger_len": len(oracle_mod._load_ledger(
|
||||
os.path.join(d, "checkpoints.json"))["snapshots"])},
|
||||
oracle_ret={"entries": ret["entries"]})
|
||||
|
||||
|
||||
def c_restore_io(rec):
|
||||
d = fresh_dir()
|
||||
db = os.path.join(d, "store.db")
|
||||
make_db(db, ["active", "active"])
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
first = oracle_mod.snapshot("good", store_db=db, ledger_path=ledger)
|
||||
conn = sqlite3.connect(db)
|
||||
conn.execute("DELETE FROM entries")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
ret = oracle_mod.restore(first["id"], ledger_path=ledger, store_db=db)
|
||||
conn = sqlite3.connect(db)
|
||||
count = conn.execute("SELECT COUNT(*) FROM entries").fetchone()[0]
|
||||
conn.close()
|
||||
journal = os.path.join(d, "events.jsonl")
|
||||
Path(journal).write_text("event\n")
|
||||
before = Path(journal).read_text()
|
||||
oracle_mod.restore(first["id"], ledger_path=ledger, store_db=db)
|
||||
rec_facade(rec, "restore-io-db-repaired-journal-untouched",
|
||||
{"snapshot_id": first["id"]},
|
||||
{"restored": ret["restored"], "count_after": count,
|
||||
"journal_untouched": Path(journal).read_text() == before,
|
||||
"safety_in_ledger": ret["safety_snapshot"] in
|
||||
{s["id"] for s in oracle_mod._load_ledger(ledger)["snapshots"]}},
|
||||
oracle_ret=None)
|
||||
|
||||
|
||||
def c_diff_io(rec):
|
||||
d = fresh_dir()
|
||||
db = os.path.join(d, "store.db")
|
||||
make_db(db, ["active"])
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
a = oracle_mod.snapshot("a", store_db=db, ledger_path=ledger)
|
||||
conn = sqlite3.connect(db)
|
||||
conn.execute("INSERT INTO entries VALUES (99, 'active')")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
b = oracle_mod.snapshot("b", store_db=db, ledger_path=ledger)
|
||||
out = oracle_mod.diff(a["id"], b["id"], ledger_path=ledger)
|
||||
rec_facade(rec, "diff-io-real-sqlite-counts", {},
|
||||
{"before": out["before"], "after": out["after"],
|
||||
"delta_entries": out["delta_entries"]}, oracle_ret=None)
|
||||
|
||||
|
||||
def c_load_corrupt(rec):
|
||||
d = fresh_dir()
|
||||
ledger = os.path.join(d, "checkpoints.json")
|
||||
Path(ledger).write_text("{not json")
|
||||
rec_facade(rec, "load-corrupt-ledger-reads-empty", {},
|
||||
{"list": oracle_mod.list_snapshots(ledger_path=ledger),
|
||||
"db": oracle_mod._load_ledger(ledger)}, oracle_ret=None)
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
SNAPSHOT = [c_snapshot_first, c_snapshot_second, c_snapshot_entries_unknown,
|
||||
c_snapshot_label_chars]
|
||||
LIST = [c_list_empty, c_list_newest_first, c_list_tie_stable,
|
||||
c_list_missing_at]
|
||||
RESTORE = [c_restore_unknown, c_restore_dry_run, c_restore_found,
|
||||
c_restore_first_match]
|
||||
COMMIT = [c_commit_shape]
|
||||
LOOKUP = [c_lookup_both_known, c_lookup_before_unknown, c_lookup_after_unknown,
|
||||
c_lookup_dup_last_wins]
|
||||
SHAPE = [c_shape_both_ok, c_shape_before_error, c_shape_after_error,
|
||||
c_shape_both_error, c_shape_zero_delta]
|
||||
FACADE_IO = [c_snapshot_io, c_restore_io, c_diff_io, c_load_corrupt]
|
||||
|
||||
GROUPS = [("snapshot", SNAPSHOT), ("list", LIST), ("restore", RESTORE),
|
||||
("restore_commit", COMMIT), ("diff_lookup", LOOKUP),
|
||||
("diff_shape", SHAPE), ("facade_io", FACADE_IO)]
|
||||
|
||||
# kernel symbol per group (None → facade-only group, skipped kernel-side)
|
||||
KERNEL = {
|
||||
"snapshot": "sck_snapshot",
|
||||
"list": "sck_list",
|
||||
"restore": "sck_restore",
|
||||
"restore_commit": "sck_restore_commit",
|
||||
"diff_lookup": "sck_diff_lookup",
|
||||
"diff_shape": "sck_diff_shape",
|
||||
"facade_io": None,
|
||||
}
|
||||
|
||||
# loader wrapper + slice of `expected` the unwrapped FFI value maps to
|
||||
CALLS = {
|
||||
"snapshot": (lambda w: w,
|
||||
lambda i, ld: ld.snapshot(ledger_doc=i["ledger_doc"], ts=i["ts"],
|
||||
at=i["at"], label=i["label"],
|
||||
entries=i["entries"],
|
||||
snap_path=i["path"])),
|
||||
"list": (lambda w: w["snapshots"],
|
||||
lambda i, ld: ld.list(snapshots=i["snapshots"])),
|
||||
"restore": (lambda w: w,
|
||||
lambda i, ld: ld.restore(snapshots=i["snapshots"],
|
||||
snapshot_id=i["snapshot_id"],
|
||||
dry_run=i["dry_run"])),
|
||||
"restore_commit": (lambda w: w,
|
||||
lambda i, ld: ld.restore_commit(record=i["record"],
|
||||
safety_id=i["safety_id"])),
|
||||
"diff_lookup": (lambda w: ({"error": w["error"]} if "error" in w else
|
||||
{"before_path": w["before_path"],
|
||||
"after_path": w["after_path"]}),
|
||||
lambda i, ld: ld.diff_lookup(snapshots=i["snapshots"],
|
||||
before_id=i["before_id"],
|
||||
after_id=i["after_id"])),
|
||||
"diff_shape": (lambda w: w,
|
||||
lambda i, ld: ld.diff_shape(before=i["before"],
|
||||
after=i["after"])),
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization: a sorted dump makes the comparison
|
||||
key-order independent (int keys never occur here)."""
|
||||
return json.dumps(json.loads(json.dumps(x)), sort_keys=True)
|
||||
|
||||
|
||||
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, inp, oracle_ret, expected, _r=recs):
|
||||
entry = {"name": name, "input": inp, "return": oracle_ret,
|
||||
"expected": expected}
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[: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
|
||||
checked = 0
|
||||
for group, cases in frozen.items():
|
||||
spec = CALLS.get(group)
|
||||
if spec is None:
|
||||
continue
|
||||
unwrap, call = spec
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = call(copy.deepcopy(case["input"]), sck_loader)
|
||||
want = unwrap(case["expected"])
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {checked - failures}/{checked}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": FROZEN_AT,
|
||||
"oracle": "src/rune/state_checkpoint.py",
|
||||
"pin": "RUNE_SCK_CORE=python",
|
||||
"note": "Frozen via scripts/gen_state_checkpoint_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_AT +
|
||||
" (_FrozenDT patched into state_checkpoint.datetime; epoch " +
|
||||
str(FROZEN_TS) + "). Battery embeds this VERBATIM. The "
|
||||
"`facade_io` group is facade-only (sqlite/copy/ledger IO stays "
|
||||
"in Python).",
|
||||
"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)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the FULL Rust state_checkpoint core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* sck_snapshot({ledger_doc, ts, at, label, entries, snap_path})
|
||||
-> {record, ledger}
|
||||
* sck_list({snapshots}) -> {snapshots}
|
||||
* sck_restore({snapshots, snapshot_id, dry_run}) -> decision doc
|
||||
* sck_restore_commit({record, safety_id}) -> final doc
|
||||
* sck_diff_lookup({snapshots, before_id, after_id})
|
||||
-> {error} | {before_path, after_path}
|
||||
* sck_diff_shape({before, after}) -> {before, after, delta_entries}
|
||||
|
||||
No-clock law: `ts` (%Y%m%dT%H%M%S) and `at` (ISO-8601) are always
|
||||
arguments — the kernel never reads the wall clock.
|
||||
|
||||
Boundary: the FILE IO (sqlite open/count, shutil copy2, ledger
|
||||
load/save, env resolution in _store_db/_ledger_path) stays in the Python
|
||||
facade (state_checkpoint.py); the kernel is a pure function of the
|
||||
loaded ledger doc and facade-computed counts.
|
||||
|
||||
Exception law: the kernel's `err` string carries the oracle's Python
|
||||
exception kind ("KeyError: <key>") and this loader re-raises the SAME
|
||||
exception type the oracle raised.
|
||||
|
||||
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 SCK_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" / "state_checkpoint"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"SCK_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" / "libstate_checkpoint.so",
|
||||
)
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"sck_snapshot",
|
||||
"sck_list",
|
||||
"sck_restore",
|
||||
"sck_restore_commit",
|
||||
"sck_diff_lookup",
|
||||
"sck_diff_shape",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class StateCheckpointCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust state_checkpoint 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 StateCheckpointCoreUnavailable(
|
||||
f"Rust state_checkpoint 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"):
|
||||
err = doc.get("err", f"{sym} failed")
|
||||
if err.startswith("ValueError: "):
|
||||
raise ValueError(err[len("ValueError: "):])
|
||||
if err.startswith("KeyError: "):
|
||||
raise KeyError(err[len("KeyError: "):])
|
||||
raise RuntimeError(err)
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def snapshot(ledger_doc: dict, *, ts: str, at: str, label: str,
|
||||
entries: int, snap_path: str) -> dict:
|
||||
return _call("sck_snapshot", {"ledger_doc": ledger_doc, "ts": ts,
|
||||
"at": at, "label": label,
|
||||
"entries": entries,
|
||||
"snap_path": snap_path})
|
||||
|
||||
|
||||
def list(snapshots: list) -> list:
|
||||
return _call("sck_list", {"snapshots": snapshots})["snapshots"]
|
||||
|
||||
|
||||
def restore(snapshots: list, *, snapshot_id: str, dry_run: bool) -> dict:
|
||||
return _call("sck_restore", {"snapshots": snapshots,
|
||||
"snapshot_id": snapshot_id,
|
||||
"dry_run": dry_run})
|
||||
|
||||
|
||||
def restore_commit(record: dict, *, safety_id: str) -> dict:
|
||||
return _call("sck_restore_commit", {"record": record,
|
||||
"safety_id": safety_id})
|
||||
|
||||
|
||||
def diff_lookup(snapshots: list, *, before_id: str, after_id: str) -> dict:
|
||||
return _call("sck_diff_lookup", {"snapshots": snapshots,
|
||||
"before_id": before_id,
|
||||
"after_id": after_id})
|
||||
|
||||
|
||||
def diff_shape(before: dict, after: dict) -> dict:
|
||||
return _call("sck_diff_shape", {"before": before, "after": after})
|
||||
@@ -12,6 +12,15 @@ be restored WITHOUT deleting raw historical evidence:
|
||||
the causal event history (journal) is NEVER touched — restore changes
|
||||
state, not evidence
|
||||
- diff(before_id, after_id): entry-count + status-shape comparison
|
||||
|
||||
Dichotomy wave #2479: the pure decision surface (snapshot record
|
||||
shaping + ledger append, newest-first listing, restore decision,
|
||||
restore final doc, diff lookup/shape) lives in the Rust kernel
|
||||
(src/rust/state_checkpoint) behind doc-envelope FFI; the FILE IO
|
||||
(sqlite open/count, shutil copy2, ledger load/save, env resolution)
|
||||
stays here — the kernel is a pure function of the loaded ledger doc
|
||||
and facade-computed counts. RUNE_SCK_CORE=python pins the Python
|
||||
fallback (parity harnesses).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -23,6 +32,22 @@ import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def _sck_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_SCK_CORE=python pins
|
||||
the Python fallback — parity harnesses). The kernel owns the
|
||||
decision surface; the FILE IO stays in this facade."""
|
||||
if os.environ.get("RUNE_SCK_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import sck_loader
|
||||
|
||||
if sck_loader.available():
|
||||
return sck_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _store_db():
|
||||
try:
|
||||
import memory_env
|
||||
@@ -65,11 +90,17 @@ def snapshot(label: str, store_db=None, ledger_path=None) -> dict:
|
||||
count = -1
|
||||
finally:
|
||||
conn.close()
|
||||
rec = {"id": snap_id, "label": label, "path": snap_path,
|
||||
"entries": count,
|
||||
"at": datetime.now(timezone.utc).isoformat()}
|
||||
data = _load_ledger(ledger)
|
||||
data["snapshots"].append(rec)
|
||||
at = datetime.now(timezone.utc).isoformat()
|
||||
core = _sck_core()
|
||||
if core is not None:
|
||||
out = core.snapshot(_load_ledger(ledger), ts=ts, at=at, label=label,
|
||||
entries=count, snap_path=snap_path)
|
||||
rec, data = out["record"], out["ledger"]
|
||||
else:
|
||||
rec = {"id": snap_id, "label": label, "path": snap_path,
|
||||
"entries": count, "at": at}
|
||||
data = _load_ledger(ledger)
|
||||
data["snapshots"].append(rec)
|
||||
_save_ledger(ledger, data)
|
||||
return rec
|
||||
|
||||
@@ -78,6 +109,9 @@ def list_snapshots(ledger_path=None, store_db=None) -> list:
|
||||
"""The ledger, newest first."""
|
||||
ledger = ledger_path or _ledger_path(store_db)
|
||||
snaps = _load_ledger(ledger).get("snapshots", [])
|
||||
core = _sck_core()
|
||||
if core is not None:
|
||||
return core.list(snapshots=snaps)
|
||||
return sorted(snaps, key=lambda s: s.get("at", ""), reverse=True)
|
||||
|
||||
|
||||
@@ -87,16 +121,29 @@ def restore(snapshot_id: str, ledger_path=None, store_db=None,
|
||||
first (rollback of the rollback). The journal is never touched."""
|
||||
ledger = ledger_path or _ledger_path(store_db)
|
||||
data = _load_ledger(ledger)
|
||||
match = [s for s in data.get("snapshots", []) if s["id"] == snapshot_id]
|
||||
if not match:
|
||||
return {"restored": False, "error": f"unknown snapshot: {snapshot_id}"}
|
||||
rec = match[0]
|
||||
core = _sck_core()
|
||||
if core is not None:
|
||||
decision = core.restore(snapshots=data.get("snapshots", []),
|
||||
snapshot_id=snapshot_id, dry_run=dry_run)
|
||||
else:
|
||||
match = [s for s in data.get("snapshots", [])
|
||||
if s["id"] == snapshot_id]
|
||||
if not match:
|
||||
return {"restored": False,
|
||||
"error": f"unknown snapshot: {snapshot_id}"}
|
||||
rec = match[0]
|
||||
decision = ({"restored": False, "dry_run": True,
|
||||
"would_restore": rec}
|
||||
if dry_run else {"record": rec})
|
||||
if "record" not in decision:
|
||||
return decision
|
||||
rec = decision["record"]
|
||||
db = store_db or _store_db()
|
||||
if dry_run:
|
||||
return {"restored": False, "dry_run": True, "would_restore": rec}
|
||||
# safety: snapshot the live state first
|
||||
# safety: snapshot the live state first (rollback of the rollback)
|
||||
safety = snapshot("pre-restore", store_db=db, ledger_path=ledger)
|
||||
shutil.copy2(rec["path"], db)
|
||||
if core is not None:
|
||||
return core.restore_commit(record=rec, safety_id=safety["id"])
|
||||
return {"restored": True, "snapshot": rec, "safety_snapshot": safety["id"]}
|
||||
|
||||
|
||||
@@ -104,12 +151,22 @@ def diff(before_id: str, after_id: str, ledger_path=None) -> dict:
|
||||
"""Entry-count + status-shape comparison between two snapshots."""
|
||||
ledger = ledger_path or _ledger_path()
|
||||
data = _load_ledger(ledger)
|
||||
by_id = {s["id"]: s for s in data.get("snapshots", [])}
|
||||
if before_id not in by_id or after_id not in by_id:
|
||||
return {"error": "unknown snapshot id"}
|
||||
core = _sck_core()
|
||||
if core is not None:
|
||||
lookup = core.diff_lookup(snapshots=data.get("snapshots", []),
|
||||
before_id=before_id, after_id=after_id)
|
||||
if "error" in lookup:
|
||||
return lookup
|
||||
else:
|
||||
by_id = {s["id"]: s for s in data.get("snapshots", [])}
|
||||
if before_id not in by_id or after_id not in by_id:
|
||||
return {"error": "unknown snapshot id"}
|
||||
lookup = {"before_path": by_id[before_id]["path"],
|
||||
"after_path": by_id[after_id]["path"]}
|
||||
out = {}
|
||||
for label, sid in (("before", before_id), ("after", after_id)):
|
||||
conn = sqlite3.connect(by_id[sid]["path"])
|
||||
for label, path in (("before", lookup["before_path"]),
|
||||
("after", lookup["after_path"])):
|
||||
conn = sqlite3.connect(path)
|
||||
try:
|
||||
out[label] = {
|
||||
"entries": conn.execute("SELECT COUNT(*) FROM entries").fetchone()[0],
|
||||
@@ -119,5 +176,7 @@ def diff(before_id: str, after_id: str, ledger_path=None) -> dict:
|
||||
out[label] = {"error": str(e)}
|
||||
finally:
|
||||
conn.close()
|
||||
if core is not None:
|
||||
return core.diff_shape(before=out["before"], after=out["after"])
|
||||
out["delta_entries"] = out["after"].get("entries", 0) - out["before"].get("entries", 0)
|
||||
return out
|
||||
Generated
+135
@@ -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 = "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 = "state_checkpoint"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "state_checkpoint"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# preserve_order: Python-dict insertion-order semantics for the snapshot
|
||||
# record ({id, label, path, entries, at}) and the pass-through ledger
|
||||
# doc; float_roundtrip: correctly-rounded float parsing (entry counts
|
||||
# ride along verbatim).
|
||||
serde_json = { version = "1", features = ["preserve_order", "float_roundtrip"] }
|
||||
@@ -0,0 +1,101 @@
|
||||
//! state_checkpoint branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_sck_truth.json: 26 cases across every public function
|
||||
//! and every branch — snapshot record shaping (fresh ledger, append,
|
||||
//! entries −1 not-a-db, dashed/dotted label), newest-first listing
|
||||
//! (empty, out-of-order, TIE stability, missing-at sorts last), restore
|
||||
//! decision (unknown error doc, dry-run would_restore, proceed record,
|
||||
//! FIRST-match-wins), restore_commit final-doc shaping, diff lookup
|
||||
//! (both known, either side unknown, duplicate-id LAST-wins with real
|
||||
//! distinct dbs), diff shape (both ok delta, per-side error tolerance,
|
||||
//! zero delta)).
|
||||
//! Frozen 2026-10-12 via scripts/gen_state_checkpoint_truth.py with
|
||||
//! RUNE_SCK_CORE=python pinned (poisoned-truth law) and the wall clock
|
||||
//! frozen at 2026-10-12T12:00:00+00:00 (epoch 1791806400). Embedded
|
||||
//! VERBATIM — never transcribed. The `facade_io` group is facade-only
|
||||
//! (sqlite/copy/ledger IO stays in Python) and is not replayed here.
|
||||
|
||||
use crate::{diff_lookup, diff_shape, list, restore, restore_commit, snapshot};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_sck_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 ledger_of(i: &Value) -> Map<String, Value> {
|
||||
i["ledger_doc"].as_object().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn snaps_of(i: &Value) -> Vec<Value> {
|
||||
i["snapshots"].as_array().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
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 snapshot_parity_battery() {
|
||||
replay("snapshot", |i| {
|
||||
let (record, ledger) = snapshot(
|
||||
&ledger_of(i),
|
||||
i["ts"].as_str().unwrap(),
|
||||
i["at"].as_str().unwrap(),
|
||||
i["label"].as_str().unwrap(),
|
||||
i["entries"].as_i64().unwrap(),
|
||||
i["path"].as_str().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
json!({"record": record, "ledger": ledger})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_parity_battery() {
|
||||
replay("list", |i| json!({"snapshots": list(&snaps_of(i))}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_parity_battery() {
|
||||
replay("restore", |i| {
|
||||
restore(
|
||||
&snaps_of(i),
|
||||
i["snapshot_id"].as_str().unwrap(),
|
||||
i["dry_run"].as_bool().unwrap(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_commit_parity_battery() {
|
||||
replay("restore_commit", |i| {
|
||||
restore_commit(&i["record"], i["safety_id"].as_str().unwrap())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_lookup_parity_battery() {
|
||||
replay("diff_lookup", |i| {
|
||||
diff_lookup(
|
||||
&snaps_of(i),
|
||||
i["before_id"].as_str().unwrap(),
|
||||
i["after_id"].as_str().unwrap(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_shape_parity_battery() {
|
||||
replay("diff_shape", |i| diff_shape(&i["before"], &i["after"]));
|
||||
}
|
||||
@@ -0,0 +1,825 @@
|
||||
{
|
||||
"frozen": "2026-10-12T12:00:00+00:00",
|
||||
"oracle": "src/rune/state_checkpoint.py",
|
||||
"pin": "RUNE_SCK_CORE=python",
|
||||
"note": "Frozen via scripts/gen_state_checkpoint_truth.py against the live Python oracle; wall clock frozen at 2026-10-12T12:00:00+00:00 (_FrozenDT patched into state_checkpoint.datetime; epoch 1791806400). Battery embeds this VERBATIM. The `facade_io` group is facade-only (sqlite/copy/ledger IO stays in Python).",
|
||||
"cases": {
|
||||
"snapshot": [
|
||||
{
|
||||
"name": "snapshot-first-empty-ledger",
|
||||
"input": {
|
||||
"ledger_doc": {
|
||||
"snapshots": []
|
||||
},
|
||||
"ts": "20261012T120000",
|
||||
"at": "2026-10-12T12:00:00+00:00",
|
||||
"label": "nightly",
|
||||
"entries": 3,
|
||||
"path": "/tmp/sck_truth_frozen_001/snapshot-20261012T120000-nightly.db"
|
||||
},
|
||||
"return": {
|
||||
"id": "20261012T120000-nightly",
|
||||
"label": "nightly",
|
||||
"path": "/tmp/sck_truth_frozen_001/snapshot-20261012T120000-nightly.db",
|
||||
"entries": 3,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"expected": {
|
||||
"record": {
|
||||
"id": "20261012T120000-nightly",
|
||||
"label": "nightly",
|
||||
"path": "/tmp/sck_truth_frozen_001/snapshot-20261012T120000-nightly.db",
|
||||
"entries": 3,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"ledger": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "20261012T120000-nightly",
|
||||
"label": "nightly",
|
||||
"path": "/tmp/sck_truth_frozen_001/snapshot-20261012T120000-nightly.db",
|
||||
"entries": 3,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snapshot-second-appends",
|
||||
"input": {
|
||||
"ledger_doc": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "20261012T110000-a",
|
||||
"label": "a",
|
||||
"path": "/tmp/snapshot-20261012T110000-a.db",
|
||||
"entries": 1,
|
||||
"at": "2026-10-12T11:00:00+00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ts": "20261012T120000",
|
||||
"at": "2026-10-12T12:00:00+00:00",
|
||||
"label": "nightly",
|
||||
"entries": 2,
|
||||
"path": "/tmp/sck_truth_frozen_002/snapshot-20261012T120000-nightly.db"
|
||||
},
|
||||
"return": {
|
||||
"id": "20261012T120000-nightly",
|
||||
"label": "nightly",
|
||||
"path": "/tmp/sck_truth_frozen_002/snapshot-20261012T120000-nightly.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"expected": {
|
||||
"record": {
|
||||
"id": "20261012T120000-nightly",
|
||||
"label": "nightly",
|
||||
"path": "/tmp/sck_truth_frozen_002/snapshot-20261012T120000-nightly.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"ledger": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "20261012T110000-a",
|
||||
"label": "a",
|
||||
"path": "/tmp/snapshot-20261012T110000-a.db",
|
||||
"entries": 1,
|
||||
"at": "2026-10-12T11:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "20261012T120000-nightly",
|
||||
"label": "nightly",
|
||||
"path": "/tmp/sck_truth_frozen_002/snapshot-20261012T120000-nightly.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snapshot-entries-minus-one-not-a-db",
|
||||
"input": {
|
||||
"ledger_doc": {
|
||||
"snapshots": []
|
||||
},
|
||||
"ts": "20261012T120000",
|
||||
"at": "2026-10-12T12:00:00+00:00",
|
||||
"label": "weird",
|
||||
"entries": -1,
|
||||
"path": "/tmp/sck_truth_frozen_003/snapshot-20261012T120000-weird.db"
|
||||
},
|
||||
"return": {
|
||||
"id": "20261012T120000-weird",
|
||||
"label": "weird",
|
||||
"path": "/tmp/sck_truth_frozen_003/snapshot-20261012T120000-weird.db",
|
||||
"entries": -1,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"expected": {
|
||||
"record": {
|
||||
"id": "20261012T120000-weird",
|
||||
"label": "weird",
|
||||
"path": "/tmp/sck_truth_frozen_003/snapshot-20261012T120000-weird.db",
|
||||
"entries": -1,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"ledger": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "20261012T120000-weird",
|
||||
"label": "weird",
|
||||
"path": "/tmp/sck_truth_frozen_003/snapshot-20261012T120000-weird.db",
|
||||
"entries": -1,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snapshot-label-dashes-and-dots",
|
||||
"input": {
|
||||
"ledger_doc": {
|
||||
"snapshots": []
|
||||
},
|
||||
"ts": "20261012T120000",
|
||||
"at": "2026-10-12T12:00:00+00:00",
|
||||
"label": "pre-restore.v2",
|
||||
"entries": 1,
|
||||
"path": "/tmp/sck_truth_frozen_004/snapshot-20261012T120000-pre-restore.v2.db"
|
||||
},
|
||||
"return": {
|
||||
"id": "20261012T120000-pre-restore.v2",
|
||||
"label": "pre-restore.v2",
|
||||
"path": "/tmp/sck_truth_frozen_004/snapshot-20261012T120000-pre-restore.v2.db",
|
||||
"entries": 1,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"expected": {
|
||||
"record": {
|
||||
"id": "20261012T120000-pre-restore.v2",
|
||||
"label": "pre-restore.v2",
|
||||
"path": "/tmp/sck_truth_frozen_004/snapshot-20261012T120000-pre-restore.v2.db",
|
||||
"entries": 1,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"ledger": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "20261012T120000-pre-restore.v2",
|
||||
"label": "pre-restore.v2",
|
||||
"path": "/tmp/sck_truth_frozen_004/snapshot-20261012T120000-pre-restore.v2.db",
|
||||
"entries": 1,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"list": [
|
||||
{
|
||||
"name": "list-missing-ledger-empty",
|
||||
"input": {
|
||||
"snapshots": []
|
||||
},
|
||||
"return": [],
|
||||
"expected": {
|
||||
"snapshots": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list-newest-first",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "s1",
|
||||
"at": "2026-10-12T10:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "s2",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "s3",
|
||||
"at": "2026-10-12T11:00:00+00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": [
|
||||
{
|
||||
"id": "s2",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "s3",
|
||||
"at": "2026-10-12T11:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "s1",
|
||||
"at": "2026-10-12T10:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "s2",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "s3",
|
||||
"at": "2026-10-12T11:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "s1",
|
||||
"at": "2026-10-12T10:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list-tie-preserves-original-order",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "first",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "second",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "third",
|
||||
"at": "2026-10-12T13:00:00+00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": [
|
||||
{
|
||||
"id": "third",
|
||||
"at": "2026-10-12T13:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "first",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "second",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "third",
|
||||
"at": "2026-10-12T13:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "first",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "second",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list-missing-at-sorts-last",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "no-at"
|
||||
},
|
||||
{
|
||||
"id": "dated",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": [
|
||||
{
|
||||
"id": "dated",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "no-at"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "dated",
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"id": "no-at"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"restore": [
|
||||
{
|
||||
"name": "restore-unknown-error-doc",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "s1",
|
||||
"path": "/x.db"
|
||||
}
|
||||
],
|
||||
"snapshot_id": "nope",
|
||||
"dry_run": false
|
||||
},
|
||||
"return": {
|
||||
"restored": false,
|
||||
"error": "unknown snapshot: nope"
|
||||
},
|
||||
"expected": {
|
||||
"restored": false,
|
||||
"error": "unknown snapshot: nope"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "restore-dry-run-would-restore",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "s1",
|
||||
"label": "a",
|
||||
"path": "/tmp/s1.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"snapshot_id": "s1",
|
||||
"dry_run": true
|
||||
},
|
||||
"return": {
|
||||
"restored": false,
|
||||
"dry_run": true,
|
||||
"would_restore": {
|
||||
"id": "s1",
|
||||
"label": "a",
|
||||
"path": "/tmp/s1.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"restored": false,
|
||||
"dry_run": true,
|
||||
"would_restore": {
|
||||
"id": "s1",
|
||||
"label": "a",
|
||||
"path": "/tmp/s1.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "restore-found-proceed-decision",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "s1",
|
||||
"label": "a",
|
||||
"path": "/tmp/sck_truth_frozen_011/s1.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"snapshot_id": "s1",
|
||||
"dry_run": false
|
||||
},
|
||||
"return": {
|
||||
"restored": true,
|
||||
"snapshot": {
|
||||
"id": "s1",
|
||||
"label": "a",
|
||||
"path": "/tmp/sck_truth_frozen_011/s1.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"safety_snapshot": "20261012T120000-pre-restore"
|
||||
},
|
||||
"expected": {
|
||||
"record": {
|
||||
"id": "s1",
|
||||
"label": "a",
|
||||
"path": "/tmp/sck_truth_frozen_011/s1.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "restore-first-match-wins",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "s1",
|
||||
"path": "/first.db"
|
||||
},
|
||||
{
|
||||
"id": "s1",
|
||||
"path": "/second.db"
|
||||
}
|
||||
],
|
||||
"snapshot_id": "s1",
|
||||
"dry_run": true
|
||||
},
|
||||
"return": {
|
||||
"restored": false,
|
||||
"dry_run": true,
|
||||
"would_restore": {
|
||||
"id": "s1",
|
||||
"path": "/first.db"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"restored": false,
|
||||
"dry_run": true,
|
||||
"would_restore": {
|
||||
"id": "s1",
|
||||
"path": "/first.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"restore_commit": [
|
||||
{
|
||||
"name": "restore-commit-final-doc",
|
||||
"input": {
|
||||
"record": {
|
||||
"id": "20261012T120000-target",
|
||||
"label": "target",
|
||||
"path": "/tmp/sck_truth_frozen_014/snapshot-20261012T120000-target.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"safety_id": "20261012T120000-pre-restore"
|
||||
},
|
||||
"return": {
|
||||
"restored": true,
|
||||
"snapshot": {
|
||||
"id": "20261012T120000-target",
|
||||
"label": "target",
|
||||
"path": "/tmp/sck_truth_frozen_014/snapshot-20261012T120000-target.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"safety_snapshot": "20261012T120000-pre-restore"
|
||||
},
|
||||
"expected": {
|
||||
"restored": true,
|
||||
"snapshot": {
|
||||
"id": "20261012T120000-target",
|
||||
"label": "target",
|
||||
"path": "/tmp/sck_truth_frozen_014/snapshot-20261012T120000-target.db",
|
||||
"entries": 2,
|
||||
"at": "2026-10-12T12:00:00+00:00"
|
||||
},
|
||||
"safety_snapshot": "20261012T120000-pre-restore"
|
||||
}
|
||||
}
|
||||
],
|
||||
"diff_lookup": [
|
||||
{
|
||||
"name": "lookup-both-known",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "a",
|
||||
"path": "/tmp/sck_truth_frozen_015/a.db"
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"path": "/tmp/sck_truth_frozen_015/b.db"
|
||||
}
|
||||
],
|
||||
"before_id": "a",
|
||||
"after_id": "b"
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"entries": 1,
|
||||
"active": 1
|
||||
},
|
||||
"after": {
|
||||
"entries": 1,
|
||||
"active": 1
|
||||
},
|
||||
"delta_entries": 0
|
||||
},
|
||||
"expected": {
|
||||
"before_path": "/tmp/sck_truth_frozen_015/a.db",
|
||||
"after_path": "/tmp/sck_truth_frozen_015/b.db"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lookup-before-unknown",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "b",
|
||||
"path": "/tmp/sck_truth_frozen_016/b.db"
|
||||
}
|
||||
],
|
||||
"before_id": "ghost",
|
||||
"after_id": "b"
|
||||
},
|
||||
"return": {
|
||||
"error": "unknown snapshot id"
|
||||
},
|
||||
"expected": {
|
||||
"error": "unknown snapshot id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lookup-after-unknown",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "a",
|
||||
"path": "/tmp/sck_truth_frozen_017/a.db"
|
||||
}
|
||||
],
|
||||
"before_id": "a",
|
||||
"after_id": "ghost"
|
||||
},
|
||||
"return": {
|
||||
"error": "unknown snapshot id"
|
||||
},
|
||||
"expected": {
|
||||
"error": "unknown snapshot id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lookup-duplicate-id-last-wins",
|
||||
"input": {
|
||||
"snapshots": [
|
||||
{
|
||||
"id": "a",
|
||||
"path": "/tmp/sck_truth_frozen_018/first.db"
|
||||
},
|
||||
{
|
||||
"id": "a",
|
||||
"path": "/tmp/sck_truth_frozen_018/second.db"
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"path": "/tmp/sck_truth_frozen_018/b.db"
|
||||
}
|
||||
],
|
||||
"before_id": "a",
|
||||
"after_id": "b"
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"entries": 5,
|
||||
"active": 5
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"delta_entries": -3
|
||||
},
|
||||
"expected": {
|
||||
"before_path": "/tmp/sck_truth_frozen_018/second.db",
|
||||
"after_path": "/tmp/sck_truth_frozen_018/b.db"
|
||||
}
|
||||
}
|
||||
],
|
||||
"diff_shape": [
|
||||
{
|
||||
"name": "shape-both-ok-delta",
|
||||
"input": {
|
||||
"before": {
|
||||
"entries": 3,
|
||||
"active": 2
|
||||
},
|
||||
"after": {
|
||||
"entries": 4,
|
||||
"active": 3
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"entries": 3,
|
||||
"active": 2
|
||||
},
|
||||
"after": {
|
||||
"entries": 4,
|
||||
"active": 3
|
||||
},
|
||||
"delta_entries": 1
|
||||
},
|
||||
"expected": {
|
||||
"before": {
|
||||
"entries": 3,
|
||||
"active": 2
|
||||
},
|
||||
"after": {
|
||||
"entries": 4,
|
||||
"active": 3
|
||||
},
|
||||
"delta_entries": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "shape-before-error-side",
|
||||
"input": {
|
||||
"before": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"delta_entries": 2
|
||||
},
|
||||
"expected": {
|
||||
"before": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"delta_entries": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "shape-after-error-side",
|
||||
"input": {
|
||||
"before": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"after": {
|
||||
"error": "file is not a database"
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"after": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"delta_entries": -2
|
||||
},
|
||||
"expected": {
|
||||
"before": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"after": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"delta_entries": -2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "shape-both-error-sides",
|
||||
"input": {
|
||||
"before": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"after": {
|
||||
"error": "file is not a database"
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"after": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"delta_entries": 0
|
||||
},
|
||||
"expected": {
|
||||
"before": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"after": {
|
||||
"error": "file is not a database"
|
||||
},
|
||||
"delta_entries": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "shape-zero-delta",
|
||||
"input": {
|
||||
"before": {
|
||||
"entries": 2,
|
||||
"active": 1
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 1
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"before": {
|
||||
"entries": 2,
|
||||
"active": 1
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 1
|
||||
},
|
||||
"delta_entries": 0
|
||||
},
|
||||
"expected": {
|
||||
"before": {
|
||||
"entries": 2,
|
||||
"active": 1
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 1
|
||||
},
|
||||
"delta_entries": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"facade_io": [
|
||||
{
|
||||
"name": "snapshot-io-real-count-and-file",
|
||||
"input": {
|
||||
"label": "io"
|
||||
},
|
||||
"return": {
|
||||
"entries": 3
|
||||
},
|
||||
"expected": {
|
||||
"entries": 3,
|
||||
"path_exists": true,
|
||||
"ledger_len": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "restore-io-db-repaired-journal-untouched",
|
||||
"input": {
|
||||
"snapshot_id": "20261012T120000-good"
|
||||
},
|
||||
"return": null,
|
||||
"expected": {
|
||||
"restored": true,
|
||||
"count_after": 2,
|
||||
"journal_untouched": true,
|
||||
"safety_in_ledger": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "diff-io-real-sqlite-counts",
|
||||
"input": {},
|
||||
"return": null,
|
||||
"expected": {
|
||||
"before": {
|
||||
"entries": 1,
|
||||
"active": 1
|
||||
},
|
||||
"after": {
|
||||
"entries": 2,
|
||||
"active": 2
|
||||
},
|
||||
"delta_entries": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "load-corrupt-ledger-reads-empty",
|
||||
"input": {},
|
||||
"return": null,
|
||||
"expected": {
|
||||
"list": [],
|
||||
"db": {
|
||||
"snapshots": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
//! state_checkpoint — learned-state checkpoint/replay/rollback (#154).
|
||||
//!
|
||||
//! FULL parity port of src/rune/state_checkpoint.py (dichotomy wave,
|
||||
//! #2479 tranche). Source code is recoverable from git; learned state
|
||||
//! may not be. This kernel snapshots the cognitive store's CHECKPOINT
|
||||
//! LEDGER so a known-good state can be restored WITHOUT deleting raw
|
||||
//! historical evidence: snapshot record shaping + ledger append,
|
||||
//! newest-first listing, restore decision (unknown / dry-run / proceed
|
||||
//! — the live DB is itself snapshotted first: rollback of the
|
||||
//! rollback), and diff (entry-count + status-shape comparison). The
|
||||
//! causal event history (journal) is NEVER touched — restore changes
|
||||
//! state, not evidence.
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
|
||||
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
|
||||
//! * sck_snapshot({ledger_doc, ts, at, label, entries, snap_path})
|
||||
//! → {record, ledger}
|
||||
//! * sck_list({snapshots}) → {snapshots} (newest first, stable)
|
||||
//! * sck_restore({snapshots, snapshot_id, dry_run}) → decision doc:
|
||||
//! {"restored": false, "error": ...} | {"restored": false,
|
||||
//! "dry_run": true, "would_restore": rec} | {"record": rec} (the
|
||||
//! facade executes the IO the decision implies: safety snapshot +
|
||||
//! copy2 + final doc)
|
||||
//! * sck_restore_commit({record, safety_id}) → {"restored": true,
|
||||
//! "snapshot": rec, "safety_snapshot": id}
|
||||
//! * sck_diff_lookup({snapshots, before_id, after_id}) →
|
||||
//! {"error": ...} | {before_path, after_path}
|
||||
//! * sck_diff_shape({before, after}) →
|
||||
//! {before, after, delta_entries}
|
||||
//!
|
||||
//! No-clock law: `ts` (%Y%m%dT%H%M%S) and `at` (ISO-8601) are always
|
||||
//! arguments; the kernel never reads the wall clock.
|
||||
//!
|
||||
//! Boundary: the FILE IO (sqlite open/count, shutil copy2, ledger
|
||||
//! load/save, env resolution in _store_db/_ledger_path) stays in the
|
||||
//! Python facade — the kernel is a pure function of the loaded ledger
|
||||
//! doc and facade-computed counts.
|
||||
//!
|
||||
//! Lookup duality (oracle semantics, pinned by the battery): restore
|
||||
//! matches the FIRST ledger entry with the id (match[0]); diff's by_id
|
||||
//! map is built dict-comprehension style so the LAST duplicate wins.
|
||||
//!
|
||||
//! No sck_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, Map, Value};
|
||||
use std::fmt;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
// ── oracle exceptions (typed, behind the doc envelope) ──────────────────────
|
||||
|
||||
/// The oracle raises KeyError when a ledger doc lacks the `snapshots`
|
||||
/// array (data["snapshots"].append). The envelope's `err` string carries
|
||||
/// the Python kind prefix so the loader re-raises the same type.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CheckpointError {
|
||||
Key(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CheckpointError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CheckpointError::Key(k) => write!(f, "KeyError: {k}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshots_array(ledger: &Map<String, Value>) -> Result<&Vec<Value>, CheckpointError> {
|
||||
ledger
|
||||
.get("snapshots")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| CheckpointError::Key("snapshots".to_string()))
|
||||
}
|
||||
|
||||
/// `s.get("at", "")` — the listing sort key (missing → sorts last).
|
||||
fn sort_key(s: &Value) -> &str {
|
||||
s.get("at").and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
// ── checkpoint operations (pure functions of the ledger doc) ────────────────
|
||||
|
||||
/// `snapshot` — record shaping + ledger append. The DB copy and the
|
||||
/// sqlite entry count are facade IO; `entries` rides in (−1 marks
|
||||
/// unknown per the oracle's informational-count law).
|
||||
pub fn snapshot(
|
||||
ledger_doc: &Map<String, Value>,
|
||||
ts: &str,
|
||||
at: &str,
|
||||
label: &str,
|
||||
entries: i64,
|
||||
snap_path: &str,
|
||||
) -> Result<(Value, Map<String, Value>), CheckpointError> {
|
||||
let snap_id = format!("{ts}-{label}");
|
||||
let rec = json!({
|
||||
"id": snap_id,
|
||||
"label": label,
|
||||
"path": snap_path,
|
||||
"entries": entries,
|
||||
"at": at,
|
||||
});
|
||||
let mut out = ledger_doc.clone();
|
||||
let arr = snapshots_array(&out)?; // borrow ends before the mutable insert
|
||||
let mut arr = arr.clone();
|
||||
arr.push(rec.clone());
|
||||
out.insert("snapshots".to_string(), Value::Array(arr));
|
||||
Ok((rec, out))
|
||||
}
|
||||
|
||||
/// `list_snapshots` — the ledger, newest first. Python's
|
||||
/// `sorted(..., reverse=True)` is STABLE: equal `at` keys keep their
|
||||
/// original order, and Rust's stable sort-by-reversed-key matches.
|
||||
pub fn list(snapshots: &[Value]) -> Vec<Value> {
|
||||
let mut out = snapshots.to_vec();
|
||||
out.sort_by(|a, b| sort_key(b).cmp(sort_key(a)));
|
||||
out
|
||||
}
|
||||
|
||||
/// The restore decision — classified once, executed by the facade.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum RestoreOutcome {
|
||||
Unknown(String),
|
||||
DryRun(Value),
|
||||
Proceed(Value),
|
||||
}
|
||||
|
||||
/// `restore` decision core: FIRST ledger match with the id (match[0]).
|
||||
fn classify_restore(snapshots: &[Value], snapshot_id: &str, dry_run: bool) -> RestoreOutcome {
|
||||
let rec = snapshots
|
||||
.iter()
|
||||
.find(|s| s.get("id").and_then(Value::as_str) == Some(snapshot_id));
|
||||
match rec {
|
||||
None => RestoreOutcome::Unknown(format!("unknown snapshot: {snapshot_id}")),
|
||||
Some(rec) if dry_run => RestoreOutcome::DryRun(rec.clone()),
|
||||
Some(rec) => RestoreOutcome::Proceed(rec.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `restore` decision doc (see classify_restore).
|
||||
pub fn restore(snapshots: &[Value], snapshot_id: &str, dry_run: bool) -> Value {
|
||||
match classify_restore(snapshots, snapshot_id, dry_run) {
|
||||
RestoreOutcome::Unknown(err) => json!({"restored": false, "error": err}),
|
||||
RestoreOutcome::DryRun(rec) => {
|
||||
json!({"restored": false, "dry_run": true, "would_restore": rec})
|
||||
}
|
||||
RestoreOutcome::Proceed(rec) => json!({"record": rec}),
|
||||
}
|
||||
}
|
||||
|
||||
/// `restore` final doc shaping — the facade supplies the safety
|
||||
/// snapshot id after its rollback-of-the-rollback IO.
|
||||
pub fn restore_commit(record: &Value, safety_id: &str) -> Value {
|
||||
json!({"restored": true, "snapshot": record, "safety_snapshot": safety_id})
|
||||
}
|
||||
|
||||
/// `diff` lookup core: by_id built dict-comprehension style — the LAST
|
||||
/// duplicate id wins. Either side unknown → the oracle's error doc.
|
||||
pub fn diff_lookup(snapshots: &[Value], before_id: &str, after_id: &str) -> Value {
|
||||
let mut by_id: Map<String, Value> = Map::new();
|
||||
for s in snapshots {
|
||||
if let Some(id) = s.get("id").and_then(Value::as_str) {
|
||||
by_id.insert(id.to_string(), s.clone());
|
||||
}
|
||||
}
|
||||
let path_of = |sid: &str| {
|
||||
by_id
|
||||
.get(sid)
|
||||
.and_then(|s| s.get("path"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
};
|
||||
match (path_of(before_id), path_of(after_id)) {
|
||||
(Some(bp), Some(ap)) => json!({"before_path": bp, "after_path": ap}),
|
||||
_ => json!({"error": "unknown snapshot id"}),
|
||||
}
|
||||
}
|
||||
|
||||
/// `diff` shape core: per-side buckets pass through (an error side has
|
||||
/// no `entries` and contributes the oracle's .get("entries", 0) → 0).
|
||||
pub fn diff_shape(before: &Value, after: &Value) -> Value {
|
||||
let entries = |side: &Value| side.get("entries").and_then(Value::as_i64).unwrap_or(0);
|
||||
json!({
|
||||
"before": before,
|
||||
"after": after,
|
||||
"delta_entries": entries(after) - entries(before),
|
||||
})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────────
|
||||
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
Checkpoint(CheckpointError),
|
||||
}
|
||||
|
||||
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}"),
|
||||
FfiError::Checkpoint(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ledger_of(a: &Value) -> Map<String, Value> {
|
||||
a.get("ledger_doc")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn snaps_of(a: &Value) -> Vec<Value> {
|
||||
a.get("snapshots")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// sck_snapshot({ledger_doc, ts, at, label, entries, snap_path})
|
||||
/// → {record, ledger}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sck_snapshot(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
let entries = a.get("entries").and_then(Value::as_i64).unwrap_or(0);
|
||||
snapshot(&ledger_of(&a), s("ts"), s("at"), s("label"), entries, s("snap_path"))
|
||||
.map(|(record, ledger)| json!({"record": record, "ledger": ledger}))
|
||||
.map_err(FfiError::Checkpoint)
|
||||
}))
|
||||
}
|
||||
|
||||
/// sck_list({snapshots}) → {snapshots}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sck_list(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| json!({"snapshots": list(&snaps_of(&a))})))
|
||||
}
|
||||
|
||||
/// sck_restore({snapshots, snapshot_id, dry_run}) → decision doc
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sck_restore(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let snapshot_id = a.get("snapshot_id").and_then(Value::as_str).unwrap_or("");
|
||||
let dry_run = a.get("dry_run").and_then(Value::as_bool).unwrap_or(false);
|
||||
restore(&snaps_of(&a), snapshot_id, dry_run)
|
||||
}))
|
||||
}
|
||||
|
||||
/// sck_restore_commit({record, safety_id}) → final doc
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sck_restore_commit(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let record = a.get("record").cloned().unwrap_or(Value::Null);
|
||||
let safety_id = a.get("safety_id").and_then(Value::as_str).unwrap_or("");
|
||||
restore_commit(&record, safety_id)
|
||||
}))
|
||||
}
|
||||
|
||||
/// sck_diff_lookup({snapshots, before_id, after_id})
|
||||
/// → {error} | {before_path, after_path}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sck_diff_lookup(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let before_id = a.get("before_id").and_then(Value::as_str).unwrap_or("");
|
||||
let after_id = a.get("after_id").and_then(Value::as_str).unwrap_or("");
|
||||
diff_lookup(&snaps_of(&a), before_id, after_id)
|
||||
}))
|
||||
}
|
||||
|
||||
/// sck_diff_shape({before, after}) → {before, after, delta_entries}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sck_diff_shape(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let before = a.get("before").cloned().unwrap_or(Value::Null);
|
||||
let after = a.get("after").cloned().unwrap_or(Value::Null);
|
||||
diff_shape(&before, &after)
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
Reference in new issue
Block a user