Port the two audit-missing fns name-preserving into the crate: * apply_migration — the consent gate: MATERIAL without user approval is structurally impossible (ConsentRequired; the MESSAGE is the contract, arrow + Python list-repr of changes[:3]). The record carries from_v/ to_v/kind/changes/user_approved/approved_by (None unless approved, empty-string carried verbatim when approved) + applied_at. The wall clock is an ARGUMENT (no-clock law): epoch seconds → strftime(gmtime(secs)) via the project_registry civil_from_days law — two frozen instants incl. the 2024-02-29 leap day and pre-epoch -1. model_store is carried opaque — the oracle accepts and never reads it. * consent_brief — the rule-18I presentation dict (current/proposed version, benefits, semantic changes, the two fixed migration risks, expected effect, requires); serde_json preserve_order added so the insertion-order law is asserted, not assumed. Truth: gen_structural_migration_truth.py freezes all 4 fns × every branch (15 classify rows incl. the dropship-substring and case-fold traps, 3 affected_projects rows, 5 apply_migration rows incl. both error branches and the [:3] slice proof, 2 consent_brief rows) into frozen_smig_truth.json; regen x2 byte-identical (md5 evidence). Battery replays the frozen truth (25 rows: value equality + insertion-order assertions); cargo test 5/5 green, clippy -D warnings clean, build clean. Python suite 20/20 (no loader exists for this facade — pins are trivially both). structural_migration 2/4 → 4/4 fn-level FULL.
153 lines
6.0 KiB
Python
153 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""gen_structural_migration_truth.py — freeze/replay parity truth for
|
|
structural_migration (literal-100% residue lane, dichotomy wave #2479).
|
|
|
|
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
|
oracle (src/rune/structural_migration.py) across every branch and records
|
|
{fn, kwargs, now, expected} rows into
|
|
src/rust/structural_migration/src/frozen_smig_truth.json.
|
|
|
|
No-clock law: apply_migration reads `time.strftime(..., time.gmtime())`
|
|
directly — a module-global `import time` — so the rig patches the oracle's
|
|
`time` symbol with a frozen shim whose gmtime() returns a settable
|
|
instant. Every row carries its epoch (`now`); the battery passes it as
|
|
the clock argument. Two instants prove the civil-date law, incl. a leap
|
|
day and a pre-epoch negative.
|
|
|
|
Replay mode (--replay): re-runs the same scenarios against the Rust
|
|
kernel (via the frozen battery's own comparison law) is the cargo test
|
|
job; this script's replay just re-freezes and byte-compares (regen x2
|
|
law). Poisoned-truth law: no loader pin exists for this module (pure
|
|
Python facade) — the oracle IS the module.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
import time as _time
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
TRUTH_PATH = REPO / "src" / "rust" / "structural_migration" / "src" / "frozen_smig_truth.json"
|
|
|
|
sys.path.insert(0, str(REPO / "src"))
|
|
sys.path.insert(0, str(REPO / "src" / "rune"))
|
|
|
|
import structural_migration as oracle_mod # noqa: E402
|
|
|
|
# ── the frozen-clock rig (settable instant) ─────────────────────────────────
|
|
FROZEN = {"epoch": None}
|
|
|
|
|
|
def _frozen_struct():
|
|
return _time.gmtime(FROZEN["epoch"])
|
|
|
|
|
|
class _FrozenTime:
|
|
"""Module-global `time` shim: gmtime() honors the settable frozen
|
|
instant; strftime stays the real implementation (struct is real)."""
|
|
|
|
@staticmethod
|
|
def gmtime(secs=None):
|
|
return _frozen_struct() if secs is None else _time.gmtime(secs)
|
|
|
|
@staticmethod
|
|
def strftime(fmt, t=None):
|
|
return _time.strftime(fmt, t if t is not None else _frozen_struct())
|
|
|
|
|
|
oracle_mod.time = _FrozenTime
|
|
|
|
|
|
def case(fn, kwargs, now=None):
|
|
try:
|
|
result = getattr(oracle_mod, fn)(**kwargs)
|
|
expected = {"ok": True, "result": result}
|
|
if isinstance(result, dict):
|
|
expected["key_order"] = list(result.keys())
|
|
except Exception as ex: # the message is the contract (quirks law)
|
|
expected = {"ok": False, "error": str(ex)}
|
|
return {"fn": fn, "kwargs": kwargs, "now": now, "expected": expected}
|
|
|
|
|
|
def main():
|
|
cases = []
|
|
|
|
# classify_migration — every marker + case-sensitivity + empty
|
|
for changes, tag in [
|
|
(["add optional kind"], "minor_additive"),
|
|
(["Add Optional Tags list"], "minor_case_fold"),
|
|
(["rename field x to y"], "material_rename"),
|
|
(["drop legacy field"], "material_drop"),
|
|
(["remove the old index"], "material_remove"),
|
|
(["delete stale docs"], "material_delete"),
|
|
(["id format now uuid"], "material_id_format"),
|
|
(["semantic shift of x"], "material_semantic"),
|
|
(["restructure the tree"], "material_restructure"),
|
|
(["change format of ids"], "material_change_format"),
|
|
(["merge the two kinds"], "material_merge"),
|
|
(["split the id field"], "material_split"),
|
|
([], "minor_empty"),
|
|
(["add optional kind", "drop legacy field"], "material_wins_mixed"),
|
|
(["dropship unrelated word"], "material_substring_drop"),
|
|
]:
|
|
cases.append(case("classify_migration",
|
|
{"from_v": 3, "to_v": 4, "changes": changes}))
|
|
|
|
# affected_projects — gating, default schema_version, sorted output
|
|
stores = {
|
|
"delta": {"schema_version": 3},
|
|
"alpha": {"schema_version": 5},
|
|
"charlie": {"schema_version": 5},
|
|
"bravo": {},
|
|
}
|
|
for proposed_v, tag in [(5, "lt_strict"), (3, "default_zero"), (6, "all")]:
|
|
cases.append(case("affected_projects",
|
|
{"stores": stores, "proposed_v": proposed_v}))
|
|
|
|
# apply_migration — every branch of the consent gate.
|
|
# Two clock instants: a leap day and a pre-epoch negative (civil law).
|
|
LEAP = 1_709_164_800 # 2024-02-29T00:00:00Z
|
|
NEG = -1 # 1969-12-31T23:59:59Z
|
|
material = ["rename field x to y", "drop legacy field",
|
|
"semantic shift of x", "merge the two kinds"]
|
|
minor = ["add optional kind"]
|
|
|
|
FROZEN["epoch"] = LEAP
|
|
cases.append(case("apply_migration", {
|
|
"model_store": {"schema_version": 3}, "from_v": 3, "to_v": 4,
|
|
"changes": material, "user_approved": False,
|
|
}, now=LEAP))
|
|
cases.append(case("apply_migration", {
|
|
"model_store": None, "from_v": 3, "to_v": 4,
|
|
"changes": material, "user_approved": True, "approved_by": "Nick",
|
|
}, now=LEAP))
|
|
cases.append(case("apply_migration", {
|
|
"model_store": None, "from_v": 3, "to_v": 4,
|
|
"changes": minor, "user_approved": False,
|
|
}, now=LEAP)) # MINOR: auto — approved_by dropped to None
|
|
cases.append(case("apply_migration", {
|
|
"model_store": None, "from_v": 3, "to_v": 4,
|
|
"changes": minor, "user_approved": True, "approved_by": "",
|
|
}, now=LEAP)) # MINOR + approved: empty string is carried verbatim
|
|
cases.append(case("apply_migration", {
|
|
"model_store": None, "from_v": 0, "to_v": 1,
|
|
"changes": ["split the id field"], "user_approved": False,
|
|
}, now=NEG)) # error branch on the pre-epoch clock (message has no stamp)
|
|
|
|
# consent_brief — pure presentation dict, insertion order frozen
|
|
cases.append(case("consent_brief", {
|
|
"from_v": 3, "to_v": 4,
|
|
"changes": ["rename field x to y", "id format now uuid"],
|
|
"benefits": ["faster projections", "one canonical id"],
|
|
}))
|
|
cases.append(case("consent_brief", {
|
|
"from_v": 0, "to_v": 1, "changes": [], "benefits": [],
|
|
}))
|
|
|
|
TRUTH_PATH.write_text(json.dumps(cases, indent=1) + "\n")
|
|
print(f"wrote {TRUTH_PATH} ({len(cases)} cases)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|