feat(project_registry): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
b1cc915dae
commit
e978510b0b
8 files changed
+3126
-61
No files matched your search
@@ -0,0 +1,585 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_project_registry_truth.py — freeze/replay parity truth for
|
||||
project_registry (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/project_registry.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/project_registry/src/frozen_pr_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 methods delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_PR_CORE=python BEFORE any import (a
|
||||
no-op pre-port, load-bearing post-port — the facade must never delegate
|
||||
into the kernel while truth is being frozen).
|
||||
|
||||
No-clock law: the wall clock is frozen by patching the `time` symbol in
|
||||
project_registry, so every kernel payload carries its own `now` and
|
||||
replay is deterministic regardless of run date.
|
||||
|
||||
Boundary law: the registry FILE IO (_load hit branch, save) stays in the
|
||||
Python facade — the kernel is a pure function of the loaded doc. The
|
||||
`load` and `save` truth groups are facade-only (no kernel symbol) and
|
||||
are skipped by the kernel-level replay pass.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "project_registry", "src",
|
||||
"frozen_pr_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
os.environ["RUNE_PR_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import project_registry as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import pr_loader # noqa: E402 (kernel-level replay side)
|
||||
# replay law: the facade pass must exercise the LIVE Rust delegation
|
||||
# path, so the pin is lifted AFTER import (the facade reads the env
|
||||
# at call time). The kernel pass calls the loader directly.
|
||||
del os.environ["RUNE_PR_CORE"]
|
||||
|
||||
FROZEN_TS = 1791806400 # 2026-10-12T12:00:00Z
|
||||
FROZEN_NOW = "2026-10-12T12:00:00Z"
|
||||
|
||||
|
||||
class FakeTime:
|
||||
@staticmethod
|
||||
def strftime(fmt, t):
|
||||
import time as _t
|
||||
return _t.strftime(fmt, t)
|
||||
|
||||
@staticmethod
|
||||
def gmtime():
|
||||
import time as _t
|
||||
return _t.gmtime(FROZEN_TS)
|
||||
|
||||
|
||||
oracle_mod.time = FakeTime
|
||||
|
||||
|
||||
def fresh_dir():
|
||||
return tempfile.mkdtemp(prefix="pr_truth_")
|
||||
|
||||
|
||||
def fresh_registry(**kw):
|
||||
return oracle_mod.ProjectRegistry(fresh_dir(), **kw)
|
||||
|
||||
|
||||
def rec_kernel(rec, name, inp, expected, oracle_ret=None):
|
||||
"""Record a kernel case; inputs deep-copied BEFORE oracle mutation."""
|
||||
rec(name, copy.deepcopy(inp), oracle_ret, expected)
|
||||
|
||||
|
||||
def rec_facade(rec, name, inp, expected, oracle_ret=None):
|
||||
rec(name, inp, oracle_ret, expected)
|
||||
|
||||
|
||||
def err_or(fn):
|
||||
"""Run a facade call; freeze the kernel error envelope on exception."""
|
||||
try:
|
||||
return fn(), None
|
||||
except ValueError as e:
|
||||
return None, f"ValueError: {e}"
|
||||
except KeyError as e:
|
||||
return None, f"KeyError: {e.args[0]}"
|
||||
|
||||
|
||||
# ── default_doc (kernel) ─────────────────────────────────────────────────────
|
||||
|
||||
def c_default_doc(rec):
|
||||
r = fresh_registry()
|
||||
rec_kernel(rec, "default-doc-fresh", {"now": FROZEN_NOW}, {"doc": r.doc})
|
||||
|
||||
|
||||
# ── register (kernel) ────────────────────────────────────────────────────────
|
||||
|
||||
def _register_case(rec, name, doc_before, **kw):
|
||||
r = doc_before if doc_before is not None else fresh_registry()
|
||||
before = copy.deepcopy(r.doc)
|
||||
entry = r.register(**kw)
|
||||
inp = {"doc": before, "project_id": kw["project_id"],
|
||||
"scaffold_location": kw.get("scaffold_location", ""),
|
||||
"repo_identity": kw.get("repo_identity", ""),
|
||||
"forge_repo": kw.get("forge_repo", ""),
|
||||
"default_branch": kw.get("default_branch", "master"),
|
||||
"structural_schema_version": kw.get("structural_schema_version", 1),
|
||||
"runtime_generation": kw.get("runtime_generation", ""),
|
||||
"last_known_good": kw.get("last_known_good", ""),
|
||||
"now": FROZEN_NOW}
|
||||
rec_kernel(rec, name, inp, {"doc": r.doc, "entry": entry})
|
||||
|
||||
|
||||
def c_register_defaults(rec):
|
||||
_register_case(rec, "register-defaults", None,
|
||||
project_id="alpha", scaffold_location="/tmp/alpha/scaffold")
|
||||
|
||||
|
||||
def c_register_full(rec):
|
||||
_register_case(rec, "register-full-kwargs", None,
|
||||
project_id="beta", scaffold_location="/tmp/beta/scaffold",
|
||||
repo_identity="git:/repos/beta", forge_repo="conductor/beta",
|
||||
default_branch="main", structural_schema_version=3,
|
||||
runtime_generation="gen-7", last_known_good="gen-6")
|
||||
|
||||
|
||||
def c_register_overwrites(rec):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/old")
|
||||
_register_case(rec, "register-overwrites-existing", r,
|
||||
project_id="alpha", scaffold_location="/new",
|
||||
repo_identity="git:/repos/alpha")
|
||||
|
||||
|
||||
def c_register_second_project(rec):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/a")
|
||||
_register_case(rec, "register-second-preserves-first", r,
|
||||
project_id="beta", scaffold_location="/b")
|
||||
|
||||
|
||||
# ── get (kernel) ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_case(rec, name, register_kw, probe):
|
||||
r = fresh_registry()
|
||||
if register_kw:
|
||||
r.register(**register_kw)
|
||||
entry = r.get(probe)
|
||||
inp = {"doc": copy.deepcopy(r.doc), "project_id": probe}
|
||||
rec_kernel(rec, name, inp, {"entry": entry})
|
||||
|
||||
|
||||
def c_get_present(rec):
|
||||
_get_case(rec, "get-present", {"project_id": "alpha",
|
||||
"scaffold_location": "/a"}, "alpha")
|
||||
|
||||
|
||||
def c_get_missing(rec):
|
||||
_get_case(rec, "get-missing-null", {"project_id": "alpha",
|
||||
"scaffold_location": "/a"}, "ghost")
|
||||
|
||||
|
||||
def c_get_empty_registry(rec):
|
||||
_get_case(rec, "get-empty-registry", None, "alpha")
|
||||
|
||||
|
||||
# ── all_projects (kernel) ────────────────────────────────────────────────────
|
||||
|
||||
def _all_case(rec, name, pids):
|
||||
r = fresh_registry()
|
||||
for pid in pids:
|
||||
r.register(project_id=pid, scaffold_location=f"/{pid}")
|
||||
projs = r.all_projects()
|
||||
inp = {"doc": copy.deepcopy(r.doc)}
|
||||
rec_kernel(rec, name, inp, {"projects": projs})
|
||||
|
||||
|
||||
def c_all_empty(rec):
|
||||
_all_case(rec, "all-empty-registry", [])
|
||||
|
||||
|
||||
def c_all_single(rec):
|
||||
_all_case(rec, "all-single", ["alpha"])
|
||||
|
||||
|
||||
def c_all_sorted(rec):
|
||||
_all_case(rec, "all-sorted-by-project-id",
|
||||
["zeta", "alpha", "mu", "beta"])
|
||||
|
||||
|
||||
# ── set_health (kernel) ──────────────────────────────────────────────────────
|
||||
|
||||
def _health_case(rec, name, health, note="", pid="alpha", setup=None):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/a",
|
||||
repo_identity="git:/repos/alpha")
|
||||
if setup:
|
||||
setup(r)
|
||||
before = copy.deepcopy(r.doc)
|
||||
ret, err = err_or(lambda: r.set_health(pid, health, note=note))
|
||||
inp = {"doc": before, "project_id": pid, "health": health, "note": note,
|
||||
"now": FROZEN_NOW}
|
||||
rec_kernel(rec, name, inp,
|
||||
{"doc": r.doc} if err is None else {"error": err})
|
||||
|
||||
|
||||
def c_health_valid_no_note(rec):
|
||||
_health_case(rec, "health-valid-no-note", "HEALTHY")
|
||||
|
||||
|
||||
def c_health_with_note(rec):
|
||||
_health_case(rec, "health-with-note", "DRIFTED", note="diverged upstream")
|
||||
|
||||
|
||||
def c_health_note_truncated(rec):
|
||||
_health_case(rec, "health-note-truncated-160", "MIGRATION_REQUIRED",
|
||||
note="n" * 200)
|
||||
|
||||
|
||||
def c_health_appends_notes(rec):
|
||||
def setup(r):
|
||||
r.set_health("alpha", "DRIFTED", note="first")
|
||||
_health_case(rec, "health-second-note-appends", "HEALTHY", note="second",
|
||||
setup=setup)
|
||||
|
||||
|
||||
def c_health_invalid(rec):
|
||||
_health_case(rec, "health-invalid-valueerror", "BROKEN")
|
||||
|
||||
|
||||
def c_health_unknown_project(rec):
|
||||
_health_case(rec, "health-unknown-project-keyerror", "HEALTHY",
|
||||
pid="ghost")
|
||||
|
||||
|
||||
# ── set_outstanding_validations (kernel) ─────────────────────────────────────
|
||||
|
||||
def _outstanding_case(rec, name, pid, count, setup=None):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/a")
|
||||
if setup:
|
||||
setup(r)
|
||||
before = copy.deepcopy(r.doc)
|
||||
ret, err = err_or(lambda: r.set_outstanding_validations(pid, count))
|
||||
inp = {"doc": before, "project_id": pid, "count": count}
|
||||
rec_kernel(rec, name, inp,
|
||||
{"doc": r.doc} if err is None else {"error": err})
|
||||
|
||||
|
||||
def c_outstanding_set(rec):
|
||||
_outstanding_case(rec, "outstanding-set-count", "alpha", 4)
|
||||
|
||||
|
||||
def c_outstanding_overwrite(rec):
|
||||
def setup(r):
|
||||
r.set_outstanding_validations("alpha", 9)
|
||||
_outstanding_case(rec, "outstanding-overwrite", "alpha", 2, setup=setup)
|
||||
|
||||
|
||||
def c_outstanding_unknown(rec):
|
||||
_outstanding_case(rec, "outstanding-unknown-keyerror", "ghost", 1)
|
||||
|
||||
|
||||
# ── mark_generation (kernel) ─────────────────────────────────────────────────
|
||||
|
||||
def _mark_case(rec, name, current, last_known_good="", setup=None):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/a",
|
||||
runtime_generation="gen-1")
|
||||
if setup:
|
||||
setup(r)
|
||||
before = copy.deepcopy(r.doc)
|
||||
ret, err = err_or(lambda: r.mark_generation(
|
||||
"alpha", current=current, last_known_good=last_known_good))
|
||||
inp = {"doc": before, "project_id": "alpha", "current": current,
|
||||
"last_known_good": last_known_good}
|
||||
rec_kernel(rec, name, inp,
|
||||
{"doc": r.doc} if err is None else {"error": err})
|
||||
|
||||
|
||||
def c_mark_with_lkg(rec):
|
||||
_mark_case(rec, "mark-generation-with-lkg", "gen-2", "gen-1")
|
||||
|
||||
|
||||
def c_mark_no_lkg(rec):
|
||||
_mark_case(rec, "mark-generation-no-lkg-untouched", "gen-3")
|
||||
|
||||
|
||||
def c_mark_unknown(rec):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/a")
|
||||
before = copy.deepcopy(r.doc)
|
||||
ret, err = err_or(lambda: r.mark_generation(
|
||||
"ghost", current="gen-9", last_known_good="gen-8"))
|
||||
rec_kernel(rec, "mark-generation-unknown-keyerror",
|
||||
{"doc": before, "project_id": "ghost", "current": "gen-9",
|
||||
"last_known_good": "gen-8"}, {"error": err})
|
||||
|
||||
|
||||
# ── resolve_cwd (kernel) ─────────────────────────────────────────────────────
|
||||
|
||||
def _cwd_case(rec, name, register_kw, probe, mutate=None):
|
||||
r = fresh_registry()
|
||||
r.register(**register_kw)
|
||||
if mutate:
|
||||
mutate(r)
|
||||
before = copy.deepcopy(r.doc)
|
||||
ret, err = err_or(lambda: r.resolve_cwd(probe))
|
||||
inp = {"doc": before, "project_id": probe}
|
||||
rec_kernel(rec, name, inp,
|
||||
{"cwd": ret} if err is None else {"error": err})
|
||||
|
||||
|
||||
def c_cwd_git_prefix(rec):
|
||||
_cwd_case(rec, "cwd-git-prefix-stripped",
|
||||
{"project_id": "rune", "scaffold_location": "/x/scaffold",
|
||||
"repo_identity": "git:/repos/rune"}, "rune")
|
||||
|
||||
|
||||
def c_cwd_bare_path(rec):
|
||||
_cwd_case(rec, "cwd-bare-path", {"project_id": "ext",
|
||||
"scaffold_location": "/x/s",
|
||||
"repo_identity": "/tmp/ext/repo"}, "ext")
|
||||
|
||||
|
||||
def c_cwd_worktree_overrides(rec):
|
||||
_cwd_case(rec, "cwd-active-worktree-overrides",
|
||||
{"project_id": "rune", "scaffold_location": "/x/scaffold",
|
||||
"repo_identity": "git:/repos/rune"}, "rune",
|
||||
mutate=lambda r: r.doc["projects"]["rune"].update(
|
||||
{"active_worktree": "/wt/rune"}))
|
||||
|
||||
|
||||
def c_cwd_whitespace_repo(rec):
|
||||
_cwd_case(rec, "cwd-whitespace-repo-refused",
|
||||
{"project_id": "ws", "scaffold_location": "/x/s",
|
||||
"repo_identity": " "}, "ws")
|
||||
|
||||
|
||||
def c_cwd_unknown(rec):
|
||||
_cwd_case(rec, "cwd-unknown-project-refused",
|
||||
{"project_id": "rune", "scaffold_location": "/x/s",
|
||||
"repo_identity": "git:/repos/rune"}, "ghost")
|
||||
|
||||
|
||||
def c_cwd_no_repo_identity(rec):
|
||||
_cwd_case(rec, "cwd-no-repo-identity-refused",
|
||||
{"project_id": "remote-only", "scaffold_location": "/x/s"}, "remote-only")
|
||||
|
||||
|
||||
# ── needs_migration (kernel) ─────────────────────────────────────────────────
|
||||
|
||||
def _migration_case(rec, name, versions, proposed):
|
||||
r = fresh_registry()
|
||||
for pid, ver in versions.items():
|
||||
r.register(project_id=pid, scaffold_location=f"/{pid}",
|
||||
structural_schema_version=ver)
|
||||
out = r.needs_migration(proposed_schema_version=proposed)
|
||||
inp = {"doc": copy.deepcopy(r.doc), "proposed": proposed}
|
||||
rec_kernel(rec, name, inp, {"ids": out})
|
||||
|
||||
|
||||
def c_migration_mixed(rec):
|
||||
_migration_case(rec, "migration-mixed-sorted",
|
||||
{"new": 3, "old": 1, "mid": 2}, 2)
|
||||
|
||||
|
||||
def c_migration_none(rec):
|
||||
_migration_case(rec, "migration-none-older", {"a": 2, "b": 3}, 2)
|
||||
|
||||
|
||||
def c_migration_default_version(rec):
|
||||
_migration_case(rec, "migration-default-version-1", {"a": 1, "b": 1}, 2)
|
||||
|
||||
|
||||
def c_migration_empty(rec):
|
||||
_migration_case(rec, "migration-empty-registry", {}, 5)
|
||||
|
||||
|
||||
# ── load / save (facade-only: FILE IO stays in Python) ──────────────────────
|
||||
|
||||
def c_load_missing(rec):
|
||||
r = fresh_registry()
|
||||
rec_facade(rec, "load-missing-file-default-doc", {"path": "missing"},
|
||||
{"doc": r.doc})
|
||||
|
||||
|
||||
def c_load_valid(rec):
|
||||
d = fresh_dir()
|
||||
pre = {"schema_version": 1, "projects": {"alpha": {"project_id": "alpha"}},
|
||||
"updated_at": FROZEN_NOW}
|
||||
Path(d, "project_registry.json").write_text(
|
||||
json.dumps(pre, indent=1, sort_keys=True))
|
||||
r = oracle_mod.ProjectRegistry(d)
|
||||
rec_facade(rec, "load-valid-file-roundtrip", {"path": "valid"},
|
||||
{"doc": r.doc})
|
||||
|
||||
|
||||
def c_save(rec):
|
||||
r = fresh_registry()
|
||||
r.register(project_id="alpha", scaffold_location="/a",
|
||||
repo_identity="git:/repos/alpha")
|
||||
r.set_health("alpha", "DRIFTED", note="sweep")
|
||||
r.save()
|
||||
text = Path(r.path).read_text()
|
||||
rec_facade(rec, "save-stamps-updated-at-indent1-sortkeys",
|
||||
{"doc": copy.deepcopy(r.doc)}, {"file_text": text},
|
||||
oracle_ret=None)
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
DEFAULT_DOC = [c_default_doc]
|
||||
REGISTER = [c_register_defaults, c_register_full, c_register_overwrites,
|
||||
c_register_second_project]
|
||||
GET = [c_get_present, c_get_missing, c_get_empty_registry]
|
||||
ALL = [c_all_empty, c_all_single, c_all_sorted]
|
||||
HEALTH = [c_health_valid_no_note, c_health_with_note, c_health_note_truncated,
|
||||
c_health_appends_notes, c_health_invalid, c_health_unknown_project]
|
||||
OUTSTANDING = [c_outstanding_set, c_outstanding_overwrite, c_outstanding_unknown]
|
||||
MARK = [c_mark_with_lkg, c_mark_no_lkg, c_mark_unknown]
|
||||
CWD = [c_cwd_git_prefix, c_cwd_bare_path, c_cwd_worktree_overrides,
|
||||
c_cwd_whitespace_repo, c_cwd_unknown, c_cwd_no_repo_identity]
|
||||
MIGRATION = [c_migration_mixed, c_migration_none, c_migration_default_version,
|
||||
c_migration_empty]
|
||||
LOAD = [c_load_missing, c_load_valid]
|
||||
SAVE = [c_save]
|
||||
|
||||
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)]
|
||||
|
||||
# kernel symbol per group (None → facade-only group, skipped kernel-side)
|
||||
KERNEL = {
|
||||
"default_doc": "pr_default_doc",
|
||||
"register": "pr_register",
|
||||
"get": "pr_get",
|
||||
"all_projects": "pr_all_projects",
|
||||
"set_health": "pr_set_health",
|
||||
"set_outstanding": "pr_set_outstanding",
|
||||
"mark_generation": "pr_mark_generation",
|
||||
"resolve_cwd": "pr_resolve_cwd",
|
||||
"needs_migration": "pr_needs_migration",
|
||||
"load": None,
|
||||
"save": None,
|
||||
}
|
||||
|
||||
# loader wrapper + slice of `expected` the unwrapped FFI value maps to
|
||||
CALLS = {
|
||||
"default_doc": (lambda w: w["doc"],
|
||||
lambda i, ld: ld.default_doc(now=i["now"])),
|
||||
"register": (lambda w: w,
|
||||
lambda i, ld: ld.register(i["doc"], project_id=i["project_id"],
|
||||
scaffold_location=i["scaffold_location"],
|
||||
repo_identity=i["repo_identity"],
|
||||
forge_repo=i["forge_repo"],
|
||||
default_branch=i["default_branch"],
|
||||
structural_schema_version=i["structural_schema_version"],
|
||||
runtime_generation=i["runtime_generation"],
|
||||
last_known_good=i["last_known_good"],
|
||||
now=i["now"])),
|
||||
"get": (lambda w: w["entry"],
|
||||
lambda i, ld: ld.get(i["doc"], i["project_id"])),
|
||||
"all_projects": (lambda w: w["projects"],
|
||||
lambda i, ld: ld.all_projects(i["doc"])),
|
||||
"set_health": (lambda w: w["doc"],
|
||||
lambda i, ld: ld.set_health(i["doc"], i["project_id"],
|
||||
i["health"], note=i["note"],
|
||||
now=i["now"])),
|
||||
"set_outstanding": (lambda w: w["doc"],
|
||||
lambda i, ld: ld.set_outstanding(i["doc"],
|
||||
i["project_id"],
|
||||
i["count"])),
|
||||
"mark_generation": (lambda w: w["doc"],
|
||||
lambda i, ld: ld.mark_generation(i["doc"],
|
||||
i["project_id"],
|
||||
current=i["current"],
|
||||
last_known_good=i["last_known_good"])),
|
||||
"resolve_cwd": (lambda w: w["cwd"],
|
||||
lambda i, ld: ld.resolve_cwd(i["doc"], i["project_id"])),
|
||||
"needs_migration": (lambda w: w["ids"],
|
||||
lambda i, ld: ld.needs_migration(i["doc"],
|
||||
i["proposed"])),
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization: a sorted dump makes the comparison
|
||||
key-order independent (int keys never occur here)."""
|
||||
return json.dumps(json.loads(json.dumps(x)), 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():
|
||||
spec = CALLS.get(group)
|
||||
if spec is None:
|
||||
continue
|
||||
unwrap, call = spec
|
||||
for case in cases:
|
||||
checked += 1
|
||||
try:
|
||||
got = call(copy.deepcopy(case["input"]), pr_loader)
|
||||
except (ValueError, KeyError) as e:
|
||||
got = (f"ValueError: {e}" if isinstance(e, ValueError)
|
||||
else f"KeyError: {e.args[0]}")
|
||||
want = (case["expected"]["error"]
|
||||
if isinstance(case["expected"], dict)
|
||||
and "error" in case["expected"]
|
||||
else unwrap(case["expected"]))
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[: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": FROZEN_NOW,
|
||||
"oracle": "src/rune/project_registry.py",
|
||||
"pin": "RUNE_PR_CORE=python",
|
||||
"note": "Frozen via scripts/gen_project_registry_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_NOW +
|
||||
" (FakeTime patched into project_registry.time; epoch " +
|
||||
str(FROZEN_TS) + "). Battery embeds this VERBATIM. Groups `load` "
|
||||
"and `save` are facade-only (FILE IO stays in Python).",
|
||||
"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,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the FULL Rust project_registry core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* pr_default_doc({now}) -> {doc}
|
||||
* pr_register({doc, project_id, scaffold_location, repo_identity,
|
||||
forge_repo, default_branch, structural_schema_version,
|
||||
runtime_generation, last_known_good, now}) -> {doc, entry}
|
||||
* pr_get({doc, project_id}) -> {entry}
|
||||
* pr_all_projects({doc}) -> {projects}
|
||||
* pr_set_health({doc, project_id, health, note, now}) -> {doc}
|
||||
* pr_set_outstanding({doc, project_id, count}) -> {doc}
|
||||
* pr_mark_generation({doc, project_id, current, last_known_good})
|
||||
-> {doc}
|
||||
* pr_resolve_cwd({doc, project_id}) -> {cwd}
|
||||
* pr_needs_migration({doc, proposed}) -> {ids}
|
||||
|
||||
No-clock law: `now` (UTC ISO-8601) is always an argument to the stamping
|
||||
calls — the kernel never reads the wall clock.
|
||||
|
||||
Boundary: the registry FILE IO (_load hit branch, save — including the
|
||||
updated_at stamp and the indent=1/sort_keys dump) stays in the Python
|
||||
facade (project_registry.py); the kernel is a pure function of the
|
||||
loaded doc.
|
||||
|
||||
Exception law: the kernel's `err` string carries the oracle's Python
|
||||
exception kind — "ValueError: <msg>" / "KeyError: <key>" — and this
|
||||
loader re-raises the SAME exception type the oracle raised (parity for
|
||||
pytest.raises callers).
|
||||
|
||||
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 PR_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" / "project_registry"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"PR_RUST_LIB", _CRATE_DIR / "target" / "release" / "libproject_registry.so"
|
||||
)
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"pr_default_doc",
|
||||
"pr_register",
|
||||
"pr_get",
|
||||
"pr_all_projects",
|
||||
"pr_set_health",
|
||||
"pr_set_outstanding",
|
||||
"pr_mark_generation",
|
||||
"pr_resolve_cwd",
|
||||
"pr_needs_migration",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ProjectRegistryCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust project_registry 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 ProjectRegistryCoreUnavailable(
|
||||
f"Rust project_registry 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"):
|
||||
err = doc.get("err", f"{sym} failed")
|
||||
if err.startswith("ValueError: "):
|
||||
raise ValueError(err[len("ValueError: "):])
|
||||
if err.startswith("KeyError: "):
|
||||
raise KeyError(err[len("KeyError: "):])
|
||||
raise RuntimeError(err)
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def default_doc(now: str) -> dict:
|
||||
return _call("pr_default_doc", {"now": now})["doc"]
|
||||
|
||||
|
||||
def register(doc: dict, *, project_id: str, scaffold_location: str,
|
||||
repo_identity: str = "", forge_repo: str = "",
|
||||
default_branch: str = "master",
|
||||
structural_schema_version: int = 1,
|
||||
runtime_generation: str = "",
|
||||
last_known_good: str = "", now: str = "") -> dict:
|
||||
return _call("pr_register", {
|
||||
"doc": doc, "project_id": project_id,
|
||||
"scaffold_location": scaffold_location,
|
||||
"repo_identity": repo_identity, "forge_repo": forge_repo,
|
||||
"default_branch": default_branch,
|
||||
"structural_schema_version": structural_schema_version,
|
||||
"runtime_generation": runtime_generation,
|
||||
"last_known_good": last_known_good, "now": now})
|
||||
|
||||
|
||||
def get(doc: dict, project_id: str):
|
||||
return _call("pr_get", {"doc": doc, "project_id": project_id})["entry"]
|
||||
|
||||
|
||||
def all_projects(doc: dict) -> list:
|
||||
return _call("pr_all_projects", {"doc": doc})["projects"]
|
||||
|
||||
|
||||
def set_health(doc: dict, project_id: str, health: str, *,
|
||||
note: str = "", now: str = "") -> dict:
|
||||
return _call("pr_set_health", {"doc": doc, "project_id": project_id,
|
||||
"health": health, "note": note,
|
||||
"now": now})["doc"]
|
||||
|
||||
|
||||
def set_outstanding(doc: dict, project_id: str, count: int) -> dict:
|
||||
return _call("pr_set_outstanding", {"doc": doc, "project_id": project_id,
|
||||
"count": count})["doc"]
|
||||
|
||||
|
||||
def mark_generation(doc: dict, project_id: str, *, current: str,
|
||||
last_known_good: str = "") -> dict:
|
||||
return _call("pr_mark_generation", {"doc": doc, "project_id": project_id,
|
||||
"current": current,
|
||||
"last_known_good": last_known_good})["doc"]
|
||||
|
||||
|
||||
def resolve_cwd(doc: dict, project_id: str) -> str:
|
||||
return _call("pr_resolve_cwd", {"doc": doc,
|
||||
"project_id": project_id})["cwd"]
|
||||
|
||||
|
||||
def needs_migration(doc: dict, proposed: int) -> list:
|
||||
return _call("pr_needs_migration", {"doc": doc,
|
||||
"proposed": proposed})["ids"]
|
||||
@@ -12,9 +12,17 @@ BOUNDARIES (rule 19C, proven by tests):
|
||||
Registry state: one JSON document (versioned, provenance-aware per rule
|
||||
G) at <store_dir>/project_registry.json. Operational uses: /testing
|
||||
across projects (19L), drift/migration sweeps, re-entry discovery.
|
||||
|
||||
Dichotomy wave #2479: the doc transformations (register entry shaping,
|
||||
health lattice + note log, generation marks, resolve_cwd precedence,
|
||||
migration sweep) live in the Rust kernel (src/rust/project_registry)
|
||||
behind doc-envelope FFI; the registry FILE IO (_load hit branch, save)
|
||||
stays here — the kernel is a pure function of the loaded doc.
|
||||
RUNE_PR_CORE=python pins the Python fallback (parity harnesses).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -32,6 +40,22 @@ def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _pr_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_PR_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the doc
|
||||
transformations; the FILE IO stays in this facade."""
|
||||
if os.environ.get("RUNE_PR_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import pr_loader
|
||||
|
||||
if pr_loader.available():
|
||||
return pr_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class ProjectRegistry:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "project_registry.json"
|
||||
@@ -41,6 +65,9 @@ class ProjectRegistry:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
return core.default_doc(now=_now())
|
||||
return {"schema_version": REGISTRY_SCHEMA_VERSION,
|
||||
"projects": {}, "updated_at": _now()}
|
||||
|
||||
@@ -58,6 +85,18 @@ class ProjectRegistry:
|
||||
last_known_good: str = "") -> dict:
|
||||
"""Register/index a scaffolded project. References only — the
|
||||
project's structural truth stays in ITS store (boundary rule)."""
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
out = core.register(self.doc, project_id=project_id,
|
||||
scaffold_location=scaffold_location,
|
||||
repo_identity=repo_identity,
|
||||
forge_repo=forge_repo,
|
||||
default_branch=default_branch,
|
||||
structural_schema_version=structural_schema_version,
|
||||
runtime_generation=runtime_generation,
|
||||
last_known_good=last_known_good, now=_now())
|
||||
self.doc = out["doc"]
|
||||
return out["entry"]
|
||||
entry = {
|
||||
"project_id": project_id,
|
||||
"scaffold_location": scaffold_location,
|
||||
@@ -76,14 +115,25 @@ class ProjectRegistry:
|
||||
return entry
|
||||
|
||||
def get(self, project_id: str) -> dict | None:
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
return core.get(self.doc, project_id)
|
||||
return self.doc["projects"].get(project_id)
|
||||
|
||||
def all_projects(self) -> list[dict]:
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
return core.all_projects(self.doc)
|
||||
return sorted(self.doc["projects"].values(),
|
||||
key=lambda p: p["project_id"])
|
||||
|
||||
def set_health(self, project_id: str, health: str, *,
|
||||
note: str = "") -> None:
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
self.doc = core.set_health(self.doc, project_id, health,
|
||||
note=note, now=_now())
|
||||
return
|
||||
if health not in (HEALTHY, DRIFTED, MIGRATION_REQUIRED, UNKNOWN):
|
||||
raise ValueError(f"unknown health {health!r}")
|
||||
p = self.doc["projects"][project_id]
|
||||
@@ -95,10 +145,20 @@ class ProjectRegistry:
|
||||
|
||||
def set_outstanding_validations(self, project_id: str,
|
||||
count: int) -> None:
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
self.doc = core.set_outstanding(self.doc, project_id, count)
|
||||
return
|
||||
self.doc["projects"][project_id]["outstanding_validations"] = count
|
||||
|
||||
def mark_generation(self, project_id: str, *, current: str,
|
||||
last_known_good: str = "") -> None:
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
self.doc = core.mark_generation(self.doc, project_id,
|
||||
current=current,
|
||||
last_known_good=last_known_good)
|
||||
return
|
||||
p = self.doc["projects"][project_id]
|
||||
p["runtime_generation"] = current
|
||||
if last_known_good:
|
||||
@@ -112,6 +172,9 @@ class ProjectRegistry:
|
||||
Registry stores repo_identity as "git:<path>" or a bare path.
|
||||
Scaffold storage is NEVER used as a repo-cwd proxy. Raises for
|
||||
unknown projects (refuse mutation rather than guessing)."""
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
return core.resolve_cwd(self.doc, project_id)
|
||||
entry = self.get(project_id)
|
||||
if entry is None:
|
||||
raise ValueError(
|
||||
@@ -134,6 +197,9 @@ class ProjectRegistry:
|
||||
def needs_migration(self, proposed_schema_version: int) -> list[str]:
|
||||
"""Projects whose structural schema is older than a proposed
|
||||
version (feeds the 18I consent sweep — never silent upgrades)."""
|
||||
core = _pr_core()
|
||||
if core is not None:
|
||||
return core.needs_migration(self.doc, proposed_schema_version)
|
||||
return sorted(pid for pid, p in self.doc["projects"].items()
|
||||
if p.get("structural_schema_version", 1)
|
||||
< proposed_schema_version)
|
||||
Generated
+23
-2
@@ -2,6 +2,28 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[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"
|
||||
@@ -27,7 +49,6 @@ dependencies = [
|
||||
name = "project_registry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -47,7 +68,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -76,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,5 +7,8 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# preserve_order: Python-dict insertion-order semantics for the registry
|
||||
# doc and the register() entry literal;
|
||||
# float_roundtrip: correctly-rounded float parsing (schema versions and
|
||||
# validation counts ride along verbatim).
|
||||
serde_json = { version = "1", features = ["preserve_order", "float_roundtrip"] }
|
||||
@@ -0,0 +1,142 @@
|
||||
//! project_registry branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_pr_truth.json: 36 cases across every public function and
|
||||
//! every branch — fresh-registry default doc, register (defaults, full
|
||||
//! kwargs, overwrite, second-project preservation), get (present /
|
||||
//! missing / empty registry), all_projects sorting (empty / single /
|
||||
//! out-of-order insertion), set_health (valid with and without notes,
|
||||
//! 160-CHAR note truncation, note appending, invalid health → ValueError,
|
||||
//! unknown project → KeyError), set_outstanding_validations (set /
|
||||
//! overwrite / unknown project → KeyError), mark_generation (with and
|
||||
//! without last_known_good, unknown project → KeyError), resolve_cwd
|
||||
//! (git: prefix strip, bare path, active-worktree override, whitespace
|
||||
//! repo refused, unknown project refused, no repo_identity refused),
|
||||
//! needs_migration (mixed sorted / none older / default version 1 /
|
||||
//! empty registry)).
|
||||
//! Frozen 2026-10-12 via scripts/gen_project_registry_truth.py with
|
||||
//! RUNE_PR_CORE=python pinned (poisoned-truth law) and the wall clock
|
||||
//! frozen at 2026-10-12T12:00:00Z (epoch 1791806400). Embedded VERBATIM —
|
||||
//! never transcribed. The `load` and `save` groups are facade-only (FILE
|
||||
//! IO stays in Python) and are not replayed here.
|
||||
|
||||
use crate::{
|
||||
all_projects, default_doc, get, mark_generation, needs_migration, register, resolve_cwd,
|
||||
set_health, set_outstanding,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_pr_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 doc_of(i: &Value) -> Map<String, Value> {
|
||||
i["doc"].as_object().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn s<'a>(i: &'a Value, k: &str) -> &'a str {
|
||||
i[k].as_str().unwrap()
|
||||
}
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn replay_result(group: &str, f: impl Fn(&Value) -> Result<Value, crate::RegistryError>) {
|
||||
replay(group, |i| match f(i) {
|
||||
Ok(v) => v,
|
||||
Err(e) => json!({"error": e.to_string()}),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_doc_parity_battery() {
|
||||
replay("default_doc", |i| {
|
||||
json!({"doc": default_doc(s(i, "now"))})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_parity_battery() {
|
||||
replay("register", |i| {
|
||||
let (doc, entry) = register(
|
||||
&doc_of(i),
|
||||
s(i, "project_id"),
|
||||
s(i, "scaffold_location"),
|
||||
s(i, "repo_identity"),
|
||||
s(i, "forge_repo"),
|
||||
s(i, "default_branch"),
|
||||
&i["structural_schema_version"],
|
||||
s(i, "runtime_generation"),
|
||||
s(i, "last_known_good"),
|
||||
s(i, "now"),
|
||||
)
|
||||
.unwrap();
|
||||
json!({"doc": doc, "entry": entry})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_parity_battery() {
|
||||
replay("get", |i| {
|
||||
json!({"entry": get(&doc_of(i), s(i, "project_id")).unwrap()})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_projects_parity_battery() {
|
||||
replay("all_projects", |i| {
|
||||
json!({"projects": all_projects(&doc_of(i)).unwrap()})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_health_parity_battery() {
|
||||
replay_result("set_health", |i| {
|
||||
set_health(&doc_of(i), s(i, "project_id"), s(i, "health"), s(i, "note"),
|
||||
s(i, "now"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_outstanding_parity_battery() {
|
||||
replay_result("set_outstanding", |i| {
|
||||
set_outstanding(&doc_of(i), s(i, "project_id"), i["count"].as_i64().unwrap())
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_generation_parity_battery() {
|
||||
replay_result("mark_generation", |i| {
|
||||
mark_generation(&doc_of(i), s(i, "project_id"), s(i, "current"),
|
||||
s(i, "last_known_good"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_cwd_parity_battery() {
|
||||
replay_result("resolve_cwd", |i| {
|
||||
resolve_cwd(&doc_of(i), s(i, "project_id"))
|
||||
.map(|cwd| json!({"cwd": cwd}))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_migration_parity_battery() {
|
||||
replay("needs_migration", |i| {
|
||||
json!({"ids": needs_migration(&doc_of(i), i["proposed"].as_i64().unwrap()).unwrap()})
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,74 +1,461 @@
|
||||
//! project_registry — known project registry (W6).
|
||||
//! PARITY port of src/rune/project_registry.py.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
//! project_registry — canonical registry of scaffolded projects (T19
|
||||
//! §19C).
|
||||
//!
|
||||
//! FULL parity port of src/rune/project_registry.py (dichotomy wave,
|
||||
//! #2479 tranche). The registry is DISCOVERY/INDEX only (what projects
|
||||
//! exist, where their truth lives, health/migration state); a project's
|
||||
//! structural truth stays in ITS store, and Spine never absorbs it
|
||||
//! (boundary rule 19C).
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
|
||||
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
|
||||
//! * pr_default_doc({now}) → {doc}
|
||||
//! * pr_register({doc, project_id, scaffold_location, repo_identity,
|
||||
//! forge_repo, default_branch, structural_schema_version,
|
||||
//! runtime_generation, last_known_good, now}) → {doc, entry}
|
||||
//! * pr_get({doc, project_id}) → {entry} (null → unknown project)
|
||||
//! * pr_all_projects({doc}) → {projects} (sorted by project_id)
|
||||
//! * pr_set_health({doc, project_id, health, note, now}) → {doc}
|
||||
//! * pr_set_outstanding({doc, project_id, count}) → {doc}
|
||||
//! * pr_mark_generation({doc, project_id, current, last_known_good})
|
||||
//! → {doc}
|
||||
//! * pr_resolve_cwd({doc, project_id}) → {cwd}
|
||||
//! * pr_needs_migration({doc, proposed}) → {ids}
|
||||
//!
|
||||
//! Error envelope: oracle exceptions ride the `err` string with their
|
||||
//! Python kind prefix — "ValueError: <msg>" / "KeyError: <key>" — so the
|
||||
//! ctypes loader re-raises the SAME exception type the oracle raised.
|
||||
//!
|
||||
//! No-clock law: `now` (UTC ISO-8601) is always an argument to the
|
||||
//! stamping calls; the kernel never reads the wall clock.
|
||||
//!
|
||||
//! Boundary: the registry FILE IO (_load hit branch / save — including
|
||||
//! the updated_at stamp and the indent=1/sort_keys dump) stays in the
|
||||
//! Python facade; the kernel is a pure function of the loaded doc.
|
||||
//!
|
||||
//! No pr_free_string export — sibling crates sharing the Python process
|
||||
//! already export identical free symbols; callers free through
|
||||
//! capability_scoring's cs_free_string.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectEntry {
|
||||
pub project_id: String,
|
||||
pub scaffold_location: String,
|
||||
pub registered_at: String,
|
||||
pub description: String,
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::fmt;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
pub const REGISTRY_SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
// project health states (operational: drives /testing and migration
|
||||
// consent sweeps)
|
||||
pub const HEALTHY: &str = "HEALTHY";
|
||||
pub const DRIFTED: &str = "DRIFTED";
|
||||
pub const MIGRATION_REQUIRED: &str = "MIGRATION_REQUIRED";
|
||||
pub const UNKNOWN: &str = "UNKNOWN";
|
||||
|
||||
/// The four legal health states — the oracle's ValueError lattice.
|
||||
const LEGAL_HEALTH: [&str; 4] = [HEALTHY, DRIFTED, MIGRATION_REQUIRED, UNKNOWN];
|
||||
|
||||
// ── oracle exceptions (typed, behind the doc envelope) ──────────────────────
|
||||
|
||||
/// The oracle raises ValueError (validation/refusal) or KeyError (missing
|
||||
/// registry entry). The envelope's `err` string carries the Python kind
|
||||
/// prefix so the loader re-raises the same exception type.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RegistryError {
|
||||
Value(String),
|
||||
Key(String),
|
||||
}
|
||||
|
||||
pub struct ProjectRegistry {
|
||||
pub projects: BTreeMap<String, ProjectEntry>,
|
||||
}
|
||||
|
||||
impl ProjectRegistry {
|
||||
pub fn new() -> Self {
|
||||
ProjectRegistry {
|
||||
projects: BTreeMap::new(),
|
||||
impl fmt::Display for RegistryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RegistryError::Value(m) => write!(f, "ValueError: {m}"),
|
||||
RegistryError::Key(k) => write!(f, "KeyError: {k}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
project_id: &str,
|
||||
scaffold_location: &str,
|
||||
description: &str,
|
||||
) -> ProjectEntry {
|
||||
let entry = ProjectEntry {
|
||||
project_id: project_id.to_string(),
|
||||
scaffold_location: scaffold_location.to_string(),
|
||||
registered_at: now(),
|
||||
description: description.to_string(),
|
||||
};
|
||||
self.projects.insert(project_id.to_string(), entry.clone());
|
||||
entry
|
||||
fn unknown_health(health: &str) -> RegistryError {
|
||||
RegistryError::Value(format!("unknown health '{health}'"))
|
||||
}
|
||||
|
||||
fn unknown_project(project_id: &str) -> RegistryError {
|
||||
RegistryError::Value(format!(
|
||||
"unknown project '{project_id}' — resolve identity before \
|
||||
mutation; never execute from HOME"
|
||||
))
|
||||
}
|
||||
|
||||
fn no_repo_identity(project_id: &str) -> RegistryError {
|
||||
RegistryError::Value(format!(
|
||||
"project '{project_id}' has no repo_identity — no local \
|
||||
execution checkout available"
|
||||
))
|
||||
}
|
||||
|
||||
fn projects_of(doc: &Map<String, Value>) -> Result<&Map<String, Value>, RegistryError> {
|
||||
doc.get("projects")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| RegistryError::Key("projects".to_string()))
|
||||
}
|
||||
|
||||
fn projects_mut(doc: &mut Map<String, Value>) -> Result<&mut Map<String, Value>, RegistryError> {
|
||||
doc.get_mut("projects")
|
||||
.and_then(Value::as_object_mut)
|
||||
.ok_or_else(|| RegistryError::Key("projects".to_string()))
|
||||
}
|
||||
|
||||
fn truncate_chars(s: &str, n: usize) -> String {
|
||||
s.chars().take(n).collect()
|
||||
}
|
||||
|
||||
// ── registry operations (pure functions of the loaded doc) ──────────────────
|
||||
|
||||
/// `_load` miss branch — the fresh registry document.
|
||||
pub fn default_doc(now: &str) -> Value {
|
||||
json!({"schema_version": REGISTRY_SCHEMA_VERSION,
|
||||
"projects": {}, "updated_at": now})
|
||||
}
|
||||
|
||||
/// `register` — index a scaffolded project. References only — the
|
||||
/// project's structural truth stays in ITS store (boundary rule).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn register(
|
||||
doc: &Map<String, Value>,
|
||||
project_id: &str,
|
||||
scaffold_location: &str,
|
||||
repo_identity: &str,
|
||||
forge_repo: &str,
|
||||
default_branch: &str,
|
||||
structural_schema_version: &Value,
|
||||
runtime_generation: &str,
|
||||
last_known_good: &str,
|
||||
now: &str,
|
||||
) -> Result<(Map<String, Value>, Value), RegistryError> {
|
||||
let mut out = doc.clone();
|
||||
let entry = json!({
|
||||
"project_id": project_id,
|
||||
"scaffold_location": scaffold_location,
|
||||
"repo_identity": repo_identity,
|
||||
"forge_repo": forge_repo,
|
||||
"default_branch": default_branch,
|
||||
"structural_schema_version": structural_schema_version,
|
||||
"scaffold_state": {"health": UNKNOWN, "last_material_update": now},
|
||||
"runtime_generation": runtime_generation,
|
||||
"last_known_good": last_known_good,
|
||||
"outstanding_validations": 0,
|
||||
"registered_at": now,
|
||||
});
|
||||
projects_mut(&mut out)?.insert(project_id.to_string(), entry.clone());
|
||||
Ok((out, entry))
|
||||
}
|
||||
|
||||
/// `get` — registry lookup; null for unknown projects (the facade maps
|
||||
/// JSON null back to Python None).
|
||||
pub fn get(doc: &Map<String, Value>, project_id: &str) -> Result<Value, RegistryError> {
|
||||
Ok(projects_of(doc)?
|
||||
.get(project_id)
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
/// `all_projects` — every entry, sorted by project_id (Python `sorted`).
|
||||
pub fn all_projects(doc: &Map<String, Value>) -> Result<Vec<Value>, RegistryError> {
|
||||
let mut projects: Vec<Value> = projects_of(doc)?.values().cloned().collect();
|
||||
projects.sort_by_key(|p| {
|
||||
p.get("project_id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
});
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
/// `set_health` — validate against the health lattice first (ValueError
|
||||
/// beats KeyError when both would fire), then stamp the material update
|
||||
/// and append the (160-char-truncated) note when present.
|
||||
pub fn set_health(
|
||||
doc: &Map<String, Value>,
|
||||
project_id: &str,
|
||||
health: &str,
|
||||
note: &str,
|
||||
now: &str,
|
||||
) -> Result<Map<String, Value>, RegistryError> {
|
||||
if !LEGAL_HEALTH.contains(&health) {
|
||||
return Err(unknown_health(health));
|
||||
}
|
||||
let mut out = doc.clone();
|
||||
let p = projects_mut(&mut out)?
|
||||
.get_mut(project_id)
|
||||
.ok_or_else(|| RegistryError::Key(project_id.to_string()))?;
|
||||
let state = p
|
||||
.get_mut("scaffold_state")
|
||||
.and_then(Value::as_object_mut)
|
||||
.ok_or_else(|| RegistryError::Key("scaffold_state".to_string()))?;
|
||||
state.insert("health".to_string(), json!(health));
|
||||
state
|
||||
.insert("last_material_update".to_string(), json!(now));
|
||||
if !note.is_empty() {
|
||||
let notes = state
|
||||
.entry("notes".to_string())
|
||||
.or_insert_with(|| json!([]));
|
||||
if let Some(list) = notes.as_array_mut() {
|
||||
list.push(json!({
|
||||
"at": now,
|
||||
"note": truncate_chars(note, 160),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn get(&self, project_id: &str) -> Option<&ProjectEntry> {
|
||||
self.projects.get(project_id)
|
||||
/// `set_outstanding_validations` — the /testing aggregate writes back the
|
||||
/// outstanding count per project.
|
||||
pub fn set_outstanding(
|
||||
doc: &Map<String, Value>,
|
||||
project_id: &str,
|
||||
count: i64,
|
||||
) -> Result<Map<String, Value>, RegistryError> {
|
||||
let mut out = doc.clone();
|
||||
let p = projects_mut(&mut out)?
|
||||
.get_mut(project_id)
|
||||
.ok_or_else(|| RegistryError::Key(project_id.to_string()))?
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| RegistryError::Key(project_id.to_string()))?;
|
||||
p.insert("outstanding_validations".to_string(), json!(count));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `mark_generation` — runtime generation bump; last_known_good only
|
||||
/// moves when a non-empty value is supplied.
|
||||
pub fn mark_generation(
|
||||
doc: &Map<String, Value>,
|
||||
project_id: &str,
|
||||
current: &str,
|
||||
last_known_good: &str,
|
||||
) -> Result<Map<String, Value>, RegistryError> {
|
||||
let mut out = doc.clone();
|
||||
let p = projects_mut(&mut out)?
|
||||
.get_mut(project_id)
|
||||
.ok_or_else(|| RegistryError::Key(project_id.to_string()))?
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| RegistryError::Key(project_id.to_string()))?;
|
||||
p.insert("runtime_generation".to_string(), json!(current));
|
||||
if !last_known_good.is_empty() {
|
||||
p.insert("last_known_good".to_string(), json!(last_known_good));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `resolve_cwd` — T21.2 §21.2B: canonical REPOSITORY/worktree execution
|
||||
/// cwd for a project — NOT scaffold_location. Precedence (21.2B-3):
|
||||
/// explicit contract/worktree cwd > programme cwd > registry repo cwd >
|
||||
/// unresolved/refuse. Registry stores repo_identity as "git:<path>" or a
|
||||
/// bare path. Scaffold storage is NEVER used as a repo-cwd proxy. Raises
|
||||
/// for unknown projects (refuse mutation rather than guessing).
|
||||
pub fn resolve_cwd(
|
||||
doc: &Map<String, Value>,
|
||||
project_id: &str,
|
||||
) -> Result<String, RegistryError> {
|
||||
let entry = projects_of(doc)?
|
||||
.get(project_id)
|
||||
.ok_or_else(|| unknown_project(project_id))?;
|
||||
let mut repo = entry
|
||||
.get("repo_identity")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if let Some(stripped) = repo.strip_prefix("git:") {
|
||||
repo = stripped.to_string();
|
||||
}
|
||||
let repo = repo.trim().to_string();
|
||||
if repo.is_empty() {
|
||||
return Err(no_repo_identity(project_id));
|
||||
}
|
||||
// an active worktree overrides the repo root (21.2B-3 precedence)
|
||||
let wt = entry
|
||||
.get("active_worktree")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
if !wt.is_empty() {
|
||||
return Ok(wt.to_string());
|
||||
}
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
/// `needs_migration` — projects whose structural schema is older than a
|
||||
/// proposed version (feeds the 18I consent sweep — never silent
|
||||
/// upgrades). Entries default to schema version 1.
|
||||
pub fn needs_migration(doc: &Map<String, Value>, proposed: i64) -> Result<Vec<String>, RegistryError> {
|
||||
let mut ids: Vec<String> = projects_of(doc)?
|
||||
.iter()
|
||||
.filter(|(_, p)| {
|
||||
p.get("structural_schema_version")
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(1)
|
||||
< proposed
|
||||
})
|
||||
.map(|(pid, _)| pid.clone())
|
||||
.collect();
|
||||
ids.sort();
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────────
|
||||
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
Registry(RegistryError),
|
||||
}
|
||||
|
||||
impl fmt::Display for FfiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FfiError::NullPtr => write!(f, "null argument pointer"),
|
||||
FfiError::Utf8(e) => write!(f, "{e}"),
|
||||
FfiError::Json(e) => write!(f, "{e}"),
|
||||
FfiError::Registry(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProjectRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
|
||||
if ptr.is_null() {
|
||||
return Err(FfiError::NullPtr);
|
||||
}
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(FfiError::Utf8)?;
|
||||
serde_json::from_str(s).map_err(FfiError::Json)
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
format!(
|
||||
"{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
)
|
||||
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e.to_string()}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn doc_of(a: &Value) -> Map<String, Value> {
|
||||
a.get("doc")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// pr_default_doc({now}) → {doc}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_default_doc(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let now = a.get("now").and_then(Value::as_str).unwrap_or("");
|
||||
json!({"doc": default_doc(now)})
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_register({doc, project_id, scaffold_location, repo_identity,
|
||||
/// forge_repo, default_branch, structural_schema_version,
|
||||
/// runtime_generation, last_known_good, now}) → {doc, entry}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_register(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
register(
|
||||
&doc_of(&a),
|
||||
s("project_id"),
|
||||
s("scaffold_location"),
|
||||
s("repo_identity"),
|
||||
s("forge_repo"),
|
||||
s("default_branch"),
|
||||
a.get("structural_schema_version")
|
||||
.unwrap_or(&Value::Null),
|
||||
s("runtime_generation"),
|
||||
s("last_known_good"),
|
||||
s("now"),
|
||||
)
|
||||
.map(|(doc, entry)| json!({"doc": doc, "entry": entry}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_get({doc, project_id}) → {entry}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_get(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
get(&doc_of(&a), s("project_id"))
|
||||
.map(|entry| json!({"entry": entry}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_all_projects({doc}) → {projects}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_all_projects(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
all_projects(&doc_of(&a))
|
||||
.map(|projects| json!({"projects": projects}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_set_health({doc, project_id, health, note, now}) → {doc}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_set_health(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
set_health(&doc_of(&a), s("project_id"), s("health"), s("note"), s("now"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_set_outstanding({doc, project_id, count}) → {doc}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_set_outstanding(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
let count = a.get("count").and_then(Value::as_i64).unwrap_or(0);
|
||||
set_outstanding(&doc_of(&a), s("project_id"), count)
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_mark_generation({doc, project_id, current, last_known_good}) → {doc}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_mark_generation(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
mark_generation(&doc_of(&a), s("project_id"), s("current"), s("last_known_good"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_resolve_cwd({doc, project_id}) → {cwd}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_resolve_cwd(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
resolve_cwd(&doc_of(&a), s("project_id"))
|
||||
.map(|cwd| json!({"cwd": cwd}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
/// pr_needs_migration({doc, proposed}) → {ids}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pr_needs_migration(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let proposed = a.get("proposed").and_then(Value::as_i64).unwrap_or(0);
|
||||
needs_migration(&doc_of(&a), proposed)
|
||||
.map(|ids| json!({"ids": ids}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_register_and_get() {
|
||||
let mut reg = ProjectRegistry::new();
|
||||
reg.register("test-project", "/tmp/test", "A test project");
|
||||
let entry = reg.get("test-project");
|
||||
assert!(entry.is_some());
|
||||
assert_eq!(entry.unwrap().scaffold_location, "/tmp/test");
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
Reference in new issue
Block a user