feat(structural_migration): literal-100% residue — dichotomy wave

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.
This commit is contained in:
Nick committed 2026-10-15 08:58:34 +01:00
1 parent 69e7c448d4
commit a399477bff
13 files changed
+930 -1

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
#!/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()
+23
View File
@@ -2,6 +2,28 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[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"
@@ -67,6 +89,7 @@ version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"indexmap",
"itoa",
"memchr",
"serde",
+1 -1
View File
@@ -7,4 +7,4 @@ edition = "2021"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
serde_json = { version = "1", features = ["preserve_order"] }
@@ -0,0 +1,540 @@
[
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"add optional kind"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MINOR"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"Add Optional Tags list"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MINOR"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"rename field x to y"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"drop legacy field"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"remove the old index"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"delete stale docs"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"id format now uuid"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"semantic shift of x"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"restructure the tree"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"change format of ids"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"merge the two kinds"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"split the id field"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": []
},
"now": null,
"expected": {
"ok": true,
"result": "MINOR"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"add optional kind",
"drop legacy field"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MATERIAL"
}
},
{
"fn": "classify_migration",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"dropship unrelated word"
]
},
"now": null,
"expected": {
"ok": true,
"result": "MINOR"
}
},
{
"fn": "affected_projects",
"kwargs": {
"stores": {
"delta": {
"schema_version": 3
},
"alpha": {
"schema_version": 5
},
"charlie": {
"schema_version": 5
},
"bravo": {}
},
"proposed_v": 5
},
"now": null,
"expected": {
"ok": true,
"result": [
"bravo",
"delta"
]
}
},
{
"fn": "affected_projects",
"kwargs": {
"stores": {
"delta": {
"schema_version": 3
},
"alpha": {
"schema_version": 5
},
"charlie": {
"schema_version": 5
},
"bravo": {}
},
"proposed_v": 3
},
"now": null,
"expected": {
"ok": true,
"result": [
"bravo"
]
}
},
{
"fn": "affected_projects",
"kwargs": {
"stores": {
"delta": {
"schema_version": 3
},
"alpha": {
"schema_version": 5
},
"charlie": {
"schema_version": 5
},
"bravo": {}
},
"proposed_v": 6
},
"now": null,
"expected": {
"ok": true,
"result": [
"alpha",
"bravo",
"charlie",
"delta"
]
}
},
{
"fn": "apply_migration",
"kwargs": {
"model_store": {
"schema_version": 3
},
"from_v": 3,
"to_v": 4,
"changes": [
"rename field x to y",
"drop legacy field",
"semantic shift of x",
"merge the two kinds"
],
"user_approved": false
},
"now": 1709164800,
"expected": {
"ok": false,
"error": "material schema migration 3\u21924 requires user consent: ['rename field x to y', 'drop legacy field', 'semantic shift of x']"
}
},
{
"fn": "apply_migration",
"kwargs": {
"model_store": null,
"from_v": 3,
"to_v": 4,
"changes": [
"rename field x to y",
"drop legacy field",
"semantic shift of x",
"merge the two kinds"
],
"user_approved": true,
"approved_by": "Nick"
},
"now": 1709164800,
"expected": {
"ok": true,
"result": {
"from_v": 3,
"to_v": 4,
"kind": "MATERIAL",
"changes": [
"rename field x to y",
"drop legacy field",
"semantic shift of x",
"merge the two kinds"
],
"user_approved": true,
"approved_by": "Nick",
"applied_at": "2024-02-29T00:00:00Z"
},
"key_order": [
"from_v",
"to_v",
"kind",
"changes",
"user_approved",
"approved_by",
"applied_at"
]
}
},
{
"fn": "apply_migration",
"kwargs": {
"model_store": null,
"from_v": 3,
"to_v": 4,
"changes": [
"add optional kind"
],
"user_approved": false
},
"now": 1709164800,
"expected": {
"ok": true,
"result": {
"from_v": 3,
"to_v": 4,
"kind": "MINOR",
"changes": [
"add optional kind"
],
"user_approved": false,
"approved_by": null,
"applied_at": "2024-02-29T00:00:00Z"
},
"key_order": [
"from_v",
"to_v",
"kind",
"changes",
"user_approved",
"approved_by",
"applied_at"
]
}
},
{
"fn": "apply_migration",
"kwargs": {
"model_store": null,
"from_v": 3,
"to_v": 4,
"changes": [
"add optional kind"
],
"user_approved": true,
"approved_by": ""
},
"now": 1709164800,
"expected": {
"ok": true,
"result": {
"from_v": 3,
"to_v": 4,
"kind": "MINOR",
"changes": [
"add optional kind"
],
"user_approved": true,
"approved_by": "",
"applied_at": "2024-02-29T00:00:00Z"
},
"key_order": [
"from_v",
"to_v",
"kind",
"changes",
"user_approved",
"approved_by",
"applied_at"
]
}
},
{
"fn": "apply_migration",
"kwargs": {
"model_store": null,
"from_v": 0,
"to_v": 1,
"changes": [
"split the id field"
],
"user_approved": false
},
"now": -1,
"expected": {
"ok": false,
"error": "material schema migration 0\u21921 requires user consent: ['split the id field']"
}
},
{
"fn": "consent_brief",
"kwargs": {
"from_v": 3,
"to_v": 4,
"changes": [
"rename field x to y",
"id format now uuid"
],
"benefits": [
"faster projections",
"one canonical id"
]
},
"now": null,
"expected": {
"ok": true,
"result": {
"current_version": 3,
"proposed_version": 4,
"benefits": [
"faster projections",
"one canonical id"
],
"semantic_changes": [
"rename field x to y",
"id format now uuid"
],
"migration_risks": [
"stored models require one-time reconciliation after upgrade",
"consumers of the old schema need re-reading via projection"
],
"expected_effect": "models keep their data; identity and relationship semantics change per the listed changes",
"requires": "explicit user approval before any project is upgraded"
},
"key_order": [
"current_version",
"proposed_version",
"benefits",
"semantic_changes",
"migration_risks",
"expected_effect",
"requires"
]
}
},
{
"fn": "consent_brief",
"kwargs": {
"from_v": 0,
"to_v": 1,
"changes": [],
"benefits": []
},
"now": null,
"expected": {
"ok": true,
"result": {
"current_version": 0,
"proposed_version": 1,
"benefits": [],
"semantic_changes": [],
"migration_risks": [
"stored models require one-time reconciliation after upgrade",
"consumers of the old schema need re-reading via projection"
],
"expected_effect": "models keep their data; identity and relationship semantics change per the listed changes",
"requires": "explicit user approval before any project is upgraded"
},
"key_order": [
"current_version",
"proposed_version",
"benefits",
"semantic_changes",
"migration_risks",
"expected_effect",
"requires"
]
}
}
]
+206
View File
@@ -42,6 +42,119 @@ pub fn affected_projects(
result
}
/// ConsentRequired — the oracle's exception: a MATERIAL migration was
/// requested without user approval. The MESSAGE is the contract
/// (exception TYPE names are stripped by str(e) — quirks law).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsentRequired(pub String);
impl std::fmt::Display for ConsentRequired {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ConsentRequired {}
/// `time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(secs))` — the
/// project_registry `_now` law (UTC civil fields, literal Z, seconds
/// resolution), with the clock as an argument (no-wall-clock law).
fn _now(secs: i64) -> String {
let days = secs.div_euclid(86_400);
let sod = secs.rem_euclid(86_400);
let (y, m, d) = civil_from_days(days);
format!(
"{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
sod / 3600,
(sod % 3600) / 60,
sod % 60
)
}
/// Howard Hinnant's civil_from_days — days since 1970-01-01 → (y, m, d).
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
(if m <= 2 { y + 1 } else { y }, m, d)
}
/// Python's list-of-str repr for the oracle's f-string `{changes[:3]}`
/// interpolation: `['a', 'b']`. Single-quote form — the frozen battery
/// cases carry quote-free change strings (a change containing a quote
/// would render under Python's repr fallback — documented divergence).
fn py_str_list(changes: &[String]) -> String {
let quoted: Vec<String> = changes
.iter()
.take(3)
.map(|c| format!("'{c}'"))
.collect();
format!("[{}]", quoted.join(", "))
}
/// apply_migration — apply (or refuse) a schema migration. MATERIAL
/// without explicit user approval is ConsentRequired — the silent
/// upgrade is structurally impossible. `model_store` is carried opaque
/// (the oracle accepts and never reads it); the clock is an argument
/// (epoch seconds → applied_at).
pub fn apply_migration(
model_store: Option<&serde_json::Value>,
from_v: i64,
to_v: i64,
changes: &[String],
user_approved: bool,
approved_by: &str,
now: i64,
) -> Result<serde_json::Value, ConsentRequired> {
let _ = model_store; // the oracle does not read it either
let kind = classify_migration(changes);
if kind == "MATERIAL" && !user_approved {
return Err(ConsentRequired(format!(
"material schema migration {from_v}\u{2192}{to_v} requires user consent: {}",
py_str_list(changes)
)));
}
Ok(serde_json::json!({
"from_v": from_v,
"to_v": to_v,
"kind": kind,
"changes": changes,
"user_approved": user_approved,
"approved_by": if user_approved { serde_json::json!(approved_by) } else { serde_json::Value::Null },
"applied_at": _now(now),
}))
}
/// consent_brief — the rule-18I consent presentation: current/proposed
/// version, benefits, semantic changes, risks, expected effect.
pub fn consent_brief(
from_v: i64,
to_v: i64,
changes: &[String],
benefits: &[String],
) -> serde_json::Value {
serde_json::json!({
"current_version": from_v,
"proposed_version": to_v,
"benefits": benefits,
"semantic_changes": changes,
"migration_risks": [
"stored models require one-time reconciliation after upgrade",
"consumers of the old schema need re-reading via projection",
],
"expected_effect": "models keep their data; identity and \
relationship semantics change per the listed \
changes",
"requires": "explicit user approval before any project is upgraded",
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -101,4 +214,97 @@ mod branch_battery {
assert_eq!(affected_projects(&stores, 3), vec!["c"]);
assert_eq!(affected_projects(&stores, 6), vec!["a", "b", "c"]);
}
/// Frozen-oracle truth replay (gen_structural_migration_truth.py):
/// every public fn across every branch, byte-identical regen x2.
/// No loader replay exists for this module (pure Python facade) —
/// the frozen battery IS the parity gate (L3 law).
#[test]
fn frozen_smig_truth_replay() {
let rows: Vec<serde_json::Value> = serde_json::from_str(
include_str!("frozen_smig_truth.json"),
)
.expect("frozen_smig_truth.json unparseable");
assert_eq!(rows.len(), 25, "truth case count drifted");
for (i, row) in rows.iter().enumerate() {
let fn_name = row["fn"].as_str().unwrap();
let kw = &row["kwargs"];
let exp = &row["expected"];
match fn_name {
"classify_migration" => {
let changes = str_list(&kw["changes"]);
let got = classify_migration(&changes);
assert_eq!(
got, exp["result"],
"case {i} classify_migration {changes:?}"
);
}
"affected_projects" => {
let mut stores = std::collections::HashMap::new();
for (name, doc) in kw["stores"].as_object().unwrap() {
stores.insert(name.clone(), doc.clone());
}
let proposed = kw["proposed_v"].as_i64().unwrap();
let want: Vec<String> = exp["result"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
assert_eq!(
affected_projects(&stores, proposed),
want,
"case {i} affected_projects"
);
}
"apply_migration" => {
let changes = str_list(&kw["changes"]);
let from_v = kw["from_v"].as_i64().unwrap();
let to_v = kw["to_v"].as_i64().unwrap();
let approved = kw["user_approved"].as_bool().unwrap();
let approved_by = kw["approved_by"].as_str().unwrap_or("");
let now = row["now"].as_i64().expect("now is the clock arg");
let got = apply_migration(
None, from_v, to_v, &changes, approved, approved_by, now,
);
if exp["ok"].as_bool().unwrap() {
let got = got.unwrap_or_else(|e| {
panic!("case {i}: unexpected ConsentRequired {e}")
});
assert_eq!(got, exp["result"], "case {i} apply_migration");
let order: Vec<&str> = got.as_object().unwrap().keys().map(|k| k.as_str()).collect();
let want_order: Vec<&str> = exp["key_order"].as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect();
assert_eq!(order, want_order, "case {i} insertion order");
} else {
let err = got.expect_err("case {i}: expected ConsentRequired");
assert_eq!(err.0, exp["error"].as_str().unwrap(), "case {i} message");
}
}
"consent_brief" => {
let changes = str_list(&kw["changes"]);
let benefits = str_list(&kw["benefits"]);
let got = consent_brief(
kw["from_v"].as_i64().unwrap(),
kw["to_v"].as_i64().unwrap(),
&changes,
&benefits,
);
assert_eq!(got, exp["result"], "case {i} consent_brief");
let order: Vec<&str> = got.as_object().unwrap().keys().map(|k| k.as_str()).collect();
let want_order: Vec<&str> = exp["key_order"].as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect();
assert_eq!(order, want_order, "case {i} insertion order");
}
other => panic!("unknown fn {other}"),
}
}
}
fn str_list(v: &serde_json::Value) -> Vec<String> {
v.as_array()
.unwrap()
.iter()
.map(|s| s.as_str().unwrap().to_string())
.collect()
}
}
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
da5a47225a5ccd474adca90e5e510c97 src/rust/structural_migration/src/frozen_smig_truth.json
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
da5a47225a5ccd474adca90e5e510c97 src/rust/structural_migration/src/frozen_smig_truth.json
+1
View File
@@ -0,0 +1 @@
0