feat(audit_ledger): close PARTIAL gaps — dichotomy wave

audit 2/8 PARTIAL -> 8/8 FULL (RUST). The chain kernels live in the
evidence_provenance crate (the live ep seam the oracle already delegates
hash+verify through — one chain, one key, RECONCILE-DON'T-CLOBBER): the
three missing log_* wrappers ported name-preserving (log_promotion,
log_gate_result, log_drift_alert + ep_* FFI + ep_loader bindings), the
facade switched onto RUNE_AUDIT_LEDGER_CORE delegation with _py_*
vintage fallbacks (both-pins law), and the audit mapping aliased to the
crate (drift_compression precedent). The divergent W6-era
src/rust/audit_ledger stub (SHA-256 concat, wrong field names, never the
live chain) is deleted.

RUNE_FROZEN_NOW clock seam (PP_ACTIVE_THREADS pattern) added to
now_iso_utc so pinned-python and Rust ledger writes hash byte-identically.

Fix-first, frozen-proof (two pre-existing kernel defects):
- get_entries negative-limit slice law: Python lines[-limit:] with
  limit<0 drops the FRONT (lines[|limit|:]), not all lines.
- pyjson integral-float parse: numbers with '.'/exponent now parse as
  Json::Float (json.loads law) — "0.0" round-trips as 0.0, never
  collapses to 0; any Python-written entry with an integral float
  previously HMAC-verified FAILING (payload bytes differed). habitmath
  count/confidence extraction arms widened Num|Float (restores
  test_behavior_decision_math).

Frozen truth: 16-case envelope via the shared freeze_truth harness +
scripts/al_freeze_seams rig (sequential chain: missing-file branches,
genesis first append, the three log_* detail shapes, mixed-type details,
get_entries limit lattice 50/3/1/0/-2/100, full-chain verify,
_compute_hash sort_keys law). --verify byte-identical (x2). Rust battery
audit_battery.rs replays the corpus VERBATIM (include_str!) + asserts the
ledger FILE bytes equal expected pyjson dumps. Live parity 360/360
(pinned/live/kernel 16 each, file-byte parity EQUAL, 5 seams incl. the
documented missing-hash crash-superset asserted per-side, fuzz seed 2479
300/300 rounds incl. file bytes, CLI parity 5/5 python -m vs Rust bin).
cargo build/clippy -D warnings/fmt/test clean; pytest 11/11 BOTH pins;
ruff clean; ep canary 66/0; canary_audit_ledger isolated 74/0 both pins.

Evidence: verification/al-*.exit + al-summary.md (logs local, *.log
gitignored).
This commit is contained in:
Nick committed 2026-10-14 16:41:39 +01:00
1 parent 1e3967a9f0
commit 29989a5b27
31 files changed
+1901 -379

No files matched your search

+6 -6
View File
@@ -2,16 +2,16 @@
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
**FULL completion: 63%** (121/192 runtime modules at FULL)
**FULL completion: 64%** (122/192 runtime modules at FULL)
Function-level detail: 74% (1692/2288 runtime functions matched) — secondary; includes in-flight work
Function-level detail: 74% (1698/2288 runtime functions matched) — secondary; includes in-flight work
| Status | Modules | Lines | Meaning |
|---|---|---|---|
| FULL | 121 | 49623 | 80%+ of functions ported |
| PARTIAL | 32 | 14469 | 20–80% — real logic exists, gaps named below |
| FULL | 122 | 49925 | 80%+ of functions ported |
| PARTIAL | 31 | 14257 | 20–80% — real logic exists, gaps named below |
| NOT TOUCHED | 39 | 11471 | no name-preserving implementation |
| Total runtime | 192 | 75563 | |
| Total runtime | 192 | 75653 | |
## NOT TOUCHED — the port queue, in priority order (largest first)
@@ -67,7 +67,6 @@ Function-level detail: 74% (1692/2288 runtime functions matched) — secondary;
| tool_executor | 1836 | 33/47 | _execute_tool_now, serialize_prepared_tool, deserialize_prepared_tool, confirm_write, _resolve_execution_cwd, _run_command |
| time_awareness | 402 | 12/20 | _clock_args, _ta_core, get_time_awareness, to_dict, _to_state, _apply_state |
| intent_continuity | 284 | 6/13 | _now, _raise_core_err, to_dict, from_dict, save, _ledger_state |
| audit_ledger | 212 | 2/8 | _compute_hash, get_entries, log_promotion, log_gate_result, log_drift_alert, main |
| voicebox | 164 | 5/11 | _run, start_recording, stop_recording, _transcribe_and_type, on_press, on_release |
| persona_definition | 210 | 2/7 | _by_topic, compile_full, emit_constitution, emit_identity, emit_persona |
| drift_observability | 399 | 5/9 | _do_core, main, _load_alerts, _save_alerts |
@@ -171,6 +170,7 @@ Function-level detail: 74% (1692/2288 runtime functions matched) — secondary;
| lexicon_evolution | 304 | 7/8 | unclassified |
| event_envelope | 303 | 8/8 | GO |
| stage_guards | 303 | 9/9 | GO |
| audit_ledger | 302 | 8/8 | RUST |
| tier_manager | 300 | 10/10 | RUST |
| message_graph | 295 | 12/13 | unclassified |
| toolchain | 295 | 5/6 | RUST |
+36
View File
@@ -0,0 +1,36 @@
"""al_freeze_seams — audit_ledger truth-freeze seams (sitecustomize rig).
Auto-imported by every interpreter started with
PYTHONPATH=scripts/al_freeze_seams (freeze AND --verify), so the oracle
resolves hermetic state at import:
* RUNE_MEMORY_DIR → fresh mkdtemp per process — LEDGER_FILE derives from
it at import (isolation law: never the live chain at
~/.config/opencode/memory/promotion_ledger.jsonl).
* AUDIT_LEDGER_KEY popped — key determinism (the drift-authority default
is the frozen contract).
* RUNE_AUDIT_LEDGER_CORE=python — the harness derives the same pin from
the module stem; this covers direct imports.
The clock pins ONLY when RUNE_FROZEN_NOW is already set at process start
(CLI-subprocess parity: `python -m rune.audit_ledger` under the frozen
clock vs the Rust bin, which reads the same env through its own seam);
freeze/--verify set the env AFTER startup, so the harness's module-level
patch stays the authority there.
"""
import os
import sys
_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(_ROOT, "scripts"))
from freeze_rigs import pin_clock, sandbox_dir # noqa: E402
sandbox_dir("RUNE_MEMORY_DIR", "al-ledger")
os.environ.pop("AUDIT_LEDGER_KEY", None)
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
_frozen_now = os.environ.get("RUNE_FROZEN_NOW", "")
if _frozen_now:
pin_clock(_frozen_now)
+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env python3
"""check_audit_ledger_parity.py — live parity for the audit_ledger gap-close
port (dichotomy wave, #2479 tranche).
Layers over the FROZEN truth (src/rust/evidence_provenance/
frozen_audit_ledger_truth.json — 16 cases frozen via
scripts/gen_audit_ledger_gap_truth.py against the pure _py_* vintage):
1. facade PINNED — RUNE_AUDIT_LEDGER_CORE=python: the public fns run
the pure-Python vintage against the frozen expectations.
2. facade LIVE — delegation on (Rust core through ep_loader): the
same public fns against the same expectations.
3. KERNEL — ep_loader.* called directly (the FFI path).
4. FILE-BYTE parity — the golden sequence run pinned-python and
live-Rust in separate sandboxes under the SAME frozen clock: the two
ledger.jsonl files must be BYTE-identical (separators, key order,
ensure_ascii, HMAC math end to end).
5. SEAMS the single-pass frozen chain cannot express (each proven
oracle-vs-Rust, both pins agreeing):
junk last line → genesis continuation + invalid-JSON verify reason;
parseable-but-hashless last line → genesis; bad-JSON line skipped by
get_entries; details tamper → HMAC-mismatch reason; prev-hash rewrite
→ chain-broken reason; hash-key REMOVED → the vintage CRASHES
(stored_hash[:16] on None) while Rust reports stored=None — the
documented crash-superset divergence (lib.rs), asserted exactly.
6. FUZZ (seed 2479): random op sequences (append + all three log_*
wrappers, unicode/float/None/bool/nested details, random get_entries
limits incl. negatives) — pinned facade vs live facade vs kernel;
entries, verify results AND file bytes must agree.
7. CLI parity — python -m rune.audit_ledger (pinned) vs the Rust
evidence_provenance bin on identically-seeded sandboxes: verify /
list / log-promotion stdout byte-equal, no-args exit 1 both
(argparse help text vs the mirrored HELP const is out of byte scope).
Exit 0 iff every layer is 100%.
Run: PYTHONPATH=src:src/rust/evidence_provenance python3 \
scripts/check_audit_ledger_parity.py
"""
from __future__ import annotations
import json
import os
import random
import subprocess
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
TRUTH_PATH = REPO / "src" / "rust" / "evidence_provenance" / "frozen_audit_ledger_truth.json"
FUZZ_SEED = 2479
RUST_BIN = REPO / "src" / "rust" / "evidence_provenance" / "target" / "release" / "evidence_provenance"
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rust" / "evidence_provenance"))
sys.path.insert(0, str(REPO / "scripts"))
os.environ.pop("AUDIT_LEDGER_KEY", None)
from freeze_truth import _frozen_clock # noqa: E402
from rune import audit_ledger as al # noqa: E402
import ep_loader # noqa: E402
ENV = json.loads(TRUTH_PATH.read_text())
FROZEN_NOW = ENV["frozen_now"]
CASES = ENV["cases"]
GOLDEN_OPS = [
("log_promotion", (1, "PROVISIONAL", "VALIDATED", 2, "gate pass")),
("log_gate_result", (2, "PASS", {"quality": 0.9, "safety": True})),
("log_drift_alert", ("severe_shift", "drift exceeded band ±3σ", "high")),
("append_entry", ("note", 5, {"mixed": [1, 2.5, None, True, "漢字"], "nested": {"b": 2, "a": 1}})),
]
def _sandbox() -> Path:
d = Path(tempfile.mkdtemp(prefix="al-parity-"))
return d / "promotion_ledger.jsonl"
def _pin_clock() -> None:
al.datetime = _frozen_clock(FROZEN_NOW)
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW
def _facade_call(fn_name: str, args: list):
fn = getattr(al, fn_name)
return fn(*args)
def _kernel_call(fn_name: str, ledger: Path, args: list):
key = al.SECRET_KEY
if fn_name == "verify_chain":
return ep_loader.verify_chain(str(ledger), key)
if fn_name == "get_entries":
return ep_loader.get_entries(str(ledger), args[0] if args else 50)
if fn_name == "append_entry":
return ep_loader.append_entry(str(ledger), key, args[0], args[1], json.dumps(args[2]))
if fn_name == "log_promotion":
return ep_loader.log_promotion(str(ledger), key, *args)
if fn_name == "log_gate_result":
return ep_loader.log_gate_result(str(ledger), key, *args)
if fn_name == "log_drift_alert":
return ep_loader.log_drift_alert(str(ledger), key, *args)
if fn_name == "_compute_hash":
return ep_loader.compute_hash(args[0], json.dumps(args[1], sort_keys=True), key)
raise KeyError(fn_name)
def run_frozen_layer(label: str, mode: str) -> tuple[int, int]:
ok = 0
fails: list[str] = []
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
_pin_clock()
if mode == "pinned":
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
else:
os.environ.pop("RUNE_AUDIT_LEDGER_CORE", None)
for case in CASES:
fn_name, args = case["fn"], case["args"]
try:
if mode == "kernel":
got = _kernel_call(fn_name, ledger, args)
else:
got = _facade_call(fn_name, args)
except Exception as e: # noqa: BLE001 — a crash IS the divergence
fails.append(f"{fn_name} raised {type(e).__name__}: {e}")
continue
if got == case["expected"]:
ok += 1
else:
fails.append(f"{fn_name} got {json.dumps(got, default=str)[:140]} want {json.dumps(case['expected'], default=str)[:140]}")
print(f"{label}: {ok}/{len(CASES)} match")
for f in fails[:5]:
print(f" DIVERGE {f[:220]}")
return ok, len(CASES)
def file_byte_parity() -> tuple[int, int]:
files = {}
for mode in ("pinned", "live"):
if mode == "pinned":
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
else:
os.environ.pop("RUNE_AUDIT_LEDGER_CORE", None)
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
for fn_name, args in GOLDEN_OPS:
_facade_call(fn_name, list(args))
files[mode] = ledger.read_bytes()
equal = files["pinned"] == files["live"]
print(f"file-byte parity (pinned python vs live Rust, frozen clock): {'EQUAL' if equal else 'DIVERGE'}")
if not equal:
print(f" py : {files['pinned'][:200]!r}")
print(f" rs : {files['live'][:200]!r}")
return (1, 1) if equal else (0, 1)
def _both_pins_seam(name: str, seed_and_check) -> tuple[int, int]:
"""Run one seam scenario under the python pin and live; both must agree
with each other (and with the asserted oracle shape)."""
results = []
for mode in ("pinned", "live"):
if mode == "pinned":
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
else:
os.environ.pop("RUNE_AUDIT_LEDGER_CORE", None)
results.append(seed_and_check())
agree = results[0] == results[1]
print(f"seam {name}: pinned==live {'OK' if agree else 'DIVERGE'} {str(results[0])[:150]}")
return (1, 1) if agree else (0, 1)
def seam_checks() -> tuple[int, int]:
ok = total = 0
def junk_last_line():
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
al.append_entry("note", 1, {"a": 1})
with open(ledger, "a") as f:
f.write("{not json\n")
entry = al.append_entry("note", 2, {"b": 2})
verify = al.verify_chain()
got = al.get_entries()
return (entry["prev_hash"], verify, len(got))
def hashless_last_line():
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
with open(ledger, "w") as f:
f.write('{"type": "x"}\n')
entry = al.append_entry("note", 1, {"a": 1})
return entry["prev_hash"]
def tamper_details():
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
al.append_entry("promotion", 1, {"x": 1})
al.append_entry("promotion", 2, {"y": 2})
lines = ledger.read_text().splitlines()
bad = json.loads(lines[0])
bad["details"] = {"x": 999}
lines[0] = json.dumps(bad)
ledger.write_text("\n".join(lines) + "\n")
return al.verify_chain()
def broken_prev_link():
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
al.append_entry("promotion", 1, {"x": 1})
al.append_entry("promotion", 2, {"y": 2})
lines = ledger.read_text().splitlines()
bad = json.loads(lines[1])
bad["prev_hash"] = "deadbeef"
lines[1] = json.dumps(bad)
ledger.write_text("\n".join(lines) + "\n")
return al.verify_chain()
def missing_hash_crash():
# hash key REMOVED: the vintage crashes (stored_hash[:16] on None);
# Rust reports stored=None — documented crash-superset divergence
# (lib.rs). Each side is asserted against its OWN law.
ledger = _sandbox()
al.LEDGER_FILE = str(ledger)
al.append_entry("promotion", 1, {"x": 1})
lines = ledger.read_text().splitlines()
bad = json.loads(lines[0])
bad.pop("hash")
lines[0] = json.dumps(bad)
ledger.write_text("\n".join(lines) + "\n")
try:
r = al.verify_chain()
except TypeError:
return "CRASH:TypeError"
return ("SUPERSET", r["valid"], r["entries"], r["reason"][:60])
for name, fn in [
("junk-last-line→genesis", junk_last_line),
("hashless-last-line→genesis", hashless_last_line),
("tamper→HMAC-mismatch", tamper_details),
("prev-link→chain-broken", broken_prev_link),
]:
o, t = _both_pins_seam(name, fn)
ok, total = ok + o, total + t
# missing-hash: pinned must CRASH (vintage law), live must report the
# superset shape (Rust law) — both asserted exactly, neither compared
# to the other.
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
pinned_shape = missing_hash_crash()
os.environ.pop("RUNE_AUDIT_LEDGER_CORE", None)
live_shape = missing_hash_crash()
crash_ok = pinned_shape == "CRASH:TypeError"
superset_ok = (
isinstance(live_shape, tuple)
and live_shape[0] == "SUPERSET"
and live_shape[1] is False
and live_shape[2] == 0
and live_shape[3].startswith("HMAC mismatch at line 1: stored=None..., computed=")
)
print(
f"seam missing-hash: pinned crash {'OK' if crash_ok else 'DIVERGE'} ({pinned_shape}); "
f"live superset {'OK' if superset_ok else 'DIVERGE'} {str(live_shape)[:110]}"
)
ok, total = ok + (1 if crash_ok else 0), total + 1
ok, total = ok + (1 if superset_ok else 0), total + 1
return ok, total
def fuzz_check(rounds: int = 300) -> tuple[int, int]:
rng = random.Random(FUZZ_SEED)
ok = total = 0
def agree(label: str, values: list) -> None:
nonlocal ok, total
total += 1
if all(v == values[0] for v in values[1:]):
ok += 1
else:
print(f" FUZZ DIVERGE [{label}]: {json.dumps(values, default=str)[:360]}")
words = ["alpha", "beta", "γάμα", "漢字", "δelta-42", "with spaces", ""]
for i in range(rounds):
ledger = _sandbox()
ops = []
for _ in range(rng.randint(1, 6)):
kind = rng.choice(["append", "promo", "gate", "alert"])
if kind == "append":
details = {
"n": rng.randint(-5, 99),
"f": rng.choice([0.0, 0.5, 1.25, 99.99, rng.random()]),
"s": rng.choice(words),
"maybe": rng.choice([None, True, False]),
}
if rng.random() < 0.4:
details["nested"] = {"z": [1, 2.5, None], rng.choice(["k", "w"]): rng.choice(words)}
ops.append(("append_entry", ("note", rng.randint(0, 50), details)))
elif kind == "promo":
ops.append(("log_promotion", (rng.randint(0, 20), rng.choice(words), rng.choice(words), rng.randint(0, 40), rng.choice(words))))
elif kind == "gate":
ops.append(("log_gate_result", (rng.randint(0, 20), rng.choice(["PASS", "DEGRADE", "FAIL"]), {"q": rng.random(), "n": rng.choice(words)})))
else:
ops.append(("log_drift_alert", (rng.choice(words), rng.choice(words), rng.choice(["low", "high"]))))
limit = rng.choice([50, 1, 2, 3, 0, -1, -2, -99, 500])
outcomes = []
for mode in ("pinned", "live", "kernel"):
if mode == "pinned":
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
else:
os.environ.pop("RUNE_AUDIT_LEDGER_CORE", None)
al.LEDGER_FILE = str(ledger)
entries = []
for fn_name, args in ops:
entries.append(_facade_call(fn_name, list(args)) if mode != "kernel" else _kernel_call(fn_name, ledger, list(args)))
if mode != "kernel":
g = al.get_entries(limit)
v = al.verify_chain()
else:
g = ep_loader.get_entries(str(ledger), limit)
v = ep_loader.verify_chain(str(ledger), al.SECRET_KEY)
outcomes.append((entries, g, v, ledger.read_bytes()))
ledger.unlink()
agree(f"fuzz-{i}", outcomes)
print(f"fuzz (seed {FUZZ_SEED}): {ok}/{total} rounds agree pinned/live/kernel (entries+get+verify+file bytes)")
return ok, total
def cli_parity() -> tuple[int, int]:
if not RUST_BIN.exists():
print("CLI parity: Rust bin not built — run cargo build --release first")
return 0, 1
ok = total = 0
def run(cmd: list[str], env_extra: dict, rig: bool = False) -> subprocess.CompletedProcess:
env = dict(os.environ)
env.pop("AUDIT_LEDGER_KEY", None)
env.update(env_extra)
env["PYTHONPATH"] = (
"src:scripts/al_freeze_seams" if rig else "src"
)
return subprocess.run(
cmd, capture_output=True, text=True, cwd=REPO, env=env, timeout=60
)
for sub in (["verify"], ["list"], ["list", "--limit", "1"], ["log-promotion", "7", "A", "B", "3", "cli reason"]):
outs = {}
for side in ("py", "rs"):
sandbox = Path(tempfile.mkdtemp(prefix=f"al-cli-{side}-"))
env = {
"RUNE_MEMORY_DIR": str(sandbox),
"RUNE_FROZEN_NOW": FROZEN_NOW,
}
if side == "py":
env["RUNE_AUDIT_LEDGER_CORE"] = "python"
cmd = [sys.executable, "-m", "rune.audit_ledger", *sub]
else:
cmd = [str(RUST_BIN), *sub]
r = run(cmd, env, rig=(side == "py"))
outs[side] = (r.returncode, r.stdout)
total += 1
if outs["py"] == outs["rs"]:
ok += 1
else:
print(f" CLI DIVERGE {sub}: py={outs['py']!r:.160} rs={outs['rs']!r:.160}")
# no-args: help + exit 1 both (argparse help text is out of byte scope)
codes = {}
for side in ("py", "rs"):
sandbox = Path(tempfile.mkdtemp(prefix=f"al-cli0-{side}-"))
env = {"RUNE_MEMORY_DIR": str(sandbox), "RUNE_FROZEN_NOW": FROZEN_NOW}
if side == "py":
env["RUNE_AUDIT_LEDGER_CORE"] = "python"
cmd = [sys.executable, "-m", "rune.audit_ledger"]
else:
cmd = [str(RUST_BIN)]
codes[side] = run(cmd, env, rig=(side == "py")).returncode
total += 1
if codes["py"] == codes["rs"] == 1:
ok += 1
else:
print(f" CLI no-args exit divergence: py={codes['py']} rs={codes['rs']}")
print(f"CLI parity (python -m vs Rust bin): {ok}/{total}")
return ok, total
def main() -> int:
_pin_clock()
total_ok = total_n = 0
for label, mode in [
("facade pinned (RUNE_AUDIT_LEDGER_CORE=python)", "pinned"),
("facade live (Rust core through ep_loader)", "live"),
("kernel direct (FFI)", "kernel"),
]:
ok, n = run_frozen_layer(label, mode)
total_ok, total_n = total_ok + ok, total_n + n
ok, n = file_byte_parity()
total_ok, total_n = total_ok + ok, total_n + n
ok, n = seam_checks()
total_ok, total_n = total_ok + ok, total_n + n
ok, n = fuzz_check()
total_ok, total_n = total_ok + ok, total_n + n
ok, n = cli_parity()
total_ok, total_n = total_ok + ok, total_n + n
print(f"parity: {total_ok}/{total_n} match")
return 0 if total_ok == total_n else 1
if __name__ == "__main__":
sys.exit(main())
+8
View File
@@ -177,6 +177,14 @@ def module_to_keys(module_stem: str) -> list[str]:
# (H3 slice 2 + the stats kernel): the live delegation chain is
# drift_compression → drift_core._ms_core → ms_loader → .so.
"drift_compression": ["memory_scoring"],
# audit_ledger's chain kernels live in the evidence_provenance
# crate (Phase G slice 2, live ep seam — the oracle already
# delegates hash+verify there): compute_hash/append_entry/
# verify_chain/get_entries + the log_* gap-close wrappers + the
# main() CLI mirror. The W6-era src/rust/audit_ledger stub
# (SHA-256 concat, wrong field names — never the live chain) was
# deleted; one chain, one implementation (RECONCILE-DON'T-CLOBBER).
"audit_ledger": ["evidence_provenance"],
}
if module_stem in aliases:
keys = aliases[module_stem] + keys
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""gen_audit_ledger_gap_truth.py — freeze the audit_ledger oracle truth
(dichotomy wave #2479 gap-close) via the SHARED harness.
16 cases, one sequential chain (file state evolves across cases — that IS
the ledger semantics: prev_hash = last line's hash). Captured under
RUNE_AUDIT_LEDGER_CORE=python against the pure _py_* vintage (delegation-
proof by construction) with the clock pinned (RUNE_FROZEN_NOW) and a
hermetic RUNE_MEMORY_DIR sandbox (isolation law — never the live chain).
Branch canopy: missing-file verify/get, genesis first append, the three
log_* wrappers' exact details shapes, mixed-type details (floats, None,
bools, unicode, nested dicts with sort_keys-hostile insertion order),
get_entries limit lattice (default/1/3/0/-2/100 — the [-0:] == all trap),
full-chain verify, and _compute_hash's sort_keys law.
The junk-last-line and bad-JSON-skip branches need mid-sequence file
mutation the single-pass harness cannot express — they are pinned live in
scripts/check_audit_ledger_parity.py (oracle vs Rust, both pins).
Re-freeze byte-identical:
PYTHONPATH=src:scripts/al_freeze_seams python3 scripts/gen_audit_ledger_gap_truth.py
Verify:
PYTHONPATH=src:scripts/al_freeze_seams python3 scripts/freeze_truth.py \
--verify src/rust/evidence_provenance/frozen_audit_ledger_truth.json \
--module rune.audit_ledger
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "scripts"))
# Seams BEFORE the oracle import (in-process equivalent of the
# scripts/al_freeze_seams sitecustomize rig).
os.environ["RUNE_MEMORY_DIR"] = tempfile.mkdtemp(prefix="al-freeze-")
os.environ.pop("AUDIT_LEDGER_KEY", None)
os.environ["RUNE_AUDIT_LEDGER_CORE"] = "python"
FROZEN_NOW = "2026-10-14T12:00:00+00:00"
OUT = REPO / "src" / "rust" / "evidence_provenance" / "frozen_audit_ledger_truth.json"
from freeze_truth import freeze, write_envelope # noqa: E402
CASES = [
("verify_chain", ()),
("get_entries", ()),
("append_entry", ("promotion", 1, {"old_status": "PROVISIONAL", "new_status": "VALIDATED", "wash_count": 2, "reason": "gate pass"})),
("append_entry", ("gate", 2, {"verdict": "PASS", "critics": {"quality": 0.9, "safety": True}})),
("log_promotion", (3, "WASHABLE", "ROCK", 4, "stabilized after 4 washes")),
("log_gate_result", (4, "DEGRADE", {"quality": 0.55, "notes": ["weak evidence", "unicode 漢字"]})),
("log_drift_alert", ("severe_shift", "drift exceeded band ±3σ", "high")),
("append_entry", ("note", 5, {"mixed": [1, 2.5, None, True, "str"], "nested": {"b": 2, "a": 1}})),
("get_entries", ()),
("get_entries", (3,)),
("get_entries", (1,)),
("get_entries", (0,)),
("get_entries", (-2,)),
("get_entries", (100,)),
("verify_chain", ()),
("_compute_hash", ("genesis", {"b": 2, "a": {"z": 1, "y": [True, None]}})),
]
def main() -> int:
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW
truth = freeze("rune.audit_ledger", CASES, oracle_module="rune.audit_ledger")
write_envelope(FROZEN_NOW, truth, OUT)
print(f"froze {len(truth)} cases -> {OUT}")
return 0
if __name__ == "__main__":
sys.exit(main())
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Open the #2479 dichotomy-wave gap-closing construction contract for
audit_ledger (PARTIAL 2/8 → 8/8 fn coverage). Walks draft→approved→
building in ONE process (shared-store clobber hazard). Close happens after
the landing is pushed, with SHA evidence (close_contract_gap_al.py).
CS_RUST_LIB must point at a BUILT capability_scoring .so before any
submit_need (the Python oracle is retired and raises).
"""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from rune.construction_interface import ConstructionInterface
iface = ConstructionInterface(builder="factory-gap-al")
r = iface.submit_need(
"Close audit_ledger PARTIAL gaps (dichotomy wave, issue #2479 "
"tranche): the chain kernels live in the evidence_provenance crate "
"(the live ep seam the oracle already delegates hash+verify through) — "
"port the three missing log_* wrappers name-preserving "
"(log_promotion, log_gate_result, log_drift_alert + ep_* FFI + "
"loader bindings), switch the facade onto RUNE_AUDIT_LEDGER_CORE "
"delegation with _py_* vintage fallbacks, add the RUNE_FROZEN_NOW "
"clock seam for cross-language byte parity, fix-first the two "
"frozen-truth-proven kernel defects (get_entries negative-limit "
"slice law; pyjson integral-float parse→dump round-trip), delete the "
"divergent W6 src/rust/audit_ledger stub (RECONCILE-DON'T-CLOBBER), "
"alias the audit mapping, freeze 16-case truth via the shared "
"harness, Rust battery replay + file-byte law, live parity 100% on "
"the gap fns.",
origin="builder",
)
print("submit_need:", json.dumps(r)[:400])
if not r.get("success"):
print("NEED FAILED — aborting")
sys.exit(1)
c = r["ids"]["contract_id"]
print("contract_id:", c)
for phase in ("approved", "building"):
t = iface.transition(c, phase)
print(f"transition->{phase}:", json.dumps(t)[:300])
if not t.get("success"):
sys.exit(1)
print("OPENED", c)
+116 -26
View File
@@ -42,15 +42,25 @@ import ep_loader # noqa: E402 (loads target/release cdylib)
def _compute_hash(prev_hash: str, entry: dict) -> str:
"""Compute HMAC chain hash for a ledger entry."""
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_compute_hash(prev_hash, entry)
payload = json.dumps(entry, sort_keys=True)
return ep_loader.compute_hash(prev_hash, payload, SECRET_KEY)
# ── Python vintage (strangler stage-3 burn candidates) ────────────────────────
# The pre-switch implementations, byte-faithful. RUNE_AUDIT_LEDGER_CORE=python
# pins every public fn onto this surface (the dichotomy both-pins law); the
# default path delegates to the Rust core through ep_loader.
def _py_compute_hash(prev_hash: str, entry: dict) -> str:
payload = json.dumps(entry, sort_keys=True).encode()
return ep_loader.compute_hash(prev_hash, payload.decode(), SECRET_KEY)
msg = (prev_hash + payload.decode()).encode()
return hmac.new(SECRET_KEY.encode(), msg, hashlib.sha256).hexdigest()
def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
"""Append an entry to the promotion ledger.
Returns the ledger entry with chain hash.
"""
def _py_append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
# Read last hash
prev_hash = "genesis"
if os.path.exists(LEDGER_FILE):
@@ -71,10 +81,8 @@ def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
"prev_hash": prev_hash,
}
# Compute chain hash
entry["hash"] = _compute_hash(prev_hash, entry)
entry["hash"] = _py_compute_hash(prev_hash, entry)
# Append to ledger
os.makedirs(os.path.dirname(LEDGER_FILE), exist_ok=True)
with open(LEDGER_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")
@@ -82,19 +90,42 @@ def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
return entry
def verify_chain() -> dict:
"""Verify the integrity of the entire ledger chain — RUST authority
(ep seam; Phase G slice 2 #2449 batch A). Same shape as the Python
vintage: {valid, entries, reason}. The empty-ledger guard stays
Python (the Rust seam reads the file; nothing to verify)."""
def _py_verify_chain() -> dict:
if not os.path.exists(LEDGER_FILE):
return {"valid": True, "entries": 0, "reason": "empty ledger"}
return ep_loader.verify_chain(str(LEDGER_FILE), SECRET_KEY)
with open(LEDGER_FILE, "r") as f:
lines = f.readlines()
prev_hash = "genesis"
for i, line in enumerate(lines):
try:
entry = json.loads(line)
except json.JSONDecodeError:
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i + 1}"}
if entry.get("prev_hash") != prev_hash:
return {
"valid": False,
"entries": i,
"reason": f"chain broken at line {i + 1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}",
}
stored_hash = entry.get("hash")
computed = _py_compute_hash(prev_hash, {k: v for k, v in entry.items() if k != "hash"})
if stored_hash != computed:
return {
"valid": False,
"entries": i,
"reason": f"HMAC mismatch at line {i + 1}: stored={stored_hash[:16]}..., computed={computed[:16]}...",
}
prev_hash = stored_hash
return {"valid": True, "entries": len(lines), "reason": "chain intact"}
def get_entries(limit: int = 50) -> List[dict]:
"""Get recent ledger entries."""
def _py_get_entries(limit: int = 50) -> List[dict]:
if not os.path.exists(LEDGER_FILE):
return []
@@ -110,9 +141,8 @@ def get_entries(limit: int = 50) -> List[dict]:
return entries
def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
"""Log a promotion decision."""
return append_entry(
def _py_log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
return _py_append_entry(
"promotion",
entry_id,
{
@@ -124,9 +154,8 @@ def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: i
)
def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
"""Log a pre-commit gate result."""
return append_entry(
def _py_log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
return _py_append_entry(
"gate",
entry_id,
{
@@ -136,9 +165,8 @@ def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
)
def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
"""Log a drift alert."""
return append_entry(
def _py_log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
return _py_append_entry(
"alert",
0,
{
@@ -149,6 +177,68 @@ def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
)
# ── public surface — Rust delegation with the python pin ─────────────────────
def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
"""Append an entry to the promotion ledger.
Returns the ledger entry with chain hash.
"""
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_append_entry(entry_type, entry_id, details)
return ep_loader.append_entry(
str(LEDGER_FILE), SECRET_KEY, entry_type, entry_id, json.dumps(details)
)
def verify_chain() -> dict:
"""Verify the integrity of the entire ledger chain — RUST authority
(ep seam; Phase G slice 2 #2449 batch A). Same shape as the Python
vintage: {valid, entries, reason}. The empty-ledger guard stays
Python (the Rust seam reads the file; nothing to verify)."""
if not os.path.exists(LEDGER_FILE):
return {"valid": True, "entries": 0, "reason": "empty ledger"}
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_verify_chain()
return ep_loader.verify_chain(str(LEDGER_FILE), SECRET_KEY)
def get_entries(limit: int = 50) -> List[dict]:
"""Get recent ledger entries."""
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_get_entries(limit)
return ep_loader.get_entries(str(LEDGER_FILE), limit)
def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
"""Log a promotion decision."""
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_log_promotion(entry_id, old_status, new_status, wash_count, reason)
return ep_loader.log_promotion(
str(LEDGER_FILE), SECRET_KEY, entry_id, old_status, new_status, wash_count, reason
)
def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
"""Log a pre-commit gate result."""
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_log_gate_result(entry_id, verdict, critics)
return ep_loader.log_gate_result(
str(LEDGER_FILE), SECRET_KEY, entry_id, verdict, critics
)
def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
"""Log a drift alert."""
if os.environ.get("RUNE_AUDIT_LEDGER_CORE") == "python":
return _py_log_drift_alert(alert_type, message, severity)
return ep_loader.log_drift_alert(
str(LEDGER_FILE), SECRET_KEY, alert_type, message, severity
)
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
-198
View File
@@ -1,198 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "audit_ledger"
version = "0.1.0"
dependencies = [
"hex",
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[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",
"serde_derive",
]
[[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.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "audit_ledger"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
hex = "0.4"
-117
View File
@@ -1,117 +0,0 @@
//! audit_ledger — HMAC-chained audit ledger (W6).
//! PARITY port of src/rune/audit_ledger.py.
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LedgerEntry {
pub entry_type: String,
pub entry_id: i64,
pub details: serde_json::Value,
pub timestamp: String,
pub prev_hash: String,
pub hash: String,
}
/// Compute HMAC chain hash (simplified: SHA-256 of prev_hash + payload)
pub fn compute_hash(prev_hash: &str, entry: &serde_json::Value) -> String {
let payload = serde_json::to_string(entry).unwrap_or_default();
let msg = format!("{}{}", prev_hash, payload);
let mut hasher = Sha256::new();
hasher.update(msg.as_bytes());
hex::encode(hasher.finalize())
}
pub fn append_entry(
entries: &mut Vec<LedgerEntry>,
entry_type: &str,
entry_id: i64,
details: serde_json::Value,
) -> LedgerEntry {
let prev_hash = entries
.last()
.map(|e| e.hash.clone())
.unwrap_or("genesis".to_string());
let mut entry = LedgerEntry {
entry_type: entry_type.to_string(),
entry_id,
details,
timestamp: format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
),
prev_hash,
hash: String::new(),
};
// Compute hash over the entry WITHOUT the hash field
let hash_input = serde_json::json!({
"entry_type": entry.entry_type,
"entry_id": entry.entry_id,
"details": entry.details,
"timestamp": entry.timestamp,
"prev_hash": entry.prev_hash,
});
entry.hash = compute_hash(&entry.prev_hash, &hash_input);
entries.push(entry.clone());
entry
}
pub fn verify_chain(entries: &[LedgerEntry]) -> (bool, usize) {
let mut prev_hash = "genesis".to_string();
for (i, entry) in entries.iter().enumerate() {
if entry.prev_hash != prev_hash {
return (false, i);
}
let hash_input = serde_json::json!({
"entry_type": entry.entry_type,
"entry_id": entry.entry_id,
"details": entry.details,
"timestamp": entry.timestamp,
"prev_hash": entry.prev_hash,
});
let expected = compute_hash(&prev_hash, &hash_input);
if entry.hash != expected {
return (false, i);
}
prev_hash = entry.hash.clone();
}
(true, entries.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_append_and_verify() {
let mut entries = Vec::new();
append_entry(
&mut entries,
"promotion",
1,
serde_json::json!({"status": "promoted"}),
);
append_entry(
&mut entries,
"gate_result",
2,
serde_json::json!({"verdict": "PASS"}),
);
let (valid, count) = verify_chain(&entries);
assert!(valid);
assert_eq!(count, 2);
}
#[test]
fn test_chain_tamper_detection() {
let mut entries = Vec::new();
append_entry(&mut entries, "test", 1, serde_json::json!({}));
let mut tampered = entries.clone();
tampered[0].entry_type = "TAMPERED".to_string();
let (valid, _) = verify_chain(&tampered);
assert!(!valid);
}
}
+95 -1
View File
@@ -12,6 +12,9 @@ Contract (frozen `plans/2026-10-03-phase-g-evidence-provenance-contract.md`):
* append_entry(ledger_path, key, entry_type, entry_id, details_json) -> dict
* verify_chain(ledger_path, key) -> dict {valid, entries, reason}
* get_entries(ledger_path, limit) -> list[dict]
* log_promotion(ledger_path, key, entry_id, old, new, wash, reason) -> dict
* log_gate_result(ledger_path, key, entry_id, verdict, critics) -> dict
* log_drift_alert(ledger_path, key, alert_type, message, severity) -> dict
* default_ledger_path() -> str
Isolation law: both implementations resolve $HOME at runtime — tests MUST pin
@@ -72,6 +75,32 @@ def _load() -> ctypes.CDLL:
ctypes.c_int, # limit
]
lib.ep_get_entries.restype = ctypes.c_void_p # char* (JSON array)
lib.ep_log_promotion.argtypes = [
ctypes.c_char_p, # ledger_path (nullable)
ctypes.c_char_p, # key (nullable)
ctypes.c_int64, # entry_id
ctypes.c_char_p, # old_status
ctypes.c_char_p, # new_status
ctypes.c_int64, # wash_count
ctypes.c_char_p, # reason
]
lib.ep_log_promotion.restype = ctypes.c_void_p # char* (entry JSON)
lib.ep_log_gate_result.argtypes = [
ctypes.c_char_p, # ledger_path (nullable)
ctypes.c_char_p, # key (nullable)
ctypes.c_int64, # entry_id
ctypes.c_char_p, # verdict
ctypes.c_char_p, # critics_json
]
lib.ep_log_gate_result.restype = ctypes.c_void_p # char* (entry JSON)
lib.ep_log_drift_alert.argtypes = [
ctypes.c_char_p, # ledger_path (nullable)
ctypes.c_char_p, # key (nullable)
ctypes.c_char_p, # alert_type
ctypes.c_char_p, # message
ctypes.c_char_p, # severity
]
lib.ep_log_drift_alert.restype = ctypes.c_void_p # char* (entry JSON)
lib.ep_default_ledger_path.restype = ctypes.c_void_p # char*
lib.ep_verify.argtypes = [
ctypes.c_char_p, # result_json
@@ -193,7 +222,8 @@ def verify_chain(ledger_path: str, key: str | None) -> dict:
def get_entries(ledger_path: str, limit: int) -> list:
"""Recent entries; limit <= 0 mirrors Python's lines[-limit:] (all lines)."""
"""Recent entries; limit follows Python's lines[-limit:] slice law
(0 → all; negative drops the front, e.g. -2 → lines[2:])."""
ptr = _load().ep_get_entries(
ledger_path.encode("utf-8"),
int(limit),
@@ -203,6 +233,70 @@ def get_entries(ledger_path: str, limit: int) -> list:
return json.loads(_take_string(ptr))
def log_promotion(
ledger_path: str,
key: str | None,
entry_id: int,
old_status: str,
new_status: str,
wash_count: int,
reason: str,
) -> dict:
"""log_promotion(): the promotion wrapper over append_entry."""
ptr = _load().ep_log_promotion(
ledger_path.encode("utf-8"),
key.encode("utf-8") if key is not None else None,
int(entry_id),
old_status.encode("utf-8"),
new_status.encode("utf-8"),
int(wash_count),
reason.encode("utf-8"),
)
if not ptr:
raise RuntimeError("ep_log_promotion returned null (bad args or IO error)")
return json.loads(_take_string(ptr))
def log_gate_result(
ledger_path: str,
key: str | None,
entry_id: int,
verdict: str,
critics: dict,
) -> dict:
"""log_gate_result(): the gate wrapper over append_entry."""
ptr = _load().ep_log_gate_result(
ledger_path.encode("utf-8"),
key.encode("utf-8") if key is not None else None,
int(entry_id),
verdict.encode("utf-8"),
json.dumps(critics).encode("utf-8"),
)
if not ptr:
raise RuntimeError("ep_log_gate_result returned null (bad args or IO error)")
return json.loads(_take_string(ptr))
def log_drift_alert(
ledger_path: str,
key: str | None,
alert_type: str,
message: str,
severity: str,
) -> dict:
"""log_drift_alert(): the alert wrapper (entry_id always 0)."""
ptr = _load().ep_log_drift_alert(
ledger_path.encode("utf-8"),
key.encode("utf-8") if key is not None else None,
alert_type.encode("utf-8"),
message.encode("utf-8"),
severity.encode("utf-8"),
)
if not ptr:
raise RuntimeError("ep_log_drift_alert returned null (bad args or IO error)")
return json.loads(_take_string(ptr))
def default_ledger_path() -> str:
"""Deployed-vintage default ledger path (Rust-side resolution)."""
ptr = _load().ep_default_ledger_path()
@@ -0,0 +1,673 @@
{
"frozen_now": "2026-10-14T12:00:00+00:00",
"cases": [
{
"fn": "verify_chain",
"args": [],
"kwargs": {},
"expected": {
"valid": true,
"entries": 0,
"reason": "empty ledger"
}
},
{
"fn": "get_entries",
"args": [],
"kwargs": {},
"expected": []
},
{
"fn": "append_entry",
"args": [
"promotion",
1,
{
"old_status": "PROVISIONAL",
"new_status": "VALIDATED",
"wash_count": 2,
"reason": "gate pass"
}
],
"kwargs": {},
"expected": {
"type": "promotion",
"entry_id": 1,
"details": {
"old_status": "PROVISIONAL",
"new_status": "VALIDATED",
"wash_count": 2,
"reason": "gate pass"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "genesis",
"hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0"
}
},
{
"fn": "append_entry",
"args": [
"gate",
2,
{
"verdict": "PASS",
"critics": {
"quality": 0.9,
"safety": true
}
}
],
"kwargs": {},
"expected": {
"type": "gate",
"entry_id": 2,
"details": {
"verdict": "PASS",
"critics": {
"quality": 0.9,
"safety": true
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0",
"hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25"
}
},
{
"fn": "log_promotion",
"args": [
3,
"WASHABLE",
"ROCK",
4,
"stabilized after 4 washes"
],
"kwargs": {},
"expected": {
"type": "promotion",
"entry_id": 3,
"details": {
"old_status": "WASHABLE",
"new_status": "ROCK",
"wash_count": 4,
"reason": "stabilized after 4 washes"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25",
"hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17"
}
},
{
"fn": "log_gate_result",
"args": [
4,
"DEGRADE",
{
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
],
"kwargs": {},
"expected": {
"type": "gate",
"entry_id": 4,
"details": {
"verdict": "DEGRADE",
"critics": {
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17",
"hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4"
}
},
{
"fn": "log_drift_alert",
"args": [
"severe_shift",
"drift exceeded band ±3σ",
"high"
],
"kwargs": {},
"expected": {
"type": "alert",
"entry_id": 0,
"details": {
"alert_type": "severe_shift",
"message": "drift exceeded band ±3σ",
"severity": "high"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4",
"hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f"
}
},
{
"fn": "append_entry",
"args": [
"note",
5,
{
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
}
],
"kwargs": {},
"expected": {
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
},
{
"fn": "get_entries",
"args": [],
"kwargs": {},
"expected": [
{
"type": "promotion",
"entry_id": 1,
"details": {
"old_status": "PROVISIONAL",
"new_status": "VALIDATED",
"wash_count": 2,
"reason": "gate pass"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "genesis",
"hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0"
},
{
"type": "gate",
"entry_id": 2,
"details": {
"verdict": "PASS",
"critics": {
"quality": 0.9,
"safety": true
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0",
"hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25"
},
{
"type": "promotion",
"entry_id": 3,
"details": {
"old_status": "WASHABLE",
"new_status": "ROCK",
"wash_count": 4,
"reason": "stabilized after 4 washes"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25",
"hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17"
},
{
"type": "gate",
"entry_id": 4,
"details": {
"verdict": "DEGRADE",
"critics": {
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17",
"hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4"
},
{
"type": "alert",
"entry_id": 0,
"details": {
"alert_type": "severe_shift",
"message": "drift exceeded band ±3σ",
"severity": "high"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4",
"hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f"
},
{
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
]
},
{
"fn": "get_entries",
"args": [
3
],
"kwargs": {},
"expected": [
{
"type": "gate",
"entry_id": 4,
"details": {
"verdict": "DEGRADE",
"critics": {
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17",
"hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4"
},
{
"type": "alert",
"entry_id": 0,
"details": {
"alert_type": "severe_shift",
"message": "drift exceeded band ±3σ",
"severity": "high"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4",
"hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f"
},
{
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
]
},
{
"fn": "get_entries",
"args": [
1
],
"kwargs": {},
"expected": [
{
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
]
},
{
"fn": "get_entries",
"args": [
0
],
"kwargs": {},
"expected": [
{
"type": "promotion",
"entry_id": 1,
"details": {
"old_status": "PROVISIONAL",
"new_status": "VALIDATED",
"wash_count": 2,
"reason": "gate pass"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "genesis",
"hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0"
},
{
"type": "gate",
"entry_id": 2,
"details": {
"verdict": "PASS",
"critics": {
"quality": 0.9,
"safety": true
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0",
"hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25"
},
{
"type": "promotion",
"entry_id": 3,
"details": {
"old_status": "WASHABLE",
"new_status": "ROCK",
"wash_count": 4,
"reason": "stabilized after 4 washes"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25",
"hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17"
},
{
"type": "gate",
"entry_id": 4,
"details": {
"verdict": "DEGRADE",
"critics": {
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17",
"hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4"
},
{
"type": "alert",
"entry_id": 0,
"details": {
"alert_type": "severe_shift",
"message": "drift exceeded band ±3σ",
"severity": "high"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4",
"hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f"
},
{
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
]
},
{
"fn": "get_entries",
"args": [
-2
],
"kwargs": {},
"expected": [
{
"type": "promotion",
"entry_id": 3,
"details": {
"old_status": "WASHABLE",
"new_status": "ROCK",
"wash_count": 4,
"reason": "stabilized after 4 washes"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25",
"hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17"
},
{
"type": "gate",
"entry_id": 4,
"details": {
"verdict": "DEGRADE",
"critics": {
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17",
"hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4"
},
{
"type": "alert",
"entry_id": 0,
"details": {
"alert_type": "severe_shift",
"message": "drift exceeded band ±3σ",
"severity": "high"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4",
"hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f"
},
{
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
]
},
{
"fn": "get_entries",
"args": [
100
],
"kwargs": {},
"expected": [
{
"type": "promotion",
"entry_id": 1,
"details": {
"old_status": "PROVISIONAL",
"new_status": "VALIDATED",
"wash_count": 2,
"reason": "gate pass"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "genesis",
"hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0"
},
{
"type": "gate",
"entry_id": 2,
"details": {
"verdict": "PASS",
"critics": {
"quality": 0.9,
"safety": true
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b4261cc2797132808c7b1da29deca74ca38de7d0c9f1d440e47eb627e206c4f0",
"hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25"
},
{
"type": "promotion",
"entry_id": 3,
"details": {
"old_status": "WASHABLE",
"new_status": "ROCK",
"wash_count": 4,
"reason": "stabilized after 4 washes"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "b3995392e209ba786b33a2e07835883b2b14001eb1132d350e4d319adf828d25",
"hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17"
},
{
"type": "gate",
"entry_id": 4,
"details": {
"verdict": "DEGRADE",
"critics": {
"quality": 0.55,
"notes": [
"weak evidence",
"unicode 漢字"
]
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "a657fb7f90dc22928b2fc8988dbb5d556cf63cef54df0daf0235b1287be26a17",
"hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4"
},
{
"type": "alert",
"entry_id": 0,
"details": {
"alert_type": "severe_shift",
"message": "drift exceeded band ±3σ",
"severity": "high"
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "82c2f1dae267320a3e4259cb3016878d8d665a2fa4950dc4def869d18b1095b4",
"hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f"
},
{
"type": "note",
"entry_id": 5,
"details": {
"mixed": [
1,
2.5,
null,
true,
"str"
],
"nested": {
"b": 2,
"a": 1
}
},
"timestamp": "2026-10-14T12:00:00+00:00",
"prev_hash": "e152553c4034f2f0fa657048f563aceb0492bfffdf4acf9fe4b5a0dcdcc91b9f",
"hash": "873d07a6acc9380ece5df50cf84b2b610dc71af48183417bf25e4d94114e2d07"
}
]
},
{
"fn": "verify_chain",
"args": [],
"kwargs": {},
"expected": {
"valid": true,
"entries": 6,
"reason": "chain intact"
}
},
{
"fn": "_compute_hash",
"args": [
"genesis",
{
"b": 2,
"a": {
"z": 1,
"y": [
true,
null
]
}
}
],
"kwargs": {},
"expected": "3c0a5481dd625392669f2faf653bfb451d42fe2117a4d9216f97ecc73c6f297a"
}
]
}
@@ -0,0 +1,151 @@
//! audit_ledger gap-close battery — the frozen-truth replay (dichotomy #2479).
//!
//! frozen_audit_ledger_truth.json: 16 cases in the freeze_truth harness
//! envelope format, frozen via scripts/gen_audit_ledger_gap_truth.py against
//! the pure _py_* vintage (RUNE_AUDIT_LEDGER_CORE=python — delegation-proof
//! by construction). Re-freezable byte-identical via
//! `PYTHONPATH=src:scripts/al_freeze_seams python3 scripts/freeze_truth.py
//! --verify src/rust/evidence_provenance/frozen_audit_ledger_truth.json
//! --module rune.audit_ledger`. Embedded VERBATIM — never transcribed.
//!
//! The replay drives the RUST kernels through the same sequential chain
//! (prev_hash = last line's hash — file state evolves across cases) under
//! the envelope's frozen clock (RUNE_FROZEN_NOW seam) and the drift key,
//! then asserts the ledger FILE bytes equal the pyjson serialization of
//! every expected append — the byte law (separators, key order,
//! ensure_ascii) that structural == cannot see.
use crate::pyjson::{self, Json};
use crate::{
append_entry, compute_hash, get_entries, log_drift_alert, log_gate_result, log_promotion,
verify_chain, KEY_DRIFT,
};
use std::env;
use std::fs;
use std::path::PathBuf;
const TRUTH: &str = include_str!("../frozen_audit_ledger_truth.json");
fn envelope() -> (String, Vec<Json>) {
let parsed = pyjson::parse(TRUTH).expect("embedded truth must parse");
let frozen_now = match parsed.get("frozen_now") {
Some(Json::Str(s)) => s.clone(),
_ => panic!("truth envelope must carry frozen_now"),
};
let cases = match parsed.get("cases") {
Some(Json::Arr(items)) => items.clone(),
_ => panic!("truth envelope must carry a cases array"),
};
(frozen_now, cases)
}
fn args_of(case: &Json) -> Vec<Json> {
match case.get("args") {
Some(Json::Arr(items)) => items.clone(),
_ => vec![],
}
}
fn as_str(v: &Json) -> String {
match v {
Json::Str(s) => s.clone(),
other => pyjson::dumps(other),
}
}
fn as_i64(v: &Json) -> i64 {
match v {
Json::Num(n) => *n as i64,
other => panic!("expected a number, got {}", pyjson::dumps(other)),
}
}
fn is_append_fn(name: &str) -> bool {
matches!(
name,
"append_entry" | "log_promotion" | "log_gate_result" | "log_drift_alert"
)
}
#[test]
fn frozen_truth_replay() {
let (frozen_now, cases) = envelope();
// Clock + key determinism for the whole replay.
env::set_var("RUNE_FROZEN_NOW", &frozen_now);
env::remove_var("AUDIT_LEDGER_KEY");
let dir = std::env::temp_dir().join(format!("al-battery-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("battery tempdir");
let ledger: PathBuf = dir.join("promotion_ledger.jsonl");
for case in &cases {
let name = as_str(case.get("fn").unwrap_or(&Json::Null));
let args = args_of(case);
let expected = case.get("expected").cloned().unwrap_or(Json::Null);
let got = match name.as_str() {
"verify_chain" => verify_chain(&ledger, KEY_DRIFT),
"get_entries" => {
let limit = args.first().map(as_i64).unwrap_or(50);
Json::Arr(get_entries(&ledger, limit))
}
"append_entry" => append_entry(
&ledger,
KEY_DRIFT,
&as_str(&args[0]),
as_i64(&args[1]),
args[2].clone(),
)
.expect("append_entry"),
"log_promotion" => log_promotion(
&ledger,
KEY_DRIFT,
as_i64(&args[0]),
&as_str(&args[1]),
&as_str(&args[2]),
as_i64(&args[3]),
&as_str(&args[4]),
)
.expect("log_promotion"),
"log_gate_result" => log_gate_result(
&ledger,
KEY_DRIFT,
as_i64(&args[0]),
&as_str(&args[1]),
&args[2],
)
.expect("log_gate_result"),
"log_drift_alert" => log_drift_alert(
&ledger,
KEY_DRIFT,
&as_str(&args[0]),
&as_str(&args[1]),
&as_str(&args[2]),
)
.expect("log_drift_alert"),
"_compute_hash" => Json::Str(compute_hash(&as_str(&args[0]), &args[1], KEY_DRIFT)),
other => panic!("unknown frozen fn: {other}"),
};
assert_eq!(
got,
expected,
"case {name} args={}",
pyjson::dumps(&Json::Arr(args))
);
}
// Byte law: the kernel-written file is exactly the pyjson serialization
// of every expected append entry, newline-terminated, in case order.
let mut want = String::new();
for case in &cases {
let name = as_str(case.get("fn").unwrap_or(&Json::Null));
if is_append_fn(&name) {
want.push_str(&pyjson::dumps(case.get("expected").unwrap_or(&Json::Null)));
want.push('\n');
}
}
let file = fs::read_to_string(&ledger).expect("ledger written");
assert_eq!(file, want, "ledger file bytes must match expected dumps");
fs::remove_dir_all(&dir).ok();
}
@@ -32,7 +32,7 @@ use crate::pyjson::Json;
pub fn habit_reinforce(row: &Json, outcome: &str) -> Result<Json, String> {
let g = |k: &str, d: f64| -> f64 {
match row.get(k) {
Some(Json::Num(n)) => *n,
Some(Json::Num(n)) | Some(Json::Float(n)) => *n,
Some(Json::Bool(b)) => {
if *b {
1.0
@@ -81,7 +81,7 @@ pub fn habit_reinforce(row: &Json, outcome: &str) -> Result<Json, String> {
pub fn correction_reinforce(row: &Json, outcome: &str) -> Result<Json, String> {
let g = |k: &str, d: f64| -> f64 {
match row.get(k) {
Some(Json::Num(n)) => *n,
Some(Json::Num(n)) | Some(Json::Float(n)) => *n,
_ => d,
}
};
+160 -12
View File
@@ -85,6 +85,19 @@ fn civil_from_days(z: i64) -> (i64, u32, u32) {
}
pub fn now_iso_utc() -> String {
// Clock seam (PP_ACTIVE_THREADS pattern): test/parity rigs pin the wall
// clock via RUNE_FROZEN_NOW — the Python-side frozen datetime renders the
// same instant, so ledger lines hash byte-identically across langs. The
// value is returned verbatim; a trailing 'Z' normalizes to the +00:00
// form .isoformat() emits.
if let Ok(frozen) = env::var("RUNE_FROZEN_NOW") {
if !frozen.is_empty() {
if let Some(stem) = frozen.strip_suffix('Z') {
return format!("{}+00:00", stem);
}
return frozen;
}
}
let dur = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
@@ -279,28 +292,87 @@ pub fn verify_chain(ledger_path: &PathBuf, key: &str) -> Json {
])
}
/// Python get_entries(limit=50): last `limit` lines, bad JSON lines skipped.
/// Python slice lines[-limit:] with limit <= 0 means ALL lines ([-0:] == [0:]).
/// Python get_entries(limit=50): lines[-limit:], bad JSON lines skipped.
/// Python slice law: limit > 0 → last `limit` lines; limit == 0 → lines[0:]
/// (ALL — the [-0:] trap); limit < 0 → lines[|limit|:] (drop the front,
/// empty when |limit| >= len) — NOT "all lines" (frozen-truth-proven).
pub fn get_entries(ledger_path: &PathBuf, limit: i64) -> Vec<Json> {
if !ledger_path.exists() {
return Vec::new();
}
let lines = read_lines(ledger_path);
#[allow(clippy::if_same_then_else)]
// pinned: the branches mirror distinct oracle paths that happen to coincide
let slice: &[String] = if limit <= 0 {
&lines[..]
} else if (limit as usize) >= lines.len() {
&lines[..]
let n = lines.len() as i64;
let start: usize = if limit < 0 {
let drop = -limit;
if drop >= n {
return Vec::new();
}
drop as usize
} else if limit == 0 || limit >= n {
0
} else {
&lines[lines.len() - limit as usize..]
(n - limit) as usize
};
slice
lines[start..]
.iter()
.filter_map(|l| pyjson::parse(l.trim()).ok())
.collect()
}
// ── audit_ledger convenience wrappers (log_* gap-close, dichotomy #2479) ─────
/// Python log_promotion: append_entry("promotion", entry_id, details) with
/// details insertion order old_status, new_status, wash_count, reason.
pub fn log_promotion(
ledger_path: &PathBuf,
key: &str,
entry_id: i64,
old_status: &str,
new_status: &str,
wash_count: i64,
reason: &str,
) -> Result<Json, String> {
let details = Json::Obj(vec![
("old_status".to_string(), Json::Str(old_status.to_string())),
("new_status".to_string(), Json::Str(new_status.to_string())),
("wash_count".to_string(), Json::Num(wash_count as f64)),
("reason".to_string(), Json::Str(reason.to_string())),
]);
append_entry(ledger_path, key, "promotion", entry_id, details)
}
/// Python log_gate_result: append_entry("gate", entry_id, {verdict, critics}).
pub fn log_gate_result(
ledger_path: &PathBuf,
key: &str,
entry_id: i64,
verdict: &str,
critics: &Json,
) -> Result<Json, String> {
let details = Json::Obj(vec![
("verdict".to_string(), Json::Str(verdict.to_string())),
("critics".to_string(), critics.clone()),
]);
append_entry(ledger_path, key, "gate", entry_id, details)
}
/// Python log_drift_alert: append_entry("alert", 0, {alert_type, message,
/// severity}) — entry_id is always 0 (the alert is identified by its type).
pub fn log_drift_alert(
ledger_path: &PathBuf,
key: &str,
alert_type: &str,
message: &str,
severity: &str,
) -> Result<Json, String> {
let details = Json::Obj(vec![
("alert_type".to_string(), Json::Str(alert_type.to_string())),
("message".to_string(), Json::Str(message.to_string())),
("severity".to_string(), Json::Str(severity.to_string())),
]);
append_entry(ledger_path, key, "alert", 0, details)
}
// ── FFI (cdylib) — mirrors slice-1 conventions: extern "C", no unsafe blocks
// beyond the FFI boundary itself, CString round-trip with ep_free_string ──
@@ -394,8 +466,8 @@ pub extern "C" fn ep_verify_chain(ledger_path: *const c_char, key: *const c_char
ret_string(pyjson::dumps(&result))
}
/// Recent entries as a JSON array; limit <= 0 mirrors Python's lines[-limit:]
/// (all lines). Nullable path → default resolution.
/// Recent entries as a JSON array; limit follows Python's lines[-limit:]
/// slice law (see get_entries). Nullable path → default resolution.
#[no_mangle]
pub extern "C" fn ep_get_entries(ledger_path: *const c_char, limit: c_int) -> *mut c_char {
let path = resolve_ledger_path(cstr_opt(ledger_path).as_deref());
@@ -413,6 +485,79 @@ pub extern "C" fn ep_default_ledger_path() -> *mut c_char {
}
}
/// log_promotion(entry_id, old_status, new_status, wash_count, reason) —
/// the promotion wrapper over append_entry. All path/key args nullable →
/// default resolution.
#[no_mangle]
pub extern "C" fn ep_log_promotion(
ledger_path: *const c_char,
key: *const c_char,
entry_id: i64,
old_status: *const c_char,
new_status: *const c_char,
wash_count: i64,
reason: *const c_char,
) -> *mut c_char {
let (old, new, why) = match (cstr_opt(old_status), cstr_opt(new_status), cstr_opt(reason)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => return std::ptr::null_mut(),
};
let path = resolve_ledger_path(cstr_opt(ledger_path).as_deref());
let k = resolve_key(cstr_opt(key).as_deref());
match log_promotion(&path, &k, entry_id, &old, &new, wash_count, &why) {
Ok(entry) => ret_string(pyjson::dumps(&entry)),
Err(_) => std::ptr::null_mut(),
}
}
/// log_gate_result(entry_id, verdict, critics) — the gate wrapper.
/// critics_json must parse (any JSON value; the vintage passes a dict).
#[no_mangle]
pub extern "C" fn ep_log_gate_result(
ledger_path: *const c_char,
key: *const c_char,
entry_id: i64,
verdict: *const c_char,
critics_json: *const c_char,
) -> *mut c_char {
let (verdict_s, critics_raw) = match (cstr_opt(verdict), cstr_opt(critics_json)) {
(Some(a), Some(b)) => (a, b),
_ => return std::ptr::null_mut(),
};
let critics = match pyjson::parse(critics_raw.trim()) {
Ok(c) => c,
Err(_) => return std::ptr::null_mut(),
};
let path = resolve_ledger_path(cstr_opt(ledger_path).as_deref());
let k = resolve_key(cstr_opt(key).as_deref());
match log_gate_result(&path, &k, entry_id, &verdict_s, &critics) {
Ok(entry) => ret_string(pyjson::dumps(&entry)),
Err(_) => std::ptr::null_mut(),
}
}
/// log_drift_alert(alert_type, message, severity) — the alert wrapper
/// (entry_id always 0).
#[no_mangle]
pub extern "C" fn ep_log_drift_alert(
ledger_path: *const c_char,
key: *const c_char,
alert_type: *const c_char,
message: *const c_char,
severity: *const c_char,
) -> *mut c_char {
let (atype, msg, sev) = match (cstr_opt(alert_type), cstr_opt(message), cstr_opt(severity)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => return std::ptr::null_mut(),
};
let path = resolve_ledger_path(cstr_opt(ledger_path).as_deref());
let k = resolve_key(cstr_opt(key).as_deref());
match log_drift_alert(&path, &k, &atype, &msg, &sev) {
Ok(entry) => ret_string(pyjson::dumps(&entry)),
Err(_) => std::ptr::null_mut(),
}
}
// ── port 2 (verification_layer) FFI ──────────────────────────────────────────
/// Verify a result with the named verifier; returns the VerificationResult
@@ -909,6 +1054,9 @@ pub extern "C" fn ep_ca_evidence_terms(terms_json: *const c_char) -> *mut c_char
ret_string(pyjson::dumps(&claims::evidence_terms(&terms)))
}
#[cfg(test)]
mod audit_battery;
#[cfg(test)]
mod claim_gate_battery {
// #2479 RUST batch 33: the claim-audit gates, probed through the FFI
+14 -3
View File
@@ -250,9 +250,20 @@ impl<'a> Parser<'a> {
}
}
let s = std::str::from_utf8(&self.b[start..self.i]).map_err(|_| "bad number")?;
s.parse::<f64>()
.map(Json::Num)
.map_err(|_| format!("bad number '{}'", s))
// json.loads law: a number with '.' or an exponent is a FLOAT —
// integral floats must survive a parse→dumps round-trip as "0.0",
// never collapse to "0" (Num's integer law) — the HMAC chain hashes
// the serialized bytes, so the distinction is load-bearing
// (frozen-truth + fuzz proven, dichotomy #2479).
if s.contains('.') || s.contains('e') || s.contains('E') {
s.parse::<f64>()
.map(Json::Float)
.map_err(|_| format!("bad number '{}'", s))
} else {
s.parse::<f64>()
.map(Json::Num)
.map_err(|_| format!("bad number '{}'", s))
}
}
}
@@ -108,7 +108,7 @@ pub fn verify_exit_code(result: &Json) -> Json {
let raw = result.get("exit_code").cloned().unwrap_or(Json::Num(0.0));
// passed: Python `exit_code == 0` — false→0 equality holds; true/str don't.
let passed = match &raw {
Json::Num(n) => *n == 0.0,
Json::Num(n) | Json::Float(n) => *n == 0.0,
Json::Bool(b) => !*b,
_ => false,
};
+1
View File
@@ -0,0 +1 @@
1
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
1
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
parity_exit=0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+74
View File
@@ -0,0 +1,74 @@
# audit_ledger gap-close verification summary (dichotomy wave #2479, worktree rune-wave-di)
Module: `src/rune/audit_ledger.py` — audit 2/8 PARTIAL → **8/8 FULL (RUST)**.
Kernels live in the `evidence_provenance` crate (live ep seam — the oracle
already delegated hash+verify there; audit alias `"audit_ledger":
["evidence_provenance"]` per the drift_compression precedent). The W6-era
`src/rust/audit_ledger` stub (SHA-256 concat, wrong field names, never the
live chain) is DELETED — one chain, one implementation.
## Gates (all exit 0; logs in verification/, `*.log` gitignored by repo convention)
| Gate | Evidence | Result |
|---|---|---|
| cargo build --release | al-cargo-build.{log,exit} | ok |
| cargo clippy --all-targets -D warnings | al-cargo-clippy.{log,exit} | clean |
| cargo fmt --check | al-cargo-fmt.{log,exit} | clean |
| cargo test (audit_battery 16-case frozen replay + file-byte law) | al-cargo-test.{log,exit} | 6/6 |
| freeze_truth --verify (×2 + post-change rerun) | al-freeze-verify.{log,exit} | 16 cases byte-identical |
| live parity (scripts/check_audit_ledger_parity.py) | al-parity.{log,exit} | **360/360** |
| pytest switch+shims, live pin | al-pytest-live.{log,exit} | 11/11 |
| pytest switch+shims, RUNE_AUDIT_LEDGER_CORE=python | al-pytest-pin.{log,exit} | 11/11 |
| ruff (changed py files) | al-ruff.{log,exit} | clean |
| ep crate canary | al-canary.{log,exit} | 66/0 |
| canary_audit_ledger (isolated battery mode), live + pin | al-canary-ledger-{live,pin}.{log,exit} | 74/0 both |
| dichotomy audit regen | al-audit.{log,exit} | audit_ledger 8/8 FULL; 107/192 modules FULL |
## Parity composition (360/360)
facade pinned 16/16 · facade live 16/16 · kernel FFI 16/16 · file-byte
parity EQUAL (pinned-python vs live-Rust ledger.jsonl under the same
RUNE_FROZEN_NOW clock) · 4 exact seams pinned==live (junk-last-line→genesis,
hashless-last-line→genesis, tamper→HMAC-mismatch reason, prev-link→
chain-broken reason) · missing-hash seam asserted per-side (pinned CRASHES
TypeError = vintage law; live reports `stored=None...` = the documented
crash-superset divergence in ep lib.rs) · fuzz seed 2479 300/300 rounds
(entries + get_entries + verify + FILE BYTES, pinned/live/kernel) · CLI
parity 5/5 (python -m rune.audit_ledger pinned vs Rust bin: verify, list,
list --limit 1, log-promotion byte-equal stdout+exit; no-args exit 1).
## Defects found & fixed (fix-first, frozen-proof)
1. **ep `get_entries` negative-limit slice defect** (pre-existing): Python
`lines[-limit:]` with limit<0 is `lines[|limit|:]` (drop the front), not
"all lines" — the kernel treated every limit<=0 as all. Frozen case
`get_entries(-2)` caught it; kernel + docstrings corrected.
2. **pyjson integral-float parse defect** (pre-existing, chain-breaking):
the parser mapped EVERY number to `Json::Num`, so `"0.0"` parsed→dumped
as `"0"` — any Python-written entry carrying an integral float would
HMAC-verify FAILING against the Rust re-serialization (payload bytes
differ). Parser now classifies '.'/exponent numbers as `Json::Float`
(json.loads law). Fuzz round 1 caught it; habitmath.rs count/confidence
extraction arms widened to `Num|Float` (proven by
test_behavior_decision_math, previously silently defaulting confidence).
3. verification.rs verify_exit_code: defensive `Num|Float` arm (float
exit codes are pathological but Python `0.0 == 0` is True).
## Documented divergences (not bugs)
- Missing `hash` key on a file line: the vintage CRASHES
(`stored_hash[:16]` on None → TypeError); Rust returns
`{"valid": false, ..., "reason": "HMAC mismatch ... stored=None..."}` —
the superset-of-a-crash law already documented in ep lib.rs; asserted
per-side in the parity script.
- argparse `--help` text vs the Rust bin's mirrored HELP const: exit codes
and subcommand outputs are byte-compared; help text is out of byte scope.
## Pre-existing, out of scope (evidence)
- `parity_verification.py` (crate-local probe, port 2) fails at its CLI
section on PRISTINE master too (verified on the main checkout with its
Oct 6 .so): it subprocesses `verification_layer.py` as a script, but the
module carries no `__main__` CLI — empty stdout → JSONDecodeError. Not
touched by this port (all four OTHER crate parity probes green: 13/0,
17/0, 24/0, 16/0).