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:
1 parent
1e3967a9f0
commit
29989a5b27
31 files changed
+1901
-379
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Generated
-198
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
parity_exit=0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user