diff --git a/scripts/gen_project_registry_truth.py b/scripts/gen_project_registry_truth.py index 2f7da7db..d44b90b7 100644 --- a/scripts/gen_project_registry_truth.py +++ b/scripts/gen_project_registry_truth.py @@ -440,7 +440,71 @@ GROUPS = [("default_doc", DEFAULT_DOC), ("register", REGISTER), ("get", GET), ("all_projects", ALL), ("set_health", HEALTH), ("set_outstanding", OUTSTANDING), ("mark_generation", MARK), ("resolve_cwd", CWD), ("needs_migration", MIGRATION), - ("load", LOAD), ("save", SAVE)] + ("load", LOAD), ("save", SAVE), + ("now", None), ("probe", None)] + + +def build_now_cases(): + """The oracle `_now` law: strftime over gmtime(secs) — one row per + epoch, incl. pre-epoch negatives and leap days. The oracle's patched + `time` symbol is swapped per case (its gmtime() takes no argument).""" + import time as _t + + rows = [] + for name, ts in [ + ("epoch-zero", 0), + ("frozen-ts", FROZEN_TS), + ("leap-day-2024", 1709164800), + ("leap-century-2000", 951782400), + ("non-leap-century-2100", 4102444800), + ("pre-epoch-negative", -1), + ("day-boundary", 86399), + ]: + class _T: + @staticmethod + def strftime(fmt, t): + return _t.strftime(fmt, t) + + @staticmethod + def gmtime(): + return _t.gmtime(ts) + + old = oracle_mod.time + oracle_mod.time = _T + try: + got = oracle_mod._now() + finally: + oracle_mod.time = old + rows.append({"name": name, "input": {"ts": ts}, + "return": None, "expected": {"now": got}}) + return rows + + +def build_probe_cases(): + """The oracle `_pr_core` resolution law, mg-probe pattern: the env + pin decides; a resolved core is frozen as a projection (the module + handle is not JSON-able).""" + rows = [] + for name, pin in [("pin-python", "python"), ("pin-auto", "auto"), + ("pin-other-resolves", "rust"), ("pin-unset-defaults-auto", None)]: + old = os.environ.get("RUNE_PR_CORE") + if pin is None: + os.environ.pop("RUNE_PR_CORE", None) + else: + os.environ["RUNE_PR_CORE"] = pin + try: + resolved = oracle_mod._pr_core() is not None + finally: + if old is None: + os.environ.pop("RUNE_PR_CORE", None) + else: + os.environ["RUNE_PR_CORE"] = old + rows.append({"name": name, "input": {"pin": pin}, + "return": None, "expected": {"resolved": resolved}}) + return rows + + +DYNAMIC_GROUPS = {"now": build_now_cases, "probe": build_probe_cases} # kernel symbol per group (None → facade-only group, skipped kernel-side) KERNEL = { @@ -512,6 +576,16 @@ def main(): for group, cases in GROUPS: recs = [] + if cases is None: + rows = DYNAMIC_GROUPS[group]() + if REPLAY: + for new, old in zip(rows, frozen[group]): + if canon(new) != canon(old): + print(f"FACADE DIVERGE [{group}] {new['name']}") + new["diverged"] = True + groups[group] = rows + continue + def rec(name, inp, oracle_ret, expected, _r=recs): entry = {"name": name, "input": inp, "return": oracle_ret, "expected": expected} diff --git a/src/rust/project_registry/src/branch_battery.rs b/src/rust/project_registry/src/branch_battery.rs index 763abc79..e3bec270 100644 --- a/src/rust/project_registry/src/branch_battery.rs +++ b/src/rust/project_registry/src/branch_battery.rs @@ -140,3 +140,30 @@ fn needs_migration_parity_battery() { json!({"ids": needs_migration(&doc_of(i), i["proposed"].as_i64().unwrap()).unwrap()}) }); } + +#[test] +fn now_parity_battery() { + for c in cases("now") { + let got = crate::_now(c["input"]["ts"].as_i64().unwrap_or(0)); + assert_eq!( + got, + c["expected"]["now"].as_str().unwrap_or(""), + "now case {} diverged", + c["name"].as_str().unwrap_or("") + ); + } +} + +#[test] +fn probe_parity_battery() { + for c in cases("probe") { + let pin = c["input"]["pin"].as_str(); + let got = crate::_pr_core(pin); + assert_eq!( + got.is_some(), + c["expected"]["resolved"].as_bool().unwrap_or(false), + "probe case {} diverged (pin={pin:?})", + c["name"].as_str().unwrap_or("") + ); + } +} diff --git a/src/rust/project_registry/src/frozen_pr_truth.json b/src/rust/project_registry/src/frozen_pr_truth.json index 9ac253fe..7f469c45 100644 --- a/src/rust/project_registry/src/frozen_pr_truth.json +++ b/src/rust/project_registry/src/frozen_pr_truth.json @@ -1678,6 +1678,120 @@ "file_text": "{\n \"projects\": {\n \"alpha\": {\n \"default_branch\": \"master\",\n \"forge_repo\": \"\",\n \"last_known_good\": \"\",\n \"outstanding_validations\": 0,\n \"project_id\": \"alpha\",\n \"registered_at\": \"2026-10-12T12:00:00Z\",\n \"repo_identity\": \"git:/repos/alpha\",\n \"runtime_generation\": \"\",\n \"scaffold_location\": \"/a\",\n \"scaffold_state\": {\n \"health\": \"DRIFTED\",\n \"last_material_update\": \"2026-10-12T12:00:00Z\",\n \"notes\": [\n {\n \"at\": \"2026-10-12T12:00:00Z\",\n \"note\": \"sweep\"\n }\n ]\n },\n \"structural_schema_version\": 1\n }\n },\n \"schema_version\": 1,\n \"updated_at\": \"2026-10-12T12:00:00Z\"\n}" } } + ], + "now": [ + { + "name": "epoch-zero", + "input": { + "ts": 0 + }, + "return": null, + "expected": { + "now": "1970-01-01T00:00:00Z" + } + }, + { + "name": "frozen-ts", + "input": { + "ts": 1791806400 + }, + "return": null, + "expected": { + "now": "2026-10-12T12:00:00Z" + } + }, + { + "name": "leap-day-2024", + "input": { + "ts": 1709164800 + }, + "return": null, + "expected": { + "now": "2024-02-29T00:00:00Z" + } + }, + { + "name": "leap-century-2000", + "input": { + "ts": 951782400 + }, + "return": null, + "expected": { + "now": "2000-02-29T00:00:00Z" + } + }, + { + "name": "non-leap-century-2100", + "input": { + "ts": 4102444800 + }, + "return": null, + "expected": { + "now": "2100-01-01T00:00:00Z" + } + }, + { + "name": "pre-epoch-negative", + "input": { + "ts": -1 + }, + "return": null, + "expected": { + "now": "1969-12-31T23:59:59Z" + } + }, + { + "name": "day-boundary", + "input": { + "ts": 86399 + }, + "return": null, + "expected": { + "now": "1970-01-01T23:59:59Z" + } + } + ], + "probe": [ + { + "name": "pin-python", + "input": { + "pin": "python" + }, + "return": null, + "expected": { + "resolved": false + } + }, + { + "name": "pin-auto", + "input": { + "pin": "auto" + }, + "return": null, + "expected": { + "resolved": true + } + }, + { + "name": "pin-other-resolves", + "input": { + "pin": "rust" + }, + "return": null, + "expected": { + "resolved": true + } + }, + { + "name": "pin-unset-defaults-auto", + "input": { + "pin": null + }, + "return": null, + "expected": { + "resolved": true + } + } ] } } diff --git a/src/rust/project_registry/src/lib.rs b/src/rust/project_registry/src/lib.rs index e70d4676..c8a26ee3 100644 --- a/src/rust/project_registry/src/lib.rs +++ b/src/rust/project_registry/src/lib.rs @@ -172,6 +172,54 @@ fn truncate_chars(s: &str, n: usize) -> String { // ── registry operations (pure functions of the loaded doc) ────────────────── +/// `_now()` with the clock as an argument (no-wall-clock law): epoch +/// seconds → `time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(secs))` — +/// UTC civil fields, always literal Z, seconds resolution (no fraction). +pub 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) +} + +// ── core probe (facade `_pr_core` mirror, _cs_core law) ───────────────────── + +/// The resolved in-process project-registry core. Twin of the oracle's +/// loader MODULE handle (`_pr_core` returns the pr_loader module, never +/// the raw CDLL — PrCore is that handle's shape in the reunited crate). +#[derive(Debug, Clone, Copy)] +pub struct PrCore; + +/// The Rust registry core probe (name-preserving port of `_pr_core`). +/// RUNE_PR_CORE=python pins the Python fallback → None; every other value +/// resolves the core. The oracle's load-failure branch can't fire here — +/// in-process is always loaded. +pub fn _pr_core(pin: Option<&str>) -> Option { + match pin { + Some("python") => None, + _ => Some(PrCore), + } +} + /// `_load` miss branch — the fresh registry document. pub fn default_doc(now: &str) -> Value { json!({"schema_version": REGISTRY_SCHEMA_VERSION,