feat(drift_resume): port to FULL Rust — dichotomy wave
parity: every public function of drift_resume ported branch-for-branch — classify_drift (the 'matches is True' / 'matches is None' IDENTITY checks — truthy non-True falls through to reality-differs; missing key and explicit null both mean unverifiable), the stale_ok/user_locked truthiness lattice, Python str() evidence renderings (str(None)='None', str(True)='True', ints bare) with dict.get defaults applying ONLY on a missing key (present-empty renders verbatim, present-null renders 'None'); reconcile (observer stays in the Python facade — the kernel receives the observations; blocking-class filter preserves drift order, resume = no blocking); detect_structural_drift (§18H: component filter, the ext→identities falsy-source or-chain, identities.user_locked 'is True' lock — truthy non-True is NOT locked, the exists check treats a DIRECTORY source as present, moved-twin = first basename match over the facade's sorted relative-path listing — unique-basename scenarios make the oracle's rglob-first-match order-independent, evidence + reconciliation strings exact). Replaces the old 11-line resume_state stub crate (no consumers). Boundary law: the observer callable and the filesystem walk stay in the facade; the kernel is a pure function of their results. dr_loader mirrors dc_loader (doc-envelope FFI, cs_free_string, DR_RUST_LIB override); facade delegation with RUNE_DR_CORE=python pin. Battery: 3 tests replay 35 frozen-oracle cases VERBATIM (include_str!). Live parity 35/35 facade + 35/35 kernel; clippy 0; test_drift_resume + the §18H held-out suites green live AND pinned (the one pre-existing failure on master, evidence_provenance ep_loader FileNotFoundError, is unrelated to this module).
This commit is contained in:
1 parent
7b86d15e67
commit
29ce279702
8 files changed
+2244
-21
No files matched your search
@@ -0,0 +1,473 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_drift_resume_truth.py — freeze/replay parity truth for drift_resume
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/drift_resume.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/drift_resume/src/frozen_dr_resume_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle functions delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_DR_CORE=python BEFORE any import.
|
||||
Replay leaves it UNSET so delegation is live (the real parity claim);
|
||||
the script aborts replay if the Rust core is not built.
|
||||
|
||||
Boundary law: the observer callable (reconcile) and the FILESYSTEM walk
|
||||
(detect_structural_drift: exists + the moved-twin rglob) stay in the
|
||||
Python facade — the kernel is a pure function of their results. Kernel
|
||||
inputs:
|
||||
* dr_classify_drift {claim, observation} → {record}
|
||||
* dr_reconcile {claims, observations} → {resume, drifts, blocking}
|
||||
* dr_detect_structural_drift {entities, files, dirs} → {drifts}
|
||||
where files/dirs are the facade's SORTED relative listings of
|
||||
repo_root (deterministic superset of the oracle's rglob order;
|
||||
moved-twin scenarios use unique basenames so the oracle's
|
||||
first-rglob-match is order-independent).
|
||||
|
||||
Deep-copy law: every input is deep-copied before it reaches the oracle
|
||||
(the kernel must never observe mutation aliasing).
|
||||
"""
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "drift_resume", "src",
|
||||
"frozen_dr_resume_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
os.environ["RUNE_DR_CORE"] = "python" # poisoned-truth law (no-op pre-port,
|
||||
# load-bearing post-port: the freeze must record the PYTHON oracle's truth)
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import drift_resume as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
del os.environ["RUNE_DR_CORE"] # delegation live for the replay pass
|
||||
from rune import dr_loader # noqa: E402 (kernel-level replay side)
|
||||
if not dr_loader.available():
|
||||
print("REPLAY ABORT: Rust drift_resume core not built/available")
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# ── classify_drift ───────────────────────────────────────────────────────────
|
||||
|
||||
def _cd_case(rec, name, claim, observation):
|
||||
got = oracle_mod.classify_drift(copy.deepcopy(claim),
|
||||
copy.deepcopy(observation))
|
||||
rec(name, {"claim": claim, "observation": observation}, got,
|
||||
{"record": got})
|
||||
|
||||
|
||||
def c_cd_matches_true_stale_ok(rec):
|
||||
_cd_case(rec, "cd-matches-true-stale-ok-expected",
|
||||
{"claim": "green", "stale_ok": True},
|
||||
{"actual": "", "checked_by": "pytest", "matches": True})
|
||||
|
||||
|
||||
def c_cd_matches_true_confirmed(rec):
|
||||
_cd_case(rec, "cd-matches-true-confirmed",
|
||||
{"claim": "green"},
|
||||
{"actual": "", "checked_by": "pytest", "matches": True})
|
||||
|
||||
|
||||
def c_cd_matches_true_stale_falsy(rec):
|
||||
_cd_case(rec, "cd-matches-true-stale-falsy-confirmed",
|
||||
{"claim": "green", "stale_ok": False},
|
||||
{"actual": "", "checked_by": "pytest", "matches": True})
|
||||
|
||||
|
||||
def c_cd_matches_true_stale_truthy_int(rec):
|
||||
_cd_case(rec, "cd-matches-true-stale-truthy-int-expected",
|
||||
{"claim": "green", "stale_ok": 1},
|
||||
{"actual": "", "checked_by": "pytest", "matches": True})
|
||||
|
||||
|
||||
def c_cd_matches_none_unknown(rec):
|
||||
_cd_case(rec, "cd-matches-none-unknown",
|
||||
{"claim": "x"},
|
||||
{"actual": None, "checked_by": "pytest", "matches": None})
|
||||
|
||||
|
||||
def c_cd_matches_missing_unknown(rec):
|
||||
_cd_case(rec, "cd-matches-missing-unknown",
|
||||
{"claim": "x"},
|
||||
{"actual": None, "checked_by": "probe"})
|
||||
|
||||
|
||||
def c_cd_matches_false_user_locked(rec):
|
||||
_cd_case(rec, "cd-matches-false-user-locked-conflicting",
|
||||
{"claim": "layout", "user_locked": True},
|
||||
{"actual": "layout is Y", "checked_by": "ls",
|
||||
"matches": False})
|
||||
|
||||
|
||||
def c_cd_matches_false_material(rec):
|
||||
_cd_case(rec, "cd-matches-false-material",
|
||||
{"claim": "green"},
|
||||
{"actual": "2 tests fail", "checked_by": "pytest",
|
||||
"matches": False})
|
||||
|
||||
|
||||
def c_cd_material_missing_actual(rec):
|
||||
_cd_case(rec, "cd-material-actual-missing-empty",
|
||||
{"claim": "green"},
|
||||
{"checked_by": "pytest", "matches": False})
|
||||
|
||||
|
||||
def c_cd_material_actual_null(rec):
|
||||
# Python str(None) → "None" in the f-string
|
||||
_cd_case(rec, "cd-material-actual-null-str-none",
|
||||
{"claim": "green"},
|
||||
{"actual": None, "checked_by": "pytest", "matches": False})
|
||||
|
||||
|
||||
def c_cd_material_actual_int(rec):
|
||||
_cd_case(rec, "cd-material-actual-int",
|
||||
{"claim": "count"},
|
||||
{"actual": 3, "checked_by": "wc", "matches": False})
|
||||
|
||||
|
||||
def c_cd_unknown_checked_by_missing(rec):
|
||||
_cd_case(rec, "cd-unknown-checked-by-missing-qmark",
|
||||
{"claim": "x"},
|
||||
{"matches": None})
|
||||
|
||||
|
||||
def c_cd_unknown_checked_by_empty(rec):
|
||||
# present-but-empty checked_by renders verbatim (get default only on
|
||||
# a MISSING key)
|
||||
_cd_case(rec, "cd-unknown-checked-by-empty-string",
|
||||
{"claim": "x"},
|
||||
{"checked_by": "", "matches": None})
|
||||
|
||||
|
||||
def c_cd_claim_missing(rec):
|
||||
_cd_case(rec, "cd-claim-key-missing-empty",
|
||||
{},
|
||||
{"actual": "", "checked_by": "pytest", "matches": True})
|
||||
|
||||
|
||||
# ── reconcile ────────────────────────────────────────────────────────────────
|
||||
|
||||
def _rc_case(rec, name, claims, observer):
|
||||
claims_c = copy.deepcopy(claims)
|
||||
got = oracle_mod.reconcile(claims_c, observer)
|
||||
observations = [observer(c) for c in claims_c]
|
||||
rec(name, {"claims": claims_c, "observations": observations}, got, got)
|
||||
|
||||
|
||||
def c_rc_empty(rec):
|
||||
_rc_case(rec, "rc-empty-claims-resume", [], lambda c: {"matches": True})
|
||||
|
||||
|
||||
def c_rc_all_confirmed(rec):
|
||||
_rc_case(rec, "rc-all-confirmed-resume",
|
||||
[{"claim": "a"}, {"claim": "b"}],
|
||||
lambda c: {"actual": "", "checked_by": "pytest",
|
||||
"matches": True})
|
||||
|
||||
|
||||
def c_rc_material_blocks(rec):
|
||||
_rc_case(rec, "rc-material-blocks",
|
||||
[{"claim": "green"}],
|
||||
lambda c: {"actual": "2 tests fail", "checked_by": "pytest",
|
||||
"matches": False})
|
||||
|
||||
|
||||
def c_rc_unknown_blocks(rec):
|
||||
_rc_case(rec, "rc-unknown-blocks",
|
||||
[{"claim": "x"}],
|
||||
lambda c: {"actual": None, "checked_by": "probe",
|
||||
"matches": None})
|
||||
|
||||
|
||||
def c_rc_conflicting_blocks(rec):
|
||||
_rc_case(rec, "rc-conflicting-blocks",
|
||||
[{"claim": "layout", "user_locked": True}],
|
||||
lambda c: {"actual": "layout is Y", "checked_by": "ls",
|
||||
"matches": False})
|
||||
|
||||
|
||||
def c_rc_expected_not_blocking(rec):
|
||||
_rc_case(rec, "rc-expected-not-blocking",
|
||||
[{"claim": "green", "stale_ok": True}],
|
||||
lambda c: {"actual": "", "checked_by": "pytest",
|
||||
"matches": True})
|
||||
|
||||
|
||||
def c_rc_mixed_order(rec):
|
||||
_rc_case(rec, "rc-mixed-order-blocking-subset",
|
||||
[{"claim": "ok-1"}, {"claim": "bad"}, {"claim": "mystery"},
|
||||
{"claim": "ok-2", "stale_ok": True}],
|
||||
lambda c: ({"actual": "", "checked_by": "pytest",
|
||||
"matches": True}
|
||||
if c["claim"].startswith("ok")
|
||||
else ({"actual": None, "checked_by": "probe",
|
||||
"matches": None}
|
||||
if c["claim"] == "mystery"
|
||||
else {"actual": "reality differs",
|
||||
"checked_by": "pytest",
|
||||
"matches": False})))
|
||||
|
||||
|
||||
# ── detect_structural_drift ──────────────────────────────────────────────────
|
||||
|
||||
def _dsd_case(rec, name, entities, files_present, dirs_present=()):
|
||||
root = pathlib.Path(tempfile.mkdtemp(prefix="dsd_truth_"))
|
||||
for rel in files_present:
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text("# twin\n", encoding="utf-8")
|
||||
for rel in dirs_present:
|
||||
(root / rel).mkdir(parents=True, exist_ok=True)
|
||||
model = types.SimpleNamespace(doc={"entities": copy.deepcopy(entities)})
|
||||
got = oracle_mod.detect_structural_drift(model, root)
|
||||
files = sorted(str(p.relative_to(root))
|
||||
for p in root.rglob("*") if p.is_file())
|
||||
dirs = sorted(str(p.relative_to(root))
|
||||
for p in root.rglob("*") if p.is_dir())
|
||||
rec(name, {"entities": entities, "files": files, "dirs": dirs}, got,
|
||||
{"drifts": got})
|
||||
|
||||
|
||||
def c_dsd_source_present(rec):
|
||||
_dsd_case(rec, "dsd-source-present-no-drift",
|
||||
[{"id": "component:widget", "kind": "component",
|
||||
"ext": {"source": "src/widget.py"}}],
|
||||
["src/widget.py"])
|
||||
|
||||
|
||||
def c_dsd_non_component_skipped(rec):
|
||||
_dsd_case(rec, "dsd-non-component-skipped",
|
||||
[{"id": "capability:x", "kind": "capability",
|
||||
"ext": {"source": "src/missing.py"}}],
|
||||
[])
|
||||
|
||||
|
||||
def c_dsd_no_source_skipped(rec):
|
||||
_dsd_case(rec, "dsd-no-source-skipped",
|
||||
[{"id": "component:x", "kind": "component", "ext": {}}],
|
||||
[])
|
||||
|
||||
|
||||
def c_dsd_missing_no_twin(rec):
|
||||
_dsd_case(rec, "dsd-missing-source-absent",
|
||||
[{"id": "component:widget", "kind": "component",
|
||||
"ext": {"source": "src/widget.py"}}],
|
||||
["src/other.py"])
|
||||
|
||||
|
||||
def c_dsd_moved_twin(rec):
|
||||
_dsd_case(rec, "dsd-moved-twin-material",
|
||||
[{"id": "component:widget", "kind": "component",
|
||||
"ext": {"source": "src/widget.py"}}],
|
||||
["src/widgets/widget.py"])
|
||||
|
||||
|
||||
def c_dsd_moved_twin_locked(rec):
|
||||
_dsd_case(rec, "dsd-moved-twin-user-locked-conflicting",
|
||||
[{"id": "component:widget", "kind": "component",
|
||||
"ext": {"source": "src/widget.py"},
|
||||
"identities": {"user_locked": True}}],
|
||||
["src/widgets/widget.py"])
|
||||
|
||||
|
||||
def c_dsd_missing_locked(rec):
|
||||
_dsd_case(rec, "dsd-missing-source-user-locked-conflicting",
|
||||
[{"id": "component:widget", "kind": "component",
|
||||
"ext": {"source": "src/widget.py"},
|
||||
"identities": {"user_locked": True}}],
|
||||
[])
|
||||
|
||||
|
||||
def c_dsd_ext_empty_falls_to_identities(rec):
|
||||
# ext.source == "" is falsy → identities.source wins (the or-chain)
|
||||
_dsd_case(rec, "dsd-ext-source-empty-falls-to-identities",
|
||||
[{"id": "component:alpha", "kind": "component",
|
||||
"ext": {"source": ""},
|
||||
"identities": {"source": "src/alpha.py"}}],
|
||||
["src/alpha.py"])
|
||||
|
||||
|
||||
def c_dsd_identities_source(rec):
|
||||
_dsd_case(rec, "dsd-identities-source-missing-file",
|
||||
[{"id": "component:beta", "kind": "component",
|
||||
"identities": {"source": "src/beta.py"}}],
|
||||
[])
|
||||
|
||||
|
||||
def c_dsd_multiple_entities_order(rec):
|
||||
_dsd_case(rec, "dsd-multiple-entities-order-preserved",
|
||||
[{"id": "component:a", "kind": "component",
|
||||
"ext": {"source": "src/a.py"}},
|
||||
{"id": "component:b", "kind": "component",
|
||||
"ext": {"source": "src/b.py"}},
|
||||
{"id": "component:c", "kind": "component",
|
||||
"ext": {"source": "src/c.py"}}],
|
||||
["src/c.py"])
|
||||
|
||||
|
||||
def c_dsd_nested_moved(rec):
|
||||
_dsd_case(rec, "dsd-nested-moved-twin",
|
||||
[{"id": "component:deep", "kind": "component",
|
||||
"ext": {"source": "src/deep.py"}}],
|
||||
["a/b/c/deep.py"])
|
||||
|
||||
|
||||
def c_dsd_source_is_dir(rec):
|
||||
# a DIRECTORY at the source path satisfies expected.exists() — no drift
|
||||
_dsd_case(rec, "dsd-source-is-dir-no-drift",
|
||||
[{"id": "component:pkg", "kind": "component",
|
||||
"ext": {"source": "src/pkg"}}],
|
||||
[], ["src/pkg"])
|
||||
|
||||
|
||||
def c_dsd_ext_null(rec):
|
||||
_dsd_case(rec, "dsd-ext-null-identities-fallback",
|
||||
[{"id": "component:gamma", "kind": "component",
|
||||
"ext": None,
|
||||
"identities": {"source": "src/gamma.py"}}],
|
||||
["src/gamma.py"])
|
||||
|
||||
|
||||
def c_dsd_user_locked_truthy_non_bool(rec):
|
||||
# `is True` — a truthy non-True user_locked is NOT locked
|
||||
_dsd_case(rec, "dsd-user-locked-truthy-non-bool-material",
|
||||
[{"id": "component:widget", "kind": "component",
|
||||
"ext": {"source": "src/widget.py"},
|
||||
"identities": {"user_locked": "yes"}}],
|
||||
[])
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
CLASSIFY = [
|
||||
c_cd_matches_true_stale_ok, c_cd_matches_true_confirmed,
|
||||
c_cd_matches_true_stale_falsy, c_cd_matches_true_stale_truthy_int,
|
||||
c_cd_matches_none_unknown, c_cd_matches_missing_unknown,
|
||||
c_cd_matches_false_user_locked, c_cd_matches_false_material,
|
||||
c_cd_material_missing_actual, c_cd_material_actual_null,
|
||||
c_cd_material_actual_int, c_cd_unknown_checked_by_missing,
|
||||
c_cd_unknown_checked_by_empty, c_cd_claim_missing,
|
||||
]
|
||||
RECONCILE = [
|
||||
c_rc_empty, c_rc_all_confirmed, c_rc_material_blocks,
|
||||
c_rc_unknown_blocks, c_rc_conflicting_blocks, c_rc_expected_not_blocking,
|
||||
c_rc_mixed_order,
|
||||
]
|
||||
DETECT = [
|
||||
c_dsd_source_present, c_dsd_non_component_skipped, c_dsd_no_source_skipped,
|
||||
c_dsd_missing_no_twin, c_dsd_moved_twin, c_dsd_moved_twin_locked,
|
||||
c_dsd_missing_locked, c_dsd_ext_empty_falls_to_identities,
|
||||
c_dsd_identities_source, c_dsd_multiple_entities_order,
|
||||
c_dsd_nested_moved, c_dsd_source_is_dir, c_dsd_ext_null,
|
||||
c_dsd_user_locked_truthy_non_bool,
|
||||
]
|
||||
|
||||
GROUPS = [("classify_drift", CLASSIFY), ("reconcile", RECONCILE),
|
||||
("detect_structural_drift", DETECT)]
|
||||
|
||||
KERNEL = {
|
||||
"classify_drift": "dr_classify_drift",
|
||||
"reconcile": "dr_reconcile",
|
||||
"detect_structural_drift": "dr_detect_structural_drift",
|
||||
}
|
||||
|
||||
UNWRAP = {
|
||||
"classify_drift": lambda w: w["record"],
|
||||
"reconcile": lambda w: w,
|
||||
"detect_structural_drift": lambda w: w["drifts"],
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
return json.dumps(json.loads(json.dumps(x, default=str)), sort_keys=True)
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, oracle_ret, expected, _r=recs):
|
||||
entry = {"name": name, "input": inp, "return": oracle_ret,
|
||||
"expected": expected}
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ",
|
||||
json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:",
|
||||
json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_failures = sum(1 for v in groups.values()
|
||||
for c in v if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
checked = 0
|
||||
for group, cases in frozen.items():
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = getattr(dr_loader, group)(**copy.deepcopy(case["input"]))
|
||||
want = UNWRAP[group](case["expected"])
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {checked - failures}/{checked}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/drift_resume.py",
|
||||
"pin": "RUNE_DR_CORE=python",
|
||||
"note": "Frozen via scripts/gen_drift_resume_truth.py against the "
|
||||
"live Python oracle. The observer callable (reconcile) and "
|
||||
"the filesystem walk (detect_structural_drift: exists + "
|
||||
"moved-twin rglob) stay in the Python facade; the kernel is "
|
||||
"a pure function of their results (observations / sorted "
|
||||
"relative-path listings). Moved-twin scenarios use unique "
|
||||
"basenames so first-match semantics are order-independent. "
|
||||
"Battery embeds this VERBATIM.",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust drift_resume core (dichotomy wave, #2479
|
||||
tranche). Mirrors dc_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* dr_classify_drift({claim, observation}) -> {record}
|
||||
* dr_reconcile({claims, observations}) -> {resume, drifts, blocking}
|
||||
* dr_detect_structural_drift({entities, files, dirs}) -> {drifts}
|
||||
|
||||
Boundary law: the observer callable (reconcile — it runs the actual
|
||||
check: test command, file read, git query) and the FILESYSTEM walk
|
||||
(detect_structural_drift: expected.exists() + the moved-twin rglob) stay
|
||||
in the Python facade (drift_resume.py); the kernel is a pure function of
|
||||
their results. The facade passes the observations and SORTED
|
||||
relative-path listings of repo_root (files + dirs — a directory at the
|
||||
source path satisfies the oracle's expected.exists()).
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype
|
||||
(c_void_p for returned char* — the default c_int truncates 64-bit
|
||||
pointers). Strings are freed via capability_scoring's cs_free_string —
|
||||
the kernel exports NO free symbol of its own (duplicate exports break
|
||||
the cdylib link when sibling crates share one process).
|
||||
|
||||
Override the .so with DR_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "drift_resume"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("DR_RUST_LIB", _CRATE_DIR / "target" / "release" / "libdrift_resume.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"dr_classify_drift",
|
||||
"dr_reconcile",
|
||||
"dr_detect_structural_drift",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class DriftResumeCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust drift_resume core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise DriftResumeCoreUnavailable(
|
||||
f"Rust drift_resume core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def classify_drift(claim: dict, observation: dict) -> dict:
|
||||
return _call("dr_classify_drift",
|
||||
{"claim": claim, "observation": observation})["record"]
|
||||
|
||||
|
||||
def reconcile(claims: list, observations: list) -> dict:
|
||||
return _call("dr_reconcile",
|
||||
{"claims": claims, "observations": observations})
|
||||
|
||||
|
||||
def detect_structural_drift(entities: list, files: list, dirs: list) -> list:
|
||||
return _call("dr_detect_structural_drift",
|
||||
{"entities": entities, "files": files, "dirs": dirs})["drifts"]
|
||||
@@ -22,6 +22,7 @@ Reality wins unless the claim carries explicit user-locked authority
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
EXPECTED = "EXPECTED" # checkpoint older than reality; benign aging
|
||||
@@ -33,6 +34,23 @@ UNKNOWN = "UNKNOWN" # cannot decide — treated as blocking
|
||||
BLOCKING_CLASSES = (MATERIAL, CONFLICTING, UNKNOWN)
|
||||
|
||||
|
||||
def _dr_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_DR_CORE=python pins the
|
||||
Python fallback — parity harnesses). The observer callable (reconcile)
|
||||
and the FILESYSTEM walk (detect_structural_drift) stay here; the
|
||||
kernel is a pure function of their results."""
|
||||
if os.environ.get("RUNE_DR_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import dr_loader
|
||||
|
||||
if dr_loader.available():
|
||||
return dr_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def classify_drift(claim: dict, observation: dict) -> dict:
|
||||
"""Classify one claim-vs-observation pair. Deterministic:
|
||||
verification evidence beats prose; explicit beats inferred.
|
||||
@@ -42,6 +60,9 @@ def classify_drift(claim: dict, observation: dict) -> dict:
|
||||
observation: {"actual": str|None, "checked_by": str,
|
||||
"matches": bool|None}
|
||||
"""
|
||||
core = _dr_core()
|
||||
if core is not None:
|
||||
return core.classify_drift(claim, observation)
|
||||
if observation.get("matches") is True:
|
||||
return {"claim": claim.get("claim", ""),
|
||||
"class": EXPECTED if claim.get("stale_ok") else "CONFIRMED",
|
||||
@@ -67,6 +88,10 @@ def reconcile(claims: list[dict], observer: Callable[[dict], dict]) -> dict:
|
||||
Returns {"resume": bool, "drifts": [...], "blocking": [...]}. The
|
||||
resume decision: drift-aware, not blind — ANY blocking-class drift
|
||||
stops dependent work until reconciled."""
|
||||
core = _dr_core()
|
||||
if core is not None:
|
||||
observations = [observer(c) for c in claims]
|
||||
return core.reconcile(claims, observations)
|
||||
drifts = [classify_drift(c, observer(c)) for c in claims]
|
||||
blocking = [d for d in drifts if d["class"] in BLOCKING_CLASSES]
|
||||
return {"resume": not blocking, "drifts": drifts,
|
||||
@@ -84,6 +109,16 @@ def detect_structural_drift(model, repo_root) -> list[dict]:
|
||||
drift records in the classify_drift vocabulary; reality wins unless
|
||||
the claim is user-locked (authority == 'user' on the entity's
|
||||
evidence or identities.user_locked)."""
|
||||
core = _dr_core()
|
||||
if core is not None:
|
||||
from pathlib import Path as _P
|
||||
root = _P(repo_root)
|
||||
files = sorted(str(p.relative_to(root))
|
||||
for p in root.rglob("*") if p.is_file())
|
||||
dirs = sorted(str(p.relative_to(root))
|
||||
for p in root.rglob("*") if p.is_dir())
|
||||
return core.detect_structural_drift(model.doc["entities"], files,
|
||||
dirs)
|
||||
import os
|
||||
from pathlib import Path as _P
|
||||
root = _P(repo_root)
|
||||
|
||||
Generated
+23
@@ -9,6 +9,28 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -74,6 +96,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
|
||||
@@ -7,4 +7,6 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
# preserve_order: Python-dict insertion-order semantics (entities iterate
|
||||
# in document order; drift records are emitted in that same order).
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
@@ -0,0 +1,62 @@
|
||||
//! drift_resume branch battery — parity vs the frozen Python-oracle truth
|
||||
//! (frozen_dr_resume_truth.json: 35 cases across every public function and
|
||||
//! every branch — classify_drift's identity checks (`matches is True` /
|
||||
//! `is None`), the stale_ok/user_locked truthiness lattice, Python
|
||||
//! str(None)/str(True)/int evidence renderings, dict.get defaults applying
|
||||
//! ONLY on a missing key; reconcile's blocking-class filter with order
|
||||
//! preservation; detect_structural_drift's component filter, the
|
||||
//! ext→identities falsy-source or-chain, the user_locked `is True` lock,
|
||||
//! directory sources satisfying exists(), moved-twin evidence and
|
||||
//! reconciliation strings).
|
||||
//! Frozen 2026-10-13 via scripts/gen_drift_resume_truth.py with
|
||||
//! RUNE_DR_CORE=python pinned (poisoned-truth law). Embedded VERBATIM —
|
||||
//! never transcribed.
|
||||
|
||||
use crate::{classify_drift, detect_structural_drift, reconcile};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_dr_resume_truth.json");
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][group].as_array().unwrap().clone()
|
||||
}
|
||||
|
||||
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
for case in cases(group) {
|
||||
let got = f(&case["input"]);
|
||||
assert_eq!(
|
||||
&got,
|
||||
&case["expected"],
|
||||
"[{}] case '{}' diverged",
|
||||
group,
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_drift_parity_battery() {
|
||||
replay("classify_drift", |i| {
|
||||
json!({"record": classify_drift(&i["claim"], &i["observation"])})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_parity_battery() {
|
||||
replay("reconcile", |i| {
|
||||
let claims = i["claims"].as_array().cloned().unwrap_or_default();
|
||||
let observations =
|
||||
i["observations"].as_array().cloned().unwrap_or_default();
|
||||
reconcile(&claims, &observations)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_structural_drift_parity_battery() {
|
||||
replay("detect_structural_drift", |i| {
|
||||
let arr = |k: &str| i[k].as_array().cloned().unwrap_or_default();
|
||||
json!({"drifts": detect_structural_drift(
|
||||
&arr("entities"), &arr("files"), &arr("dirs"))})
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,26 +1,314 @@
|
||||
//! drift_resume — drift detection resume state (W6).
|
||||
pub fn resume_state(last_checkpoint: i64, current_position: i64) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"resumable": true,
|
||||
"last_checkpoint": last_checkpoint,
|
||||
"current_position": current_position,
|
||||
"pending_items": current_position - last_checkpoint,
|
||||
//! drift_resume — drift-aware resume for months-later project re-entry
|
||||
//! (T16 §16P + T18 §18H, W6).
|
||||
//!
|
||||
//! FULL parity port of src/rune/drift_resume.py (dichotomy wave, #2479
|
||||
//! tranche). Months-later resume must NOT blindly trust the last
|
||||
//! checkpoint: checkpoint claims NEVER substitute for reality — reality
|
||||
//! wins unless the claim carries explicit user-locked authority.
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
|
||||
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
|
||||
//! * dr_classify_drift({claim, observation}) → {"record"}
|
||||
//! * dr_reconcile({claims, observations})
|
||||
//! → {"resume", "drifts", "blocking"}
|
||||
//! * dr_detect_structural_drift({entities, files, dirs}) → {"drifts"}
|
||||
//!
|
||||
//! Boundary law: the observer callable (reconcile — it runs the actual
|
||||
//! check: test command, file read, git query) and the FILESYSTEM walk
|
||||
//! (detect_structural_drift: expected.exists() + the moved-twin rglob)
|
||||
//! stay in the Python facade — the kernel is a pure function of their
|
||||
//! results. The facade passes the observations and SORTED relative-path
|
||||
//! listings of repo_root (files + dirs; the oracle's rglob-first-match
|
||||
//! is order-independent across the battery's unique-basename twins).
|
||||
//!
|
||||
//! Python str() law: evidence strings embed f-string renderings of JSON
|
||||
//! values — str(None) is "None", str(True) is "True", ints render bare;
|
||||
//! dict.get defaults apply ONLY on a MISSING key (a present null renders
|
||||
//! "None" via the f-string, and a present empty string renders empty).
|
||||
//!
|
||||
//! No dr_free_string export — sibling crates sharing the Python process
|
||||
//! already export identical free symbols; callers free through
|
||||
//! capability_scoring's cs_free_string.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const EXPECTED: &str = "EXPECTED"; // checkpoint older than reality; benign aging
|
||||
pub const BENIGN: &str = "BENIGN"; // difference with no bearing on the claims
|
||||
pub const MATERIAL: &str = "MATERIAL"; // reality contradicts a scaffold claim
|
||||
pub const CONFLICTING: &str = "CONFLICTING"; // two authorities disagree; needs user
|
||||
pub const UNKNOWN: &str = "UNKNOWN"; // cannot decide — treated as blocking
|
||||
|
||||
pub const BLOCKING_CLASSES: [&str; 3] = [MATERIAL, CONFLICTING, UNKNOWN];
|
||||
|
||||
// ── Python scalar semantics ─────────────────────────────────────────────────
|
||||
|
||||
/// Python str() over a JSON scalar (f-string renderings in evidence).
|
||||
fn py_str(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Null => "None".into(),
|
||||
Value::Bool(true) => "True".into(),
|
||||
Value::Bool(false) => "False".into(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness over a JSON value.
|
||||
fn py_truthy(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Null => false,
|
||||
Value::Bool(b) => *b,
|
||||
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
|
||||
Value::String(s) => !s.is_empty(),
|
||||
Value::Array(a) => !a.is_empty(),
|
||||
Value::Object(o) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `d.get(key, default)` rendered through the f-string law: the default
|
||||
/// applies ONLY on a missing key — a present null renders "None".
|
||||
fn get_or_dflt(o: &Value, key: &str, dflt: &str) -> String {
|
||||
match o.get(key) {
|
||||
None => dflt.into(),
|
||||
Some(v) => py_str(v),
|
||||
}
|
||||
}
|
||||
|
||||
// ── classify_drift ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Classify one claim-vs-observation pair. Deterministic: verification
|
||||
/// evidence beats prose; explicit beats inferred. `matches is True` and
|
||||
/// `matches is None` are IDENTITY checks in the oracle — a truthy
|
||||
/// non-True value (e.g. 1) falls through to the reality-differs branch,
|
||||
/// and "None" covers both a missing key and an explicit null.
|
||||
pub fn classify_drift(claim: &Value, observation: &Value) -> Value {
|
||||
let claim_text = claim.get("claim").and_then(Value::as_str).unwrap_or("");
|
||||
match observation.get("matches") {
|
||||
Some(Value::Bool(true)) => {
|
||||
let stale_ok = claim
|
||||
.get("stale_ok")
|
||||
.map(py_truthy)
|
||||
.unwrap_or(false);
|
||||
json!({
|
||||
"claim": claim_text,
|
||||
"class": if stale_ok { EXPECTED } else { "CONFIRMED" },
|
||||
"evidence": get_or_dflt(observation, "checked_by", ""),
|
||||
})
|
||||
}
|
||||
None | Some(Value::Null) => json!({
|
||||
"claim": claim_text,
|
||||
"class": UNKNOWN,
|
||||
"evidence": format!(
|
||||
"unverifiable via {}",
|
||||
get_or_dflt(observation, "checked_by", "?")
|
||||
),
|
||||
}),
|
||||
_ => {
|
||||
let user_locked = claim
|
||||
.get("user_locked")
|
||||
.map(py_truthy)
|
||||
.unwrap_or(false);
|
||||
if user_locked {
|
||||
json!({
|
||||
"claim": claim_text,
|
||||
"class": CONFLICTING,
|
||||
"evidence": "reality differs from a USER-LOCKED claim — user arbitration required",
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"claim": claim_text,
|
||||
"class": MATERIAL,
|
||||
"evidence": format!(
|
||||
"reality differs: {} (checked by {})",
|
||||
get_or_dflt(observation, "actual", ""),
|
||||
get_or_dflt(observation, "checked_by", "?")
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── reconcile ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Reconcile a set of checkpoint claims against reality. The observer
|
||||
/// (which runs the actual check) stays in the Python facade — the kernel
|
||||
/// receives the observations. The resume decision is drift-aware, not
|
||||
/// blind: ANY blocking-class drift stops dependent work until reconciled.
|
||||
pub fn reconcile(claims: &[Value], observations: &[Value]) -> Value {
|
||||
let drifts: Vec<Value> = claims
|
||||
.iter()
|
||||
.zip(observations.iter())
|
||||
.map(|(c, o)| classify_drift(c, o))
|
||||
.collect();
|
||||
let blocking: Vec<Value> = drifts
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
BLOCKING_CLASSES.contains(&d["class"].as_str().unwrap_or(""))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
json!({
|
||||
"resume": blocking.is_empty(),
|
||||
"drifts": drifts,
|
||||
"blocking": blocking,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
// ── detect_structural_drift (T18 §18H) ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn resume_behind() {
|
||||
let r = resume_state(50, 100);
|
||||
assert!(r.is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_at_checkpoint() {
|
||||
let r = resume_state(100, 100);
|
||||
assert!(r.is_object());
|
||||
/// `e.get(key) or {}` — a missing OR null OR falsy ext falls back to {}.
|
||||
fn obj_or_empty<'a>(e: &'a Value, key: &str) -> &'a Value {
|
||||
match e.get(key) {
|
||||
Some(v @ Value::Object(_)) => v,
|
||||
_ => &Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's or-chain: `((e.get("ext") or {}).get("source")
|
||||
/// or (e.get("identities") or {}).get("source"))` — a falsy source
|
||||
/// (missing/null/empty string) falls through to the identities twin.
|
||||
fn source_field<'a>(e: &'a Value, key: &str) -> Option<&'a str> {
|
||||
obj_or_empty(e, key)
|
||||
.get("source")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn source_of(e: &Value) -> Option<&str> {
|
||||
source_field(e, "ext").or_else(|| source_field(e, "identities"))
|
||||
}
|
||||
|
||||
/// Compare structural-model component claims against repository reality
|
||||
/// (facade-gathered relative listings). Observable cases: source file
|
||||
/// moved/missing (component identities carry "source"), entities for
|
||||
/// files that no longer exist. REALITY WINS unless the claim is
|
||||
/// user-locked (identities.user_locked is True — the oracle's `is True`
|
||||
/// identity check, so a truthy non-True value is NOT locked).
|
||||
pub fn detect_structural_drift(
|
||||
entities: &[Value],
|
||||
files: &[Value],
|
||||
dirs: &[Value],
|
||||
) -> Vec<Value> {
|
||||
let in_set = |set: &[Value], src: &str| {
|
||||
set.iter().any(|p| p.as_str() == Some(src))
|
||||
};
|
||||
let mut drifts = Vec::new();
|
||||
for e in entities {
|
||||
if e.get("kind").and_then(Value::as_str) != Some("component") {
|
||||
continue;
|
||||
}
|
||||
let Some(src) = source_of(e) else {
|
||||
continue;
|
||||
};
|
||||
let locked = obj_or_empty(e, "identities").get("user_locked")
|
||||
== Some(&Value::Bool(true));
|
||||
let exists = in_set(files, src) || in_set(dirs, src);
|
||||
if exists {
|
||||
continue;
|
||||
}
|
||||
// reality: file gone/moved — search the facade listing for a
|
||||
// moved twin (first basename match in listing order)
|
||||
let expected_name =
|
||||
std::path::Path::new(src).file_name().and_then(|n| n.to_str());
|
||||
let moved: Option<String> = expected_name.and_then(|name| {
|
||||
files.iter().find_map(|p| {
|
||||
let s = p.as_str()?;
|
||||
(std::path::Path::new(s).file_name().and_then(|n| n.to_str())
|
||||
== Some(name))
|
||||
.then(|| s.to_string())
|
||||
})
|
||||
});
|
||||
let id = e.get("id").map(py_str).unwrap_or_default();
|
||||
drifts.push(json!({
|
||||
"claim": format!("{} source at {}", id, src),
|
||||
"class": if locked { CONFLICTING } else { MATERIAL },
|
||||
"evidence": match &moved {
|
||||
Some(m) => format!("source missing; moved to {}", m),
|
||||
None => "source file absent from repo".to_string(),
|
||||
},
|
||||
"reality_wins": !locked,
|
||||
"reconciliation": match &moved {
|
||||
Some(m) => format!("update source to {}", m),
|
||||
None => "entity requires re-derivation or user-locked preservation".to_string(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
drifts
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(arg: *const std::os::raw::c_char) -> Result<Value, String> {
|
||||
if arg.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(arg) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(s).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut std::os::raw::c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// dr_classify_drift({claim, observation}) → {record}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dr_classify_drift(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({
|
||||
"record": classify_drift(
|
||||
a.get("claim").unwrap_or(&Value::Null),
|
||||
a.get("observation").unwrap_or(&Value::Null),
|
||||
)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// dr_reconcile({claims, observations}) → {resume, drifts, blocking}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dr_reconcile(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let claims = a.get("claims").and_then(Value::as_array);
|
||||
let observations = a.get("observations").and_then(Value::as_array);
|
||||
reconcile(
|
||||
claims.map(Vec::as_slice).unwrap_or(&[]),
|
||||
observations.map(Vec::as_slice).unwrap_or(&[]),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// dr_detect_structural_drift({entities, files, dirs}) → {drifts}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dr_detect_structural_drift(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let empty: Vec<Value> = Vec::new();
|
||||
let arr = |k: &str| {
|
||||
a.get(k)
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| empty.clone())
|
||||
};
|
||||
json!({
|
||||
"drifts": detect_structural_drift(
|
||||
&arr("entities"), &arr("files"), &arr("dirs"))
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
Reference in new issue
Block a user