feat(capability_ledger): port to FULL Rust — dichotomy wave
FULL parity port of src/rune/capability_ledger.py to the capability_ledger Rust core (RUST verdict, issue #2479 tranche, contract BUILDER-2026-10-11T15:50:51Z-bde08d-factory-wave): - 17 FFI symbols (cl_new/cl_define/cl_get/cl_material_change/cl_move/ cl_supersede/cl_retire/cl_active/cl_by_component/cl_context_packet/ cl_save/cl_classify_version_bump/cl_bump_version/cl_attach_spec/ cl_revise_spec/cl_spec_drift_status/cl_spec_context) via doc-envelope law; the ledger doc travels as a sibling envelope key so "value" stays oracle-verbatim; free via capability_scoring cs_free_string (no re-export); serde_json preserve_order for dict-position parity - stale partial port replaced (old CapabilityEntry struct had no Python counterpart); classify_version_bump/bump_version kept name-compatible - REAL divergences fixed pre-wiring (battery caught all three): MINOR/PATCH bumps must normalize untouched components through int() (leading zeros collapse everywhere — '01.02.03' MINOR = '1.3.0'); bump arithmetic is arbitrary-precision like Python ints (u64/u128 boundary); SEMVER_RE \\d matches Unicode Nd + int() accepts them (full Nd range table, verified against unicodedata) - py_repr() mirrors Python repr() in error envelopes (str(KeyError) is repr'd); note/reason truncation by codepoints (py_slice) - ctypes loader cl_loader.py mirrors broker_loader.py (LEDGER_RUST_LIB override); facade delegation on all 18 public fns with RUNE_LEDGER_CORE =python pin and Python-body fallback (kernel errors fall back so the proper exception types re-raise) - battery: 16 sequences / 155 ops frozen from the live oracle (frozen_cl_truth.json, replayed VERBATIM in-crate) + save() byte-parity pinned (indent=1 sort_keys identical); 500-op live fuzz seed 2479: 500/500 + final-file-bytes equal - FIX-FIRST (pre-existing latent regression surfaced by this batch): spine unseeded path bypassed the monkeypatchable ingest_umbrellas seam once the rune_spine kernel was built — the generated-view refusal fired inside the kernel where isolation tests cannot intercept it (test_spine_seeds_entry_point red on master with kernel built). build() now routes the manifest ingest through the public seam first and hands the kernel a carry-attempted doc; the kernel skips its manifest branch when existing was provided as an object (true cold path keeps the load-bearing refusal). 198 ledger/spine Python tests green.
This commit is contained in:
1 parent
59b98c7576
commit
31fb68b58d
10 files changed
+6184
-122
No files matched your search
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze capability_ledger oracle truth for the Rust port battery
|
||||
(dichotomy factory batch, #2479 tranche).
|
||||
|
||||
Exercises EVERY public function across all branches: CapabilityLedger
|
||||
(new/load, save, define defaults + full + duplicate, get prefixed/plain/
|
||||
unknown, material_change revisions + generation + source_locations + note
|
||||
truncation, move, supersede (plain + prefixed by, note truncation), retire,
|
||||
active, by_component, context_packet known/unknown) plus the module-level
|
||||
spec machinery (classify_version_bump full 4-bool matrix, bump_version
|
||||
valid/invalid/leading-zero/overflow, attach_spec defaults + errors,
|
||||
revise_spec classification + proposed-kind governance + unjustified bump +
|
||||
KeyError + covers_material_revision, spec_drift_status all outcomes,
|
||||
spec_context with/without spec).
|
||||
|
||||
The wall clock is FROZEN: every op records the exact _now() value it was
|
||||
given, and the kernel receives that same string via the FFI payload — the
|
||||
kernel itself never reads a clock.
|
||||
|
||||
Output: /tmp/cl_truth.json — replayed VERBATIM by
|
||||
src/rust/capability_ledger/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_LEDGER_CORE"] = "python" # freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import capability_ledger as cl # noqa: E402
|
||||
|
||||
# ── deterministic frozen clock ───────────────────────────────────────────
|
||||
_T0 = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
_tick = [0]
|
||||
|
||||
|
||||
def _next_now() -> str:
|
||||
_tick[0] += 1
|
||||
return (_T0 + timedelta(seconds=_tick[0] - 1)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _pin_clock(now: str) -> None:
|
||||
"""All _now() calls WITHIN one op return the same value (the kernel
|
||||
receives exactly one now per op; live wall-clock is same-second)."""
|
||||
cl._now = lambda n=now: n # noqa: E731
|
||||
|
||||
|
||||
class Seq:
|
||||
"""One replayable op sequence against one ledger document."""
|
||||
|
||||
def __init__(self, name: str, sequences: list):
|
||||
self.name = name
|
||||
self.ops: list[dict] = []
|
||||
self.expected: list[dict] = []
|
||||
self.saved_files: dict[str, str] = {}
|
||||
self.ledger: cl.CapabilityLedger | None = None
|
||||
self.tmpdir = tempfile.mkdtemp(prefix=f"cltruth-{name}-")
|
||||
sequences.append(self)
|
||||
|
||||
# -- op plumbing -------------------------------------------------------
|
||||
|
||||
def _call(self, opname: str, **args) -> None:
|
||||
op = {"op": opname, **args}
|
||||
now = _next_now()
|
||||
_pin_clock(now)
|
||||
if opname not in ("spec_context", "classify_version_bump",
|
||||
"bump_version"):
|
||||
op["now"] = now
|
||||
self.ops.append(op)
|
||||
try:
|
||||
out = getattr(self, f"_{opname}")(op)
|
||||
# oracle returns LIVE references (rec dicts) — snapshot NOW or
|
||||
# later ops mutate the frozen expectation
|
||||
out = json.loads(json.dumps(out))
|
||||
self.expected.append({"ok": True, "value": out})
|
||||
except Exception as e: # ValueError/SpecError/KeyError — message parity
|
||||
self.expected.append({"ok": False, "err": str(e)})
|
||||
|
||||
def _new(self, op) -> dict:
|
||||
file_text = op.get("file_text")
|
||||
if file_text is not None:
|
||||
(Path(self.tmpdir) / "capability_ledger.json").write_text(file_text)
|
||||
self.ledger = cl.CapabilityLedger(self.tmpdir)
|
||||
return self.ledger.doc
|
||||
|
||||
def _define(self, op) -> dict:
|
||||
kwargs = {k: v for k, v in op.items()
|
||||
if k not in ("op", "now")}
|
||||
return self.ledger.define(**kwargs)
|
||||
|
||||
def _get(self, op) -> dict | None:
|
||||
return self.ledger.get(op["capability_id"])
|
||||
|
||||
def _material_change(self, op) -> dict:
|
||||
kwargs = {"note": op["note"]}
|
||||
if "generation" in op:
|
||||
kwargs["generation"] = op["generation"]
|
||||
if "source_locations" in op:
|
||||
kwargs["source_locations"] = op["source_locations"]
|
||||
return self.ledger.material_change(op["capability_id"], **kwargs)
|
||||
|
||||
def _move(self, op) -> dict:
|
||||
return self.ledger.move(op["capability_id"], op["new_locations"])
|
||||
|
||||
def _supersede(self, op) -> dict:
|
||||
return self.ledger.supersede(op["capability_id"], op["by"],
|
||||
note=op.get("note", ""))
|
||||
|
||||
def _retire(self, op) -> dict:
|
||||
return self.ledger.retire(op["capability_id"],
|
||||
note=op.get("note", ""))
|
||||
|
||||
def _active(self, op) -> list:
|
||||
return self.ledger.active()
|
||||
|
||||
def _by_component(self, op) -> list:
|
||||
return self.ledger.by_component(op["component"])
|
||||
|
||||
def _context_packet(self, op) -> dict:
|
||||
return self.ledger.context_packet(op["capability_id"])
|
||||
|
||||
def _save(self, op) -> None:
|
||||
self.ledger.save()
|
||||
self.saved_files["capability_ledger.json"] = (
|
||||
Path(self.tmpdir) / "capability_ledger.json").read_text()
|
||||
return None
|
||||
|
||||
def _classify_version_bump(self, op) -> str:
|
||||
return cl.classify_version_bump(
|
||||
contract_changed=op["contract_changed"],
|
||||
behaviour_added=op["behaviour_added"],
|
||||
consumer_break=op["consumer_break"],
|
||||
implementation_only=op["implementation_only"])
|
||||
|
||||
def _bump_version(self, op) -> str:
|
||||
return cl.bump_version(op["version"], op["kind"])
|
||||
|
||||
def _attach_spec(self, op) -> dict:
|
||||
kwargs = {k: v for k, v in op.items()
|
||||
if k not in ("op", "now", "capability_id")}
|
||||
return cl.attach_spec(self.ledger, op["capability_id"], **kwargs)
|
||||
|
||||
def _revise_spec(self, op) -> dict:
|
||||
kwargs = {k: v for k, v in op.items()
|
||||
if k not in ("op", "now", "capability_id")}
|
||||
return cl.revise_spec(self.ledger, op["capability_id"], **kwargs)
|
||||
|
||||
def _spec_drift_status(self, op) -> dict:
|
||||
return cl.spec_drift_status(self.ledger, op["capability_id"])
|
||||
|
||||
def _spec_context(self, op) -> dict:
|
||||
rec = self.ledger.get(op["capability_id"])
|
||||
return cl.spec_context(rec)
|
||||
|
||||
|
||||
sequences: list[Seq] = []
|
||||
LONG_NOTE = "x" * 260
|
||||
LONG_REASON = "r" * 260
|
||||
|
||||
|
||||
def frozen_json(doc: dict) -> str:
|
||||
"""A saved ledger file EXACTLY as save() writes it (indent=1, sorted)."""
|
||||
return json.dumps(doc, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
# ── S1: fresh ledger — defaults, queries, save ───────────────────────────
|
||||
s = Seq("fresh-defaults-queries", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="auth-token", name="Auth Token",
|
||||
purpose="issue + verify session tokens", owning_project="rune",
|
||||
component="component:auth-core")
|
||||
s._call("define", capability_id="cap:prefixed-given", name="Prefixed",
|
||||
purpose="id given with prefix", owning_project="rune",
|
||||
component="component:auth-core")
|
||||
s._call("get", capability_id="auth-token")
|
||||
s._call("get", capability_id="capability:auth-token")
|
||||
s._call("get", capability_id="nope")
|
||||
s._call("active")
|
||||
s._call("by_component", component="component:auth-core")
|
||||
s._call("by_component", component="component:nowhere")
|
||||
s._call("context_packet", capability_id="auth-token")
|
||||
s._call("context_packet", capability_id="ghost")
|
||||
s._call("save")
|
||||
|
||||
# ── S2: define with every field provided ─────────────────────────────────
|
||||
s = Seq("define-full-fields", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="full-rec", name="Full", purpose="all fields",
|
||||
owning_project="rune", component="component:c1",
|
||||
source_locations=["src/a.py", "src/b.py"],
|
||||
contract="contract text", state_touched="store/scaffold",
|
||||
dependencies=["dep-x"], consumers=["consumer-y"],
|
||||
invariants=["inv-1"], behaviour_specs=["bs-1"],
|
||||
constraints="no raw tokens", generation="gen-7")
|
||||
s._call("define", capability_id="empty-lists", name="Empty", purpose="empties",
|
||||
owning_project="rune", component="component:c1",
|
||||
source_locations=[], dependencies=[], consumers=[],
|
||||
invariants=[], behaviour_specs=[])
|
||||
s._call("get", capability_id="full-rec")
|
||||
s._call("get", capability_id="empty-lists")
|
||||
s._call("context_packet", capability_id="full-rec")
|
||||
|
||||
# ── S3: duplicate define raises ──────────────────────────────────────────
|
||||
s = Seq("duplicate-define", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="dup", name="D", purpose="first",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("define", capability_id="dup", name="D2", purpose="second",
|
||||
owning_project="rune", component="component:c")
|
||||
|
||||
# ── S4: unknown-capability errors for every mutator/query ───────────────
|
||||
s = Seq("unknown-cap-errors", sequences)
|
||||
s._call("new")
|
||||
s._call("material_change", capability_id="ghost", note="n")
|
||||
s._call("move", capability_id="ghost", new_locations=["a.py"])
|
||||
s._call("supersede", capability_id="ghost", by="other")
|
||||
s._call("retire", capability_id="ghost")
|
||||
s._call("attach_spec", capability_id="ghost", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=["n"], acceptance_criteria=["a"])
|
||||
s._call("revise_spec", capability_id="ghost", reason="x")
|
||||
s._call("spec_drift_status", capability_id="ghost")
|
||||
|
||||
# ── S5: supersede / retire / move lifecycle ──────────────────────────────
|
||||
s = Seq("supersede-retire-move", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="old-cap", name="Old", purpose="p",
|
||||
owning_project="rune", component="component:c",
|
||||
source_locations=["src/old.py"])
|
||||
s._call("define", capability_id="doomed", name="Doomed", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("supersede", capability_id="old-cap", by="new-cap", note="replaced")
|
||||
s._call("supersede", capability_id="doomed", by="capability:explicit-prefixed",
|
||||
note=LONG_NOTE)
|
||||
s._call("retire", capability_id="doomed", note="dead")
|
||||
s._call("retire", capability_id="doomed", note=LONG_NOTE) # already retired: still appends history
|
||||
s._call("active")
|
||||
s._call("move", capability_id="old-cap", new_locations=["src/moved/old.py", "src/moved/old2.py"])
|
||||
s._call("context_packet", capability_id="old-cap") # packet still served for superseded
|
||||
s._call("by_component", component="component:c")
|
||||
|
||||
# ── S6: material_change revision counter + branches ─────────────────────
|
||||
s = Seq("material-change-revisions", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="rev-cap", name="Rev", purpose="p",
|
||||
owning_project="rune", component="component:c",
|
||||
source_locations=["src/a.py"], constraints="c0")
|
||||
s._call("material_change", capability_id="rev-cap", note="first change")
|
||||
s._call("material_change", capability_id="rev-cap", note=LONG_NOTE,
|
||||
generation="gen-9")
|
||||
s._call("material_change", capability_id="rev-cap", note="third",
|
||||
generation="", source_locations=["src/b.py"])
|
||||
s._call("material_change", capability_id="rev-cap", note="fourth",
|
||||
source_locations=[])
|
||||
s._call("get", capability_id="rev-cap")
|
||||
s._call("context_packet", capability_id="rev-cap")
|
||||
|
||||
# ── S7: classify_version_bump — full 4-bool matrix ───────────────────────
|
||||
s = Seq("classify-bump-matrix", sequences)
|
||||
s._call("new")
|
||||
for cc in (False, True):
|
||||
for ba in (False, True):
|
||||
for cb in (False, True):
|
||||
for io in (False, True):
|
||||
s._call("classify_version_bump", contract_changed=cc,
|
||||
behaviour_added=ba, consumer_break=cb,
|
||||
implementation_only=io)
|
||||
|
||||
# ── S8: bump_version — valid / invalid / zeros / overflow ────────────────
|
||||
s = Seq("bump-version-cases", sequences)
|
||||
s._call("new")
|
||||
for version, kind in [
|
||||
("0.1.0", "MAJOR"), ("0.1.0", "MINOR"), ("0.1.0", "PATCH"),
|
||||
("1.2.3", "MAJOR"), ("1.2.3", "MINOR"), ("1.2.3", "PATCH"),
|
||||
("0.0.9", "PATCH"), ("999.999.999", "MAJOR"), ("01.02.03", "MINOR"),
|
||||
("007.0.0", "PATCH"),
|
||||
("18446744073709551615.0.0", "MAJOR"), # u64 boundary
|
||||
("99999999999999999999999999.0.0", "PATCH"), # beyond u128
|
||||
("1.2", "PATCH"), ("1.2.3.4", "PATCH"), ("abc", "MAJOR"),
|
||||
("", "PATCH"), ("1..3", "MINOR"), ("-1.2.3", "MAJOR"),
|
||||
("1.2.x", "PATCH"), (" 1.2.3", "PATCH"), ("1.2.3 ", "PATCH"),
|
||||
("1.2.3", "BOGUS"), ("1.2.3", ""), ("1.2.3", "major"),
|
||||
]:
|
||||
s._call("bump_version", version=version, kind=kind)
|
||||
|
||||
# ── S9: attach_spec — defaults + explicit + errors ───────────────────────
|
||||
s = Seq("attach-spec-branches", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="specd", name="S", purpose="spec'd",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("spec_drift_status", capability_id="specd") # no spec attached
|
||||
s._call("attach_spec", capability_id="specd", requirements=["r1", "r2"],
|
||||
expected_behaviours=["b1"], non_goals=["ng1"],
|
||||
acceptance_criteria=["ac1"])
|
||||
s._call("spec_drift_status", capability_id="specd") # baseline covers rev 0
|
||||
s._call("define", capability_id="specd2", name="S2", purpose="x",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="specd2", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[],
|
||||
baseline_version="1.0.0", baseline_reason="stable, evidenced",
|
||||
governance_version_started_at="2025-12-31T23:59:59Z")
|
||||
s._call("get", capability_id="specd2")
|
||||
# errors
|
||||
s._call("define", capability_id="badbase", name="B", purpose="x",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="badbase", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[],
|
||||
baseline_version="1.0")
|
||||
s._call("attach_spec", capability_id="badbase", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[],
|
||||
baseline_version="not-semver")
|
||||
s._call("attach_spec", capability_id="ghost", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[])
|
||||
|
||||
# ── S10: revise_spec — classification, governance, drift closure ─────────
|
||||
s = Seq("revise-spec-governance", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="gov", name="G", purpose="governed",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="gov", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=["n"], acceptance_criteria=["a"])
|
||||
s._call("material_change", capability_id="gov", note="uncovered rev 1")
|
||||
s._call("spec_drift_status", capability_id="gov") # DRIFT: rev 1 > covered 0
|
||||
s._call("revise_spec", capability_id="gov", implementation_only=True,
|
||||
reason="internal tidy-up")
|
||||
s._call("spec_drift_status", capability_id="gov") # drift closed
|
||||
s._call("material_change", capability_id="gov", note="rev 2")
|
||||
s._call("material_change", capability_id="gov", note="rev 3")
|
||||
s._call("spec_drift_status", capability_id="gov") # 2 uncovered
|
||||
s._call("revise_spec", capability_id="gov", behaviour_added=True,
|
||||
reason=LONG_REASON, changed_sections=["requirements"],
|
||||
affected_consumers=["consumer-x"],
|
||||
validation_consequences=["retest suite B"])
|
||||
s._call("spec_drift_status", capability_id="gov")
|
||||
s._call("revise_spec", capability_id="gov", consumer_break=True,
|
||||
reason="breaking contract change")
|
||||
s._call("spec_drift_status", capability_id="gov")
|
||||
s._call("get", capability_id="gov")
|
||||
|
||||
# ── S11: revise_spec — proposed-kind governance branches ─────────────────
|
||||
s = Seq("revise-proposed-kind", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="pk", name="PK", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="pk", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=["n"], acceptance_criteria=["a"])
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="ok patch, proposed matches", proposed_kind="PATCH")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="patch proposed but minor earned", proposed_kind="MINOR")
|
||||
s._call("revise_spec", capability_id="pk", behaviour_added=True,
|
||||
reason="minor earned, downgraded to patch on purpose",
|
||||
proposed_kind="PATCH")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="major proposed for metadata churn — unjustified",
|
||||
proposed_kind="MAJOR")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="minor proposed for metadata churn — unjustified",
|
||||
proposed_kind="MINOR")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="garbage proposed kind", proposed_kind="HUGE")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="empty proposed kind falls back to classify",
|
||||
proposed_kind="")
|
||||
s._call("revise_spec", capability_id="pk", reason="no flags at all — churn")
|
||||
s._call("get", capability_id="pk")
|
||||
|
||||
# ── S12: revise_spec on a capability with no spec ────────────────────────
|
||||
s = Seq("revise-no-spec", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="bare", name="B", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("revise_spec", capability_id="bare", implementation_only=True,
|
||||
reason="x")
|
||||
|
||||
# ── S13: save/reload roundtrip (sorted file bytes pinned) ────────────────
|
||||
s = Seq("save-reload-roundtrip", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="zzz-last", name="Z", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("define", capability_id="aaa-first", name="A", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("save")
|
||||
saved = s.saved_files["capability_ledger.json"]
|
||||
s2 = Seq("reload-from-saved-file", sequences)
|
||||
s2._call("new", file_text=saved)
|
||||
s2._call("active") # sorted-key order from the file
|
||||
s2._call("get", capability_id="aaa-first")
|
||||
s2._call("save")
|
||||
|
||||
# ── S14: unicode + repr edge cases ───────────────────────────────────────
|
||||
s = Seq("unicode-repr-edges", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="café-x", name="Café", purpose="rôle",
|
||||
owning_project="rune", component="component:coffeé")
|
||||
s._call("context_packet", capability_id="café-x")
|
||||
s._call("material_change", capability_id="café-x", note="naïve change → ünïcode ✓")
|
||||
s._call("material_change", capability_id="ghost-cap", note="x")
|
||||
s._call("bump_version", version="café.1.0", kind="PATCH")
|
||||
s._call("context_packet", capability_id="quo'te")
|
||||
s._call("context_packet", capability_id='dq"te')
|
||||
|
||||
# ── S15: spec_context with and without spec ──────────────────────────────
|
||||
s = Seq("spec-context", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="ctx-bare", name="CB", purpose="purpose-y",
|
||||
owning_project="rune", component="component:c",
|
||||
contract="contract-y", invariants=["i1"], behaviour_specs=["bs1"])
|
||||
s._call("spec_context", capability_id="ctx-bare") # no spec — empty spec half
|
||||
s._call("attach_spec", capability_id="ctx-bare", requirements=["req1"],
|
||||
expected_behaviours=["eb1"], non_goals=["ngo1"],
|
||||
acceptance_criteria=["ac1"])
|
||||
s._call("spec_context", capability_id="ctx-bare")
|
||||
s._call("material_change", capability_id="ctx-bare", note="m")
|
||||
s._call("spec_context", capability_id="ctx-bare") # last_bump = BASELINE entry
|
||||
|
||||
truth = {
|
||||
"frozen": "capability_ledger full public surface + spec machinery",
|
||||
"oracle": "src/rune/capability_ledger.py",
|
||||
"note": "Every op records the exact _now() it was given; the kernel "
|
||||
"receives that string via the FFI payload and never reads a "
|
||||
"clock. saved_files pin save() byte output (Python facade "
|
||||
"serializes with indent=1 sort_keys=True).",
|
||||
"sequences": [
|
||||
{"name": s.name, "ops": s.ops, "expected": s.expected,
|
||||
"saved_files": s.saved_files,
|
||||
"final_doc": s.ledger.doc}
|
||||
for s in sequences
|
||||
],
|
||||
}
|
||||
|
||||
out = Path("/tmp/cl_truth.json")
|
||||
out.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
||||
n_ops = sum(len(s.ops) for s in sequences)
|
||||
print(f"froze {len(sequences)} sequences / {n_ops} ops -> {out}")
|
||||
@@ -15,6 +15,7 @@ the history list.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -28,6 +29,22 @@ def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _kernel():
|
||||
"""The Rust ledger core (cl_loader) when RUNE_LEDGER_CORE allows it,
|
||||
else None — every public function delegates there first and falls
|
||||
back to the Python body below (same process, same semantics; the
|
||||
kernel returns the oracle output verbatim plus the updated doc)."""
|
||||
if os.environ.get("RUNE_LEDGER_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cl_loader
|
||||
if cl_loader.available():
|
||||
return cl_loader
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class CapabilityLedger:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "capability_ledger.json"
|
||||
@@ -37,13 +54,26 @@ class CapabilityLedger:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.new(_now())["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return {"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capabilities": {}, "history": [],
|
||||
"updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
self.doc = k.save(self.doc, _now())["doc"]
|
||||
except Exception:
|
||||
self.doc["updated_at"] = _now()
|
||||
else:
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
|
||||
@@ -61,6 +91,23 @@ class CapabilityLedger:
|
||||
generation: str = "") -> dict:
|
||||
"""Create a capability record (auto-invoked by Scaffolding when a
|
||||
meaningful capability is created — rule 19Q)."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.define(self.doc, _now(), capability_id=capability_id,
|
||||
name=name, purpose=purpose,
|
||||
owning_project=owning_project,
|
||||
component=component,
|
||||
source_locations=source_locations,
|
||||
contract=contract, state_touched=state_touched,
|
||||
dependencies=dependencies, consumers=consumers,
|
||||
invariants=invariants,
|
||||
behaviour_specs=behaviour_specs,
|
||||
constraints=constraints, generation=generation)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass # kernel err (or absent) — Python body re-raises properly
|
||||
cid = f"capability:{capability_id}"
|
||||
if cid in self.doc["capabilities"]:
|
||||
raise ValueError(f"capability exists: {cid}")
|
||||
@@ -88,6 +135,12 @@ class CapabilityLedger:
|
||||
return rec
|
||||
|
||||
def get(self, capability_id: str) -> dict | None:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.get(self.doc, capability_id)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return self.doc["capabilities"].get(
|
||||
capability_id if ":" in capability_id
|
||||
else f"capability:{capability_id}")
|
||||
@@ -98,6 +151,16 @@ class CapabilityLedger:
|
||||
"""Record a material change (auto-invoked after construction —
|
||||
rule 19Q.2). History entry keeps traceability; dependents' harden
|
||||
state is the behaviour ledger's job (via structural closure)."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.material_change(self.doc, _now(), capability_id,
|
||||
note=note, generation=generation,
|
||||
source_locations=source_locations)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -124,6 +187,14 @@ class CapabilityLedger:
|
||||
def move(self, capability_id: str, new_locations: list[str]) -> dict:
|
||||
"""File move: STABLE IDENTITY SURVIVES (rule 19S-6); the move is
|
||||
recorded, history preserved."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.move(self.doc, _now(), capability_id, new_locations)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -137,6 +208,15 @@ class CapabilityLedger:
|
||||
|
||||
def supersede(self, capability_id: str, by: str, *,
|
||||
note: str = "") -> dict:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.supersede(self.doc, _now(), capability_id, by,
|
||||
note=note)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -150,6 +230,14 @@ class CapabilityLedger:
|
||||
return rec
|
||||
|
||||
def retire(self, capability_id: str, *, note: str = "") -> dict:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.retire(self.doc, _now(), capability_id, note=note)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -163,15 +251,33 @@ class CapabilityLedger:
|
||||
# -- queries (surgical context, 19E) -----------------------------------
|
||||
|
||||
def active(self) -> list[dict]:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.active(self.doc)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return [r for r in self.doc["capabilities"].values()
|
||||
if r["maturity"] not in ("retired", "superseded")]
|
||||
|
||||
def by_component(self, component: str) -> list[dict]:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.by_component(self.doc, component)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return [r for r in self.active() if r["component"] == component]
|
||||
|
||||
def context_packet(self, capability_id: str) -> dict:
|
||||
"""The LOCAL context packet (19E): everything a fresh builder
|
||||
needs for one capability — bounded, structured, no reasoning."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.context_packet(self.doc, capability_id)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
return {"error": f"unknown capability {capability_id!r}"}
|
||||
@@ -227,6 +333,16 @@ def classify_version_bump(*, contract_changed: bool,
|
||||
implementation_only: bool) -> str:
|
||||
"""§21D-3: derive PATCH/MINOR/MAJOR from the change shape. The model
|
||||
may propose; the machinery verifies."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.classify_version_bump(
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
if consumer_break:
|
||||
return "MAJOR"
|
||||
if contract_changed:
|
||||
@@ -241,6 +357,12 @@ def classify_version_bump(*, contract_changed: bool,
|
||||
|
||||
|
||||
def bump_version(version: str, kind: str) -> str:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.bump_version(version, kind)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
m = SEMVER_RE.match(version)
|
||||
if not m:
|
||||
raise SpecError(f"invalid semver {version!r}")
|
||||
@@ -267,6 +389,21 @@ def attach_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
the starting version is explicit — 1.0.0 only for stable contracts
|
||||
with sufficient evidence; otherwise 0.x with a reason."""
|
||||
from rune.capability_ledger import LEDGER_SCHEMA_VERSION # noqa: F401
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.attach_spec(ledger.doc, _now(), capability_id,
|
||||
requirements=requirements,
|
||||
expected_behaviours=expected_behaviours,
|
||||
non_goals=non_goals,
|
||||
acceptance_criteria=acceptance_criteria,
|
||||
baseline_version=baseline_version,
|
||||
baseline_reason=baseline_reason,
|
||||
governance_version_started_at=governance_version_started_at)
|
||||
ledger.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -307,6 +444,23 @@ def revise_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
The model proposes; the machinery classifies and records. Material
|
||||
change without a bump is impossible (every revise computes one);
|
||||
unjustified bumps are impossible (no-change revise raises)."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.revise_spec(ledger.doc, _now(), capability_id,
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only,
|
||||
reason=reason,
|
||||
changed_sections=changed_sections,
|
||||
affected_consumers=affected_consumers,
|
||||
validation_consequences=validation_consequences,
|
||||
proposed_kind=proposed_kind)
|
||||
ledger.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -357,6 +511,12 @@ def spec_drift_status(ledger: "CapabilityLedger",
|
||||
→ SPEC_VERSION_DRIFT + durable obligation
|
||||
|
||||
Returns {"drift": bool, "obligation": str|None, "detail": ...}."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.spec_drift_status(ledger.doc, capability_id)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
return {"drift": False, "obligation": None,
|
||||
@@ -389,6 +549,12 @@ def spec_drift_status(ledger: "CapabilityLedger",
|
||||
def spec_context(rec: dict) -> dict:
|
||||
"""The spec half of a surgical context packet (§21M): WHY + WHAT
|
||||
SHOULD EXIST, compact."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.spec_context(rec)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
s = rec.get("spec") or {}
|
||||
return {
|
||||
"capability_id": rec["capability_id"],
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust capability_ledger core (dichotomy factory
|
||||
batch, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cl_new({now, file_text?}) -> doc
|
||||
* cl_define({doc, now, ...define kwargs}) -> {"record", "doc"}
|
||||
* cl_get({doc, capability_id}) -> record|null
|
||||
* cl_material_change({doc, now, ...}) -> {"record", "doc"}
|
||||
* cl_move({doc, now, capability_id, new_locations}) -> {"record", "doc"}
|
||||
* cl_supersede({doc, now, capability_id, by, note?}) -> {"record", "doc"}
|
||||
* cl_retire({doc, now, capability_id, note?}) -> {"record", "doc"}
|
||||
* cl_active({doc}) / cl_by_component({doc, component}) -> [records]
|
||||
* cl_context_packet({doc, capability_id}) -> packet dict
|
||||
* cl_save({doc, now}) -> doc (stamped)
|
||||
* cl_classify_version_bump({4 bools}) -> "PATCH"|"MINOR"|"MAJOR"
|
||||
* cl_bump_version({version, kind}) -> new version
|
||||
* cl_attach_spec({doc, now, ...}) -> {"spec", "doc"}
|
||||
* cl_revise_spec({doc, now, ...}) -> {"result", "doc"}
|
||||
* cl_spec_drift_status({doc, capability_id}) -> status dict
|
||||
* cl_spec_context({rec}) -> packet dict
|
||||
|
||||
The doc is the FULL ledger document ({"schema_version", "capabilities",
|
||||
"history", "updated_at"}); mutators return the updated doc so the facade
|
||||
keeps authoritative state. Free: capability_scoring's cs_free_string
|
||||
(shared process) — NEVER re-export. Override the .so with
|
||||
LEDGER_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" / "capability_ledger"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("LEDGER_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcapability_ledger.so")
|
||||
)
|
||||
|
||||
SYMBOLS = (
|
||||
"cl_new", "cl_define", "cl_get", "cl_material_change", "cl_move",
|
||||
"cl_supersede", "cl_retire", "cl_active", "cl_by_component",
|
||||
"cl_context_packet", "cl_save", "cl_classify_version_bump",
|
||||
"cl_bump_version", "cl_attach_spec", "cl_revise_spec",
|
||||
"cl_spec_drift_status", "cl_spec_context",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class LedgerCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust ledger 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 LedgerCoreUnavailable(str(_LIB_PATH))
|
||||
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
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict) -> dict:
|
||||
"""Returns the FULL success envelope: {"value": ..., "doc": ...} for
|
||||
mutators (the updated ledger doc travels beside the oracle output so
|
||||
the facade can thread self.doc), {"value": ...} for queries. Errors
|
||||
raise RuntimeError(err) — the facade's except-path falls back to the
|
||||
Python body, which re-raises the proper exception type."""
|
||||
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 = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc
|
||||
|
||||
|
||||
def new(now: str, file_text: str | None = None) -> dict:
|
||||
payload: dict = {"now": now}
|
||||
if file_text is not None:
|
||||
payload["file_text"] = file_text
|
||||
return _call("cl_new", payload)
|
||||
|
||||
|
||||
def define(doc: dict, now: str, **kwargs) -> dict:
|
||||
return _call("cl_define", {"doc": doc, "now": now, **kwargs})
|
||||
|
||||
|
||||
def get(doc: dict, capability_id: str) -> dict | None:
|
||||
return _call("cl_get", {"doc": doc, "capability_id": capability_id})
|
||||
|
||||
|
||||
def material_change(doc: dict, now: str, capability_id: str, note: str,
|
||||
generation: str = "", source_locations=None) -> dict:
|
||||
payload = {"doc": doc, "now": now, "capability_id": capability_id,
|
||||
"note": note, "generation": generation}
|
||||
if source_locations is not None:
|
||||
payload["source_locations"] = source_locations
|
||||
return _call("cl_material_change", payload)
|
||||
|
||||
|
||||
def move(doc: dict, now: str, capability_id: str, new_locations: list) -> dict:
|
||||
return _call("cl_move", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id,
|
||||
"new_locations": new_locations})
|
||||
|
||||
|
||||
def supersede(doc: dict, now: str, capability_id: str, by: str,
|
||||
note: str = "") -> dict:
|
||||
return _call("cl_supersede", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id,
|
||||
"by": by, "note": note})
|
||||
|
||||
|
||||
def retire(doc: dict, now: str, capability_id: str, note: str = "") -> dict:
|
||||
return _call("cl_retire", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id, "note": note})
|
||||
|
||||
|
||||
def active(doc: dict) -> list:
|
||||
return _call("cl_active", {"doc": doc})
|
||||
|
||||
|
||||
def by_component(doc: dict, component: str) -> list:
|
||||
return _call("cl_by_component", {"doc": doc, "component": component})
|
||||
|
||||
|
||||
def context_packet(doc: dict, capability_id: str) -> dict:
|
||||
return _call("cl_context_packet", {"doc": doc, "capability_id": capability_id})
|
||||
|
||||
|
||||
def save(doc: dict, now: str) -> dict:
|
||||
return _call("cl_save", {"doc": doc, "now": now})
|
||||
|
||||
|
||||
def classify_version_bump(*, contract_changed: bool, behaviour_added: bool,
|
||||
consumer_break: bool,
|
||||
implementation_only: bool) -> str:
|
||||
return _call("cl_classify_version_bump", {
|
||||
"contract_changed": contract_changed,
|
||||
"behaviour_added": behaviour_added,
|
||||
"consumer_break": consumer_break,
|
||||
"implementation_only": implementation_only,
|
||||
})
|
||||
|
||||
|
||||
def bump_version(version: str, kind: str) -> str:
|
||||
return _call("cl_bump_version", {"version": version, "kind": kind})
|
||||
|
||||
|
||||
def attach_spec(doc: dict, now: str, capability_id: str, **kwargs) -> dict:
|
||||
return _call("cl_attach_spec", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id, **kwargs})
|
||||
|
||||
|
||||
def revise_spec(doc: dict, now: str, capability_id: str, **kwargs) -> dict:
|
||||
return _call("cl_revise_spec", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id, **kwargs})
|
||||
|
||||
|
||||
def spec_drift_status(doc: dict, capability_id: str) -> dict:
|
||||
return _call("cl_spec_drift_status", {"doc": doc,
|
||||
"capability_id": capability_id})
|
||||
|
||||
|
||||
def spec_context(rec: dict) -> dict:
|
||||
return _call("cl_spec_context", {"rec": rec})
|
||||
+15
-5
@@ -602,12 +602,22 @@ def build(manifest_path: Path | None = None) -> dict:
|
||||
strangler law: the old path stays load-bearing until deletion is proven)."""
|
||||
inputs = _gather_build_inputs(manifest_path)
|
||||
core = _rust_core()
|
||||
# The unseeded path is the one-time Phase A migration: it stays on the
|
||||
# Python core so the public ingest_umbrellas seam (monkeypatched by
|
||||
# isolation tests) is honored byte-for-byte. Steady-state (seeded)
|
||||
# builds delegate to the Rust core; the parity battery covers BOTH
|
||||
# paths, including an explicit unseeded-ingest parity section.
|
||||
# The unseeded path is the one-time Phase A migration: the public
|
||||
# ingest_umbrellas seam stays authoritative for it (isolation tests
|
||||
# monkeypatch it; the file-driven refusal contract lives there).
|
||||
# Route the manifest ingest through the seam FIRST, then delegate the
|
||||
# rest of the projection with the ingested doc carried as `existing` —
|
||||
# the kernel then takes the carry-forward branch (its own manifest
|
||||
# ingest would bypass the seam and raise the refusal the caller is
|
||||
# entitled to intercept; regression caught by
|
||||
# test_t214_material_closure.test_spine_seeds_entry_point).
|
||||
seeded = bool(inputs["existing"]) and any(r["type"] == "belongs_to" for r in inputs["existing"]["relationships"])
|
||||
if not seeded and core is not None:
|
||||
seed_doc = new_doc()
|
||||
mp = inputs.get("manifest_path")
|
||||
ingest_umbrellas(seed_doc, Path(mp) if mp else None)
|
||||
inputs = dict(inputs, existing=seed_doc, manifest=None)
|
||||
seeded = bool(seed_doc["entities"]) or bool(seed_doc["relationships"])
|
||||
doc = core.project(inputs) if core is not None and seeded else _py_build_from_inputs(inputs)
|
||||
# both cores funnel through the same post-projection passes (the
|
||||
# contract-discovery write-back + language placement) — the doc-level
|
||||
|
||||
Generated
+23
-2
@@ -6,10 +6,31 @@ version = 4
|
||||
name = "capability_ledger"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -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,4 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
@@ -0,0 +1,163 @@
|
||||
//! capability_ledger branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_cl_truth.json, 16 sequences / 155 ops across every
|
||||
//! public function and branch, frozen 2026-10-11). Embedded VERBATIM —
|
||||
//! never transcribed.
|
||||
//!
|
||||
//! The battery threads the ledger document exactly like the Python
|
||||
//! facade does: mutators return the updated doc, queries don't mutate.
|
||||
|
||||
use crate::{
|
||||
op_active, op_attach_spec, op_bump_version, op_by_component,
|
||||
op_classify_version_bump, op_context_packet, op_define, op_get,
|
||||
op_material_change, op_move, op_new, op_retire, op_revise_spec,
|
||||
op_save, op_spec_context, op_spec_drift_status, op_supersede,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_cl_truth.json");
|
||||
|
||||
fn run_op(doc: &Value, op: &Value) -> (Value, Option<Value>) {
|
||||
let name = op["op"].as_str().unwrap();
|
||||
let mut payload = op.clone();
|
||||
if name != "new" {
|
||||
payload["doc"] = doc.clone();
|
||||
}
|
||||
if name == "spec_context" {
|
||||
// the facade passes the rec it got from get(); resolve it here
|
||||
let cid = op["capability_id"].as_str().unwrap_or("");
|
||||
let key = if cid.contains(':') {
|
||||
cid.to_string()
|
||||
} else {
|
||||
format!("capability:{cid}")
|
||||
};
|
||||
payload["rec"] = doc["capabilities"][&key].clone();
|
||||
}
|
||||
let r = match name {
|
||||
"new" => op_new(&payload),
|
||||
"define" => op_define(&payload),
|
||||
"get" => op_get(&payload),
|
||||
"material_change" => op_material_change(&payload),
|
||||
"move" => op_move(&payload),
|
||||
"supersede" => op_supersede(&payload),
|
||||
"retire" => op_retire(&payload),
|
||||
"active" => op_active(&payload),
|
||||
"by_component" => op_by_component(&payload),
|
||||
"context_packet" => op_context_packet(&payload),
|
||||
"save" => op_save(&payload),
|
||||
"classify_version_bump" => op_classify_version_bump(&payload),
|
||||
"bump_version" => op_bump_version(&payload),
|
||||
"attach_spec" => op_attach_spec(&payload),
|
||||
"revise_spec" => op_revise_spec(&payload),
|
||||
"spec_drift_status" => op_spec_drift_status(&payload),
|
||||
"spec_context" => op_spec_context(&payload),
|
||||
other => panic!("unknown op {other}"),
|
||||
};
|
||||
match r {
|
||||
Ok((value, next)) => {
|
||||
let mut m = Map::new();
|
||||
m.insert("ok".into(), json!(true));
|
||||
m.insert("value".into(), value.clone());
|
||||
if let Some(d) = &next {
|
||||
m.insert("doc".into(), d.clone());
|
||||
}
|
||||
// "new" returns the doc as its value; the battery threads it
|
||||
let next = if name == "new" { Some(value) } else { next };
|
||||
(Value::Object(m), next)
|
||||
}
|
||||
Err(e) => (json!({"ok": false, "err": e}), None),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_battery() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for seq in truth["sequences"].as_array().unwrap() {
|
||||
let name = seq["name"].as_str().unwrap();
|
||||
let ops = seq["ops"].as_array().unwrap();
|
||||
let expected = seq["expected"].as_array().unwrap();
|
||||
assert_eq!(ops.len(), expected.len(), "seq {name}: op/expected skew");
|
||||
let mut doc = Value::Null;
|
||||
for (i, (op, exp)) in ops.iter().zip(expected.iter()).enumerate() {
|
||||
let (envelope, next) = run_op(&doc, op);
|
||||
// the frozen contract is {"ok", "value"}; "doc" is kernel
|
||||
// transport for the facade's self.doc threading
|
||||
let mut contract = envelope.clone();
|
||||
contract.as_object_mut().unwrap().remove("doc");
|
||||
assert_eq!(
|
||||
&contract, exp,
|
||||
"seq '{name}' op {i} {:?}",
|
||||
serde_json::to_string(op).unwrap_or_default()
|
||||
);
|
||||
if let Some(d) = next {
|
||||
doc = d;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
&doc, &seq["final_doc"],
|
||||
"seq '{name}': final document divergence"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_bytes_pinned() {
|
||||
// saved_files pin the Python facade's save() bytes (indent=1,
|
||||
// sort_keys=True) — the kernel must return docs that serialize to
|
||||
// exactly them.
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for seq in truth["sequences"].as_array().unwrap() {
|
||||
if seq["saved_files"].as_object().unwrap().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut doc = Value::Null;
|
||||
for op in seq["ops"].as_array().unwrap() {
|
||||
let (_env, next) = run_op(&doc, op);
|
||||
if let Some(d) = next {
|
||||
doc = d;
|
||||
}
|
||||
}
|
||||
let sorted = sort_keys(&doc);
|
||||
assert_eq!(
|
||||
serde_json::to_string_pretty(&sorted)
|
||||
.map(pretty_to_py_indent)
|
||||
.ok(),
|
||||
seq["saved_files"]["capability_ledger.json"].as_str().map(str::to_string),
|
||||
"seq '{}': save() byte parity",
|
||||
seq["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild an object with sorted keys (Python sort_keys=True semantics).
|
||||
fn sort_keys(v: &Value) -> Value {
|
||||
match v {
|
||||
Value::Object(m) => {
|
||||
let mut keys: Vec<&String> = m.keys().collect();
|
||||
keys.sort();
|
||||
Value::Object(
|
||||
keys.into_iter()
|
||||
.map(|k| (k.clone(), sort_keys(&m[k])))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Value::Array(a) => Value::Array(a.iter().map(sort_keys).collect()),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// serde_json pretty-prints with 2-space indent — Python's indent=1 uses
|
||||
/// one space per level. Convert line by line.
|
||||
fn pretty_to_py_indent(s: String) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for line in s.lines() {
|
||||
let indent = line.len() - line.trim_start().len();
|
||||
for _ in 0..indent / 2 {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push_str(line.trim_start());
|
||||
out.push('\n');
|
||||
}
|
||||
// Python's json.dumps emits no trailing newline
|
||||
out.truncate(out.trim_end_matches('\n').len());
|
||||
out
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,68 +1,175 @@
|
||||
//! capability_ledger — capability tracking ledger (W6).
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
//! capability_ledger — compact versioned Capability Ledgers (T19 §19D):
|
||||
//! local re-entry context packets. A fresh LLM rehydrates the meaning of
|
||||
//! a target area after compaction/context loss/model swap/long absence
|
||||
//! WITHOUT rereading the repository.
|
||||
//!
|
||||
//! Identity rule: capability IDs are STABLE across file moves (name-
|
||||
//! based, not path-based); history is preserved on supersede/retire.
|
||||
//!
|
||||
//! FULL parity port of src/rune/capability_ledger.py (dichotomy factory
|
||||
//! batch, #2479 tranche). The ledger DOCUMENT travels across the
|
||||
//! boundary: mutators receive the current doc and return the updated doc
|
||||
//! alongside the op result — the kernel is pure, the Python facade owns
|
||||
//! file I/O and serialization (save() bytes: indent=1, sort_keys=True).
|
||||
//! Every op receives the exact `_now()` string the facade captured; the
|
||||
//! kernel never reads a clock.
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes one JSON payload pointer
|
||||
//! and returns {"ok": true, "value": ...} | {"ok": false, "err": ...}.
|
||||
//! Free: capability_scoring's cs_free_string (shared process) — this
|
||||
//! crate deliberately does NOT re-export a free symbol.
|
||||
//!
|
||||
//! Key-ordering law: the oracle's dicts are insertion-ordered; the kernel
|
||||
//! parses and emits JSON with serde_json's preserve_order (IndexMap) so
|
||||
//! dict-position semantics (new key appends, existing key updates in
|
||||
//! place) match Python exactly.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CapabilityEntry {
|
||||
pub name: String,
|
||||
pub provider: String,
|
||||
pub registered_at: String,
|
||||
pub active: bool,
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
/// Ops return (oracle-output, optional updated doc); the doc travels as a
|
||||
/// sibling "doc" key in the success envelope so "value" stays verbatim.
|
||||
pub type OpResult = Result<(Value, Option<Value>), String>;
|
||||
|
||||
const LEDGER_SCHEMA_VERSION: i64 = 1;
|
||||
const SPEC_SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
/// Python's `\d` in SEMVER_RE matches ANY Unicode Nd (decimal digit)
|
||||
/// codepoint, and int() accepts them — so must we. Nd runs are contiguous
|
||||
/// with digit value = codepoint - run start (verified against
|
||||
/// unicodedata across the whole BMP+ planes).
|
||||
const ND_RANGES: &[(u32, u32)] = &[
|
||||
(0x30, 0x39), (0x660, 0x669), (0x6F0, 0x6F9), (0x7C0, 0x7C9),
|
||||
(0x966, 0x96F), (0x9E6, 0x9EF), (0xA66, 0xA6F), (0xAE6, 0xAEF),
|
||||
(0xB66, 0xB6F), (0xBE6, 0xBEF), (0xC66, 0xC6F), (0xCE6, 0xCEF),
|
||||
(0xD66, 0xD6F), (0xDE6, 0xDEF), (0xE50, 0xE59), (0xED0, 0xED9),
|
||||
(0xF20, 0xF29), (0x1040, 0x1049), (0x1090, 0x1099), (0x17E0, 0x17E9),
|
||||
(0x1810, 0x1819), (0x1946, 0x194F), (0x19D0, 0x19D9), (0x1A80, 0x1A89),
|
||||
(0x1A90, 0x1A99), (0x1B50, 0x1B59), (0x1BB0, 0x1BB9), (0x1C40, 0x1C49),
|
||||
(0x1C50, 0x1C59), (0xA620, 0xA629), (0xA8D0, 0xA8D9), (0xA900, 0xA909),
|
||||
(0xA9D0, 0xA9D9), (0xA9F0, 0xA9F9), (0xAA50, 0xAA59), (0xABF0, 0xABF9),
|
||||
(0xFF10, 0xFF19), (0x104A0, 0x104A9), (0x10D30, 0x10D39),
|
||||
(0x10D40, 0x10D49), (0x11066, 0x1106F), (0x110F0, 0x110F9),
|
||||
(0x11136, 0x1113F), (0x111D0, 0x111D9), (0x112F0, 0x112F9),
|
||||
(0x11450, 0x11459), (0x114D0, 0x114D9), (0x11650, 0x11659),
|
||||
(0x116C0, 0x116C9), (0x116D0, 0x116D9), (0x116DA, 0x116E3),
|
||||
(0x11730, 0x11739), (0x118E0, 0x118E9), (0x11950, 0x11959),
|
||||
(0x11BF0, 0x11BF9), (0x11C50, 0x11C59), (0x11D50, 0x11D59),
|
||||
(0x11DA0, 0x11DA9), (0x11F50, 0x11F59), (0x16130, 0x16139),
|
||||
(0x16A60, 0x16A69), (0x16AC0, 0x16AC9), (0x16B50, 0x16B59),
|
||||
(0x16D70, 0x16D79), (0x1CCF0, 0x1CCF9), (0x1D7CE, 0x1D7D7),
|
||||
(0x1D7D8, 0x1D7E1), (0x1D7E2, 0x1D7EB), (0x1D7EC, 0x1D7F5),
|
||||
(0x1D7F6, 0x1D7FF), (0x1E140, 0x1E149), (0x1E2F0, 0x1E2F9),
|
||||
(0x1E4F0, 0x1E4F9), (0x1E5F1, 0x1E5FA), (0x1E950, 0x1E959),
|
||||
(0x1FBF0, 0x1FBF9),
|
||||
];
|
||||
|
||||
fn nd_digit(c: char) -> Option<u32> {
|
||||
let cp = c as u32;
|
||||
let idx = ND_RANGES
|
||||
.binary_search_by(|&(lo, hi)| {
|
||||
if cp < lo {
|
||||
std::cmp::Ordering::Greater
|
||||
} else if cp > hi {
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(cp - ND_RANGES[idx].0)
|
||||
}
|
||||
|
||||
pub struct CapabilityLedger {
|
||||
pub capabilities: BTreeMap<String, CapabilityEntry>,
|
||||
}
|
||||
|
||||
impl CapabilityLedger {
|
||||
pub fn new() -> Self {
|
||||
CapabilityLedger {
|
||||
capabilities: BTreeMap::new(),
|
||||
/// Python repr() of a str: quote preference (' unless the string contains
|
||||
/// ' and not "), standard one-char escapes, \xNN for ASCII control.
|
||||
fn py_repr(s: &str) -> String {
|
||||
let dq = s.contains('\'') && !s.contains('"');
|
||||
let quote = if dq { '"' } else { '\'' };
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push(quote);
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\x07' => out.push_str("\\a"),
|
||||
'\x08' => out.push_str("\\b"),
|
||||
'\x0b' => out.push_str("\\v"),
|
||||
'\x0c' => out.push_str("\\f"),
|
||||
c if c == quote => {
|
||||
out.push('\\');
|
||||
out.push(c);
|
||||
}
|
||||
c if (c as u32) < 0x20 || (c as u32) == 0x7f => {
|
||||
out.push_str(&format!("\\x{:02x}", c as u32));
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
pub fn register(&mut self, name: &str, provider: &str) {
|
||||
self.capabilities.insert(
|
||||
name.to_string(),
|
||||
CapabilityEntry {
|
||||
name: name.to_string(),
|
||||
provider: provider.to_string(),
|
||||
registered_at: now(),
|
||||
active: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
out.push(quote);
|
||||
out
|
||||
}
|
||||
|
||||
impl Default for CapabilityLedger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
/// Python str slicing `s[:n]` — by code points, not bytes.
|
||||
fn py_slice(s: &str, n: usize) -> String {
|
||||
s.chars().take(n).collect()
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
format!(
|
||||
"{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
)
|
||||
fn is_semver(version: &str) -> bool {
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
parts.len() == 3
|
||||
&& parts
|
||||
.iter()
|
||||
.all(|p| !p.is_empty() && p.chars().all(|c| nd_digit(c).is_some()))
|
||||
}
|
||||
|
||||
/// classify_version_bump — §21D-3: derive PATCH/MINOR/MAJOR from the
|
||||
/// change shape (oracle: capability_ledger.classify_version_bump).
|
||||
/// Precedence: consumer_break > contract_changed > behaviour_added >
|
||||
/// implementation_only; all-false is SpecError (metadata churn never bumps).
|
||||
pub fn classify_version_bump(
|
||||
contract_changed: bool,
|
||||
behaviour_added: bool,
|
||||
consumer_break: bool,
|
||||
implementation_only: bool,
|
||||
) -> Result<&'static str, String> {
|
||||
if consumer_break {
|
||||
return Ok("MAJOR");
|
||||
/// int() semantics on a digit string: leading zeros collapse ("007" →
|
||||
/// "7", "000" → "0").
|
||||
fn norm_component(part: &str) -> String {
|
||||
let digits: Vec<u32> = part.chars().map(|c| nd_digit(c).unwrap()).collect();
|
||||
let first_nz = digits
|
||||
.iter()
|
||||
.position(|&d| d != 0)
|
||||
.unwrap_or(digits.len() - 1);
|
||||
digits[first_nz..]
|
||||
.iter()
|
||||
.map(|d| char::from_digit(*d, 10).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Decimal +1 on a digit string with int() semantics ("007" → "8").
|
||||
/// Unbounded — no overflow, exactly like Python's arbitrary-precision ints.
|
||||
fn bump_component(part: &str) -> String {
|
||||
let digits: Vec<u32> = norm_component(part)
|
||||
.chars()
|
||||
.map(|c| nd_digit(c).unwrap())
|
||||
.collect();
|
||||
let mut ds = digits;
|
||||
let mut i = ds.len();
|
||||
loop {
|
||||
i -= 1;
|
||||
if ds[i] == 9 {
|
||||
ds[i] = 0;
|
||||
if i == 0 {
|
||||
ds.insert(0, 1);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ds[i] += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if contract_changed {
|
||||
ds.iter()
|
||||
.map(|d| char::from_digit(*d, 10).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn classify_raw(contract_changed: bool, behaviour_added: bool,
|
||||
consumer_break: bool, implementation_only: bool)
|
||||
-> Result<&'static str, String> {
|
||||
if consumer_break || contract_changed {
|
||||
return Ok("MAJOR");
|
||||
}
|
||||
if behaviour_added {
|
||||
@@ -71,68 +178,580 @@ pub fn classify_version_bump(
|
||||
if implementation_only {
|
||||
return Ok("PATCH");
|
||||
}
|
||||
Err("no version-worthy change: metadata churn does not bump".to_string())
|
||||
Err("no version-worthy change: metadata churn does not bump".into())
|
||||
}
|
||||
|
||||
/// bump_version — strict ^(\d+)\.(\d+)\.(\d+)$ (oracle SEMVER_RE);
|
||||
/// MAJOR resets minor+patch, MINOR resets patch. Errors mirror SpecError.
|
||||
pub fn bump_version(version: &str, kind: &str) -> Result<String, String> {
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
if parts.len() != 3
|
||||
|| parts
|
||||
.iter()
|
||||
.any(|p| p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()))
|
||||
{
|
||||
return Err(format!("invalid semver {:?}", version));
|
||||
}
|
||||
let nums: Vec<u64> = parts.iter().map(|p| p.parse().unwrap()).collect();
|
||||
fn bump_order(kind: &str) -> Result<u8, String> {
|
||||
match kind {
|
||||
"MAJOR" => Ok(format!("{}.0.0", nums[0] + 1)),
|
||||
"MINOR" => Ok(format!("{}.{}.0", nums[0], nums[1] + 1)),
|
||||
"PATCH" => Ok(format!("{}.{}.{}", nums[0], nums[1], nums[2] + 1)),
|
||||
_ => Err(format!("unknown bump kind {:?}", kind)),
|
||||
"PATCH" => Ok(0),
|
||||
"MINOR" => Ok(1),
|
||||
"MAJOR" => Ok(2),
|
||||
// str(KeyError) is repr(key)
|
||||
_ => Err(py_repr(kind)),
|
||||
}
|
||||
}
|
||||
|
||||
fn bump_version_str(version: &str, kind: &str) -> Result<String, String> {
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
if !is_semver(version) {
|
||||
return Err(format!("invalid semver {}", py_repr(version)));
|
||||
}
|
||||
match kind {
|
||||
"MAJOR" => Ok(format!("{}.0.0", bump_component(parts[0]))),
|
||||
"MINOR" => Ok(format!("{}.{}.0", norm_component(parts[0]), bump_component(parts[1]))),
|
||||
"PATCH" => Ok(format!(
|
||||
"{}.{}.{}",
|
||||
norm_component(parts[0]),
|
||||
norm_component(parts[1]),
|
||||
bump_component(parts[2])
|
||||
)),
|
||||
_ => Err(format!("unknown bump kind {}", py_repr(kind))),
|
||||
}
|
||||
}
|
||||
|
||||
// ── doc helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn resolve_key(_doc: &Value, capability_id: &str) -> String {
|
||||
if capability_id.contains(':') {
|
||||
capability_id.to_string()
|
||||
} else {
|
||||
format!("capability:{capability_id}")
|
||||
}
|
||||
}
|
||||
|
||||
fn ledger_get<'a>(doc: &'a Value, capability_id: &str) -> Option<&'a Value> {
|
||||
let key = resolve_key(doc, capability_id);
|
||||
doc.get("capabilities").and_then(|c| c.get(&key))
|
||||
}
|
||||
|
||||
fn ledger_take(doc: &mut Value, capability_id: &str)
|
||||
-> Result<(String, Value), String> {
|
||||
let key = resolve_key(doc, capability_id);
|
||||
match doc.get("capabilities").and_then(|c| c.get(&key)) {
|
||||
Some(rec) => Ok((key, rec.clone())),
|
||||
None => Err(format!("unknown capability {}", py_repr(capability_id))),
|
||||
}
|
||||
}
|
||||
|
||||
fn put_rec(doc: &mut Value, key: &str, rec: Value) {
|
||||
doc["capabilities"][key] = rec;
|
||||
}
|
||||
|
||||
fn push_history(doc: &mut Value, entry: Value) {
|
||||
doc["history"].as_array_mut().unwrap().push(entry);
|
||||
}
|
||||
|
||||
fn arg_str<'a>(a: &'a Value, k: &str, default: &'a str) -> &'a str {
|
||||
a.get(k).and_then(|v| v.as_str()).unwrap_or(default)
|
||||
}
|
||||
|
||||
fn arg_arr(a: &Value, k: &str) -> Value {
|
||||
a.get(k)
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.map(Value::Array)
|
||||
.unwrap_or_else(|| json!([]))
|
||||
}
|
||||
|
||||
fn arg_bool(a: &Value, k: &str) -> bool {
|
||||
a.get(k).and_then(|v| v.as_bool()).unwrap_or(false)
|
||||
}
|
||||
|
||||
// ── ops (the oracle, one fn per public Python function) ──────────────────
|
||||
|
||||
pub fn op_new(a: &Value) -> OpResult {
|
||||
let now = arg_str(a, "now", "");
|
||||
let doc = match a.get("file_text").and_then(|v| v.as_str()) {
|
||||
// _load: the file IS the doc (Python facade read it; we parse it)
|
||||
Some(t) => serde_json::from_str(t).map_err(|e| e.to_string())?,
|
||||
None => json!({
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capabilities": {},
|
||||
"history": [],
|
||||
"updated_at": now,
|
||||
}),
|
||||
};
|
||||
// the doc IS the value here; the battery threads it from "value"
|
||||
Ok((doc, None))
|
||||
}
|
||||
|
||||
pub fn op_define(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let cid = format!("capability:{}", arg_str(a, "capability_id", ""));
|
||||
if ledger_get(&doc, &cid).is_some() {
|
||||
return Err(format!("capability exists: {cid}"));
|
||||
}
|
||||
let rec = json!({
|
||||
"capability_id": cid,
|
||||
"name": arg_str(a, "name", ""),
|
||||
"purpose": arg_str(a, "purpose", ""),
|
||||
"owning_project": arg_str(a, "owning_project", ""),
|
||||
"component": arg_str(a, "component", ""),
|
||||
"source_locations": arg_arr(a, "source_locations"),
|
||||
"contract": arg_str(a, "contract", ""),
|
||||
"state_touched": arg_str(a, "state_touched", ""),
|
||||
"dependencies": arg_arr(a, "dependencies"),
|
||||
"consumers": arg_arr(a, "consumers"),
|
||||
"invariants": arg_arr(a, "invariants"),
|
||||
"behaviour_specs": arg_arr(a, "behaviour_specs"),
|
||||
"maturity": "implemented",
|
||||
"constraints": arg_str(a, "constraints", ""),
|
||||
"generation": arg_str(a, "generation", ""),
|
||||
"limitations": [],
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
});
|
||||
put_rec(&mut doc, &cid, rec.clone());
|
||||
Ok((rec, Some(doc)))
|
||||
}
|
||||
|
||||
pub fn op_get(a: &Value) -> OpResult {
|
||||
let doc = &a["doc"];
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
Ok((ledger_get(doc, cid).cloned().unwrap_or(Value::Null), None))
|
||||
}
|
||||
|
||||
pub fn op_material_change(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
// T21.1 §21.1D: deterministic revision counter, not wall-clock ordering
|
||||
let rev = rec
|
||||
.get("material_revision")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
rec["material_revision"] = json!(rev);
|
||||
push_history(
|
||||
&mut doc,
|
||||
json!({
|
||||
"capability_id": rec["capability_id"],
|
||||
"at": now,
|
||||
"event": "material_change",
|
||||
"note": py_slice(arg_str(a, "note", ""), 200),
|
||||
"generation": arg_str(a, "generation", ""),
|
||||
"prior_maturity": rec["maturity"],
|
||||
"material_revision": rev,
|
||||
}),
|
||||
);
|
||||
rec["updated_at"] = json!(now);
|
||||
let generation = arg_str(a, "generation", "");
|
||||
if !generation.is_empty() {
|
||||
rec["generation"] = json!(generation);
|
||||
}
|
||||
if let Some(sl) = a.get("source_locations") {
|
||||
if !sl.is_null() {
|
||||
rec["source_locations"] = sl.clone();
|
||||
}
|
||||
}
|
||||
put_rec(&mut doc, &key, rec.clone());
|
||||
Ok((rec, Some(doc)))
|
||||
}
|
||||
|
||||
pub fn op_move(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let old = rec["source_locations"].clone();
|
||||
let new_locations = a.get("new_locations").cloned().unwrap_or(json!([]));
|
||||
rec["source_locations"] = new_locations.clone();
|
||||
rec["updated_at"] = json!(now);
|
||||
push_history(
|
||||
&mut doc,
|
||||
json!({
|
||||
"capability_id": rec["capability_id"],
|
||||
"at": now,
|
||||
"event": "moved",
|
||||
"from": old,
|
||||
"to": new_locations,
|
||||
}),
|
||||
);
|
||||
put_rec(&mut doc, &key, rec.clone());
|
||||
Ok((rec, Some(doc)))
|
||||
}
|
||||
|
||||
pub fn op_supersede(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let by_in = arg_str(a, "by", "");
|
||||
let by = if by_in.contains(':') {
|
||||
by_in.to_string()
|
||||
} else {
|
||||
format!("capability:{by_in}")
|
||||
};
|
||||
rec["maturity"] = json!("superseded");
|
||||
rec["superseded_by"] = json!(by);
|
||||
rec["updated_at"] = json!(now);
|
||||
push_history(
|
||||
&mut doc,
|
||||
json!({
|
||||
"capability_id": rec["capability_id"],
|
||||
"at": now,
|
||||
"event": "superseded",
|
||||
"by": rec["superseded_by"],
|
||||
"note": py_slice(arg_str(a, "note", ""), 160),
|
||||
}),
|
||||
);
|
||||
put_rec(&mut doc, &key, rec.clone());
|
||||
Ok((rec, Some(doc)))
|
||||
}
|
||||
|
||||
pub fn op_retire(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
rec["maturity"] = json!("retired");
|
||||
rec["updated_at"] = json!(now);
|
||||
push_history(
|
||||
&mut doc,
|
||||
json!({
|
||||
"capability_id": rec["capability_id"],
|
||||
"at": now,
|
||||
"event": "retired",
|
||||
"note": py_slice(arg_str(a, "note", ""), 160),
|
||||
}),
|
||||
);
|
||||
put_rec(&mut doc, &key, rec.clone());
|
||||
Ok((rec, Some(doc)))
|
||||
}
|
||||
|
||||
fn is_active(rec: &Value) -> bool {
|
||||
let m = rec.get("maturity").and_then(|v| v.as_str()).unwrap_or("");
|
||||
m != "retired" && m != "superseded"
|
||||
}
|
||||
|
||||
pub fn op_active(a: &Value) -> OpResult {
|
||||
let caps = a["doc"]
|
||||
.get("capabilities")
|
||||
.and_then(|c| c.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_else(Map::new);
|
||||
Ok((
|
||||
Value::Array(caps.values().filter(|r| is_active(r)).cloned().collect()),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_by_component(a: &Value) -> OpResult {
|
||||
let component = arg_str(a, "component", "");
|
||||
let caps = a["doc"]
|
||||
.get("capabilities")
|
||||
.and_then(|c| c.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_else(Map::new);
|
||||
Ok((
|
||||
Value::Array(
|
||||
caps.values()
|
||||
.filter(|r| is_active(r))
|
||||
.filter(|r| {
|
||||
r.get("component").and_then(|v| v.as_str()) == Some(component)
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_context_packet(a: &Value) -> OpResult {
|
||||
let doc = &a["doc"];
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
let Some(rec) = ledger_get(doc, cid) else {
|
||||
return Ok((json!({"error": format!("unknown capability {}", py_repr(cid))}), None));
|
||||
};
|
||||
let g = |k: &str| rec.get(k).cloned().unwrap_or(Value::Null);
|
||||
Ok((json!({
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capability_id": g("capability_id"),
|
||||
"name": g("name"),
|
||||
"purpose": g("purpose"),
|
||||
"component": g("component"),
|
||||
"source_locations": g("source_locations"),
|
||||
"contract": g("contract"),
|
||||
"state_touched": g("state_touched"),
|
||||
"dependencies": g("dependencies"),
|
||||
"consumers": g("consumers"),
|
||||
"invariants": g("invariants"),
|
||||
"behaviour_specs": g("behaviour_specs"),
|
||||
"maturity": g("maturity"),
|
||||
"constraints": g("constraints"),
|
||||
"generation": g("generation"),
|
||||
"limitations": g("limitations"),
|
||||
}), None))
|
||||
}
|
||||
|
||||
pub fn op_save(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
doc["updated_at"] = json!(arg_str(a, "now", ""));
|
||||
// oracle save() returns None; the stamped doc travels as the sibling
|
||||
Ok((Value::Null, Some(doc)))
|
||||
}
|
||||
|
||||
pub fn op_classify_version_bump(a: &Value) -> OpResult {
|
||||
Ok((
|
||||
json!(classify_raw(
|
||||
arg_bool(a, "contract_changed"),
|
||||
arg_bool(a, "behaviour_added"),
|
||||
arg_bool(a, "consumer_break"),
|
||||
arg_bool(a, "implementation_only"),
|
||||
)?),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_bump_version(a: &Value) -> OpResult {
|
||||
Ok((
|
||||
json!(bump_version_str(arg_str(a, "version", ""), arg_str(a, "kind", ""))?),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_attach_spec(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let baseline_version = arg_str(a, "baseline_version", "0.1.0").to_string();
|
||||
if !is_semver(&baseline_version) {
|
||||
return Err(format!(
|
||||
"invalid baseline semver {}",
|
||||
py_repr(&baseline_version)
|
||||
));
|
||||
}
|
||||
let baseline_reason = arg_str(a, "baseline_reason", "initial governed baseline");
|
||||
let gvs = arg_str(a, "governance_version_started_at", now);
|
||||
let spec = json!({
|
||||
"schema_version": SPEC_SCHEMA_VERSION,
|
||||
"version": baseline_version,
|
||||
"governance_version_started_at": gvs,
|
||||
"baseline_reason": baseline_reason,
|
||||
"requirements": arg_arr(a, "requirements"),
|
||||
"expected_behaviours": arg_arr(a, "expected_behaviours"),
|
||||
"non_goals": arg_arr(a, "non_goals"),
|
||||
"acceptance_criteria": arg_arr(a, "acceptance_criteria"),
|
||||
"version_history": [{
|
||||
"at": now,
|
||||
"version": baseline_version,
|
||||
"kind": "BASELINE",
|
||||
"reason": baseline_reason,
|
||||
"changed_sections": [],
|
||||
"affected_consumers": [],
|
||||
"validation_consequences": [],
|
||||
}],
|
||||
});
|
||||
rec["spec"] = spec.clone();
|
||||
rec["updated_at"] = json!(now);
|
||||
put_rec(&mut doc, &key, rec);
|
||||
Ok((spec, Some(doc)))
|
||||
}
|
||||
|
||||
pub fn op_revise_spec(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
let mut spec = match rec.get("spec") {
|
||||
Some(s) if !s.is_null() => s.clone(),
|
||||
_ => {
|
||||
return Err(format!("{cid} has no spec — attach one first"));
|
||||
}
|
||||
};
|
||||
// the machinery classifies; the model may only PROPOSE (never upgrade)
|
||||
let actual = classify_raw(
|
||||
arg_bool(a, "contract_changed"),
|
||||
arg_bool(a, "behaviour_added"),
|
||||
arg_bool(a, "consumer_break"),
|
||||
arg_bool(a, "implementation_only"),
|
||||
)?;
|
||||
let proposed = arg_str(a, "proposed_kind", "");
|
||||
let kind = if proposed.is_empty() {
|
||||
actual.to_string()
|
||||
} else {
|
||||
if bump_order(proposed)? > bump_order(actual)? {
|
||||
return Err(format!(
|
||||
"proposed {proposed} exceeds change shape {actual} — unjustified bump"
|
||||
));
|
||||
}
|
||||
proposed.to_string()
|
||||
};
|
||||
let old = spec["version"].as_str().unwrap_or("").to_string();
|
||||
let new = bump_version_str(&old, &kind)?;
|
||||
spec["version"] = json!(new);
|
||||
spec["version_history"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!({
|
||||
"at": now,
|
||||
"version": new,
|
||||
"kind": kind,
|
||||
"reason": py_slice(arg_str(a, "reason", ""), 200),
|
||||
"changed_sections": arg_arr(a, "changed_sections"),
|
||||
"affected_consumers": arg_arr(a, "affected_consumers"),
|
||||
"validation_consequences": arg_arr(a, "validation_consequences"),
|
||||
// the governed decision COVERS all material revisions up to
|
||||
// and including the current one — drift closes deterministically
|
||||
"covers_material_revision": rec
|
||||
.get("material_revision")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0),
|
||||
}));
|
||||
rec["spec"] = spec;
|
||||
rec["updated_at"] = json!(now);
|
||||
put_rec(&mut doc, &key, rec);
|
||||
Ok((
|
||||
json!({"old": old, "new": new, "kind": kind, "reason": arg_str(a, "reason", "")}),
|
||||
Some(doc),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_spec_drift_status(a: &Value) -> OpResult {
|
||||
let doc = &a["doc"];
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
let Some(rec) = ledger_get(doc, cid) else {
|
||||
return Ok((json!({
|
||||
"drift": false, "obligation": Value::Null,
|
||||
"detail": format!("unknown capability {}", py_repr(cid)),
|
||||
}), None));
|
||||
};
|
||||
let Some(spec) = rec.get("spec").filter(|s| !s.is_null()) else {
|
||||
return Ok((json!({
|
||||
"drift": false, "obligation": Value::Null,
|
||||
"detail": "no spec attached — nothing to drift from",
|
||||
}), None));
|
||||
};
|
||||
let hist = spec.get("version_history").and_then(|v| v.as_array());
|
||||
let covered = match hist {
|
||||
Some(h) if !h.is_empty() => h
|
||||
.last()
|
||||
.and_then(|e| e.get("covers_material_revision"))
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0),
|
||||
_ => 0,
|
||||
};
|
||||
let current = rec
|
||||
.get("material_revision")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0);
|
||||
let detail = json!({
|
||||
"material_revision": current,
|
||||
"governed_revision": covered,
|
||||
"spec_version": spec.get("version").cloned().unwrap_or(Value::Null),
|
||||
});
|
||||
if current > covered {
|
||||
Ok((json!({
|
||||
"drift": true,
|
||||
"obligation": format!(
|
||||
"SPEC_VERSION_DRIFT: {cid} has {} uncovered material revision(s) \
|
||||
(material rev {current} > governed rev {covered}) — \
|
||||
run revise_spec (PATCH/MINOR/MAJOR) to close",
|
||||
current - covered
|
||||
),
|
||||
"detail": detail,
|
||||
}), None))
|
||||
} else {
|
||||
Ok((json!({"drift": false, "obligation": Value::Null, "detail": detail}), None))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn op_spec_context(a: &Value) -> OpResult {
|
||||
let rec = match a.get("rec") {
|
||||
Some(r) if !r.is_null() => r.clone(),
|
||||
_ => return Err("spec_context: rec is null".into()),
|
||||
};
|
||||
let s = rec
|
||||
.get("spec")
|
||||
.filter(|s| !s.is_null())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let gk = |v: &Value, k: &str, d: Value| v.get(k).cloned().unwrap_or(d);
|
||||
let last_bump = match s.get("version_history").and_then(|v| v.as_array()) {
|
||||
Some(h) if !h.is_empty() => h.last().cloned().unwrap(),
|
||||
_ => json!({}),
|
||||
};
|
||||
Ok((
|
||||
json!({
|
||||
"capability_id": rec.get("capability_id").cloned().unwrap_or(Value::Null),
|
||||
"version": gk(&s, "version", Value::Null),
|
||||
"purpose": rec.get("purpose").cloned().unwrap_or(Value::Null),
|
||||
"requirements": gk(&s, "requirements", json!([])),
|
||||
"expected_behaviours": gk(&s, "expected_behaviours", json!([])),
|
||||
"non_goals": gk(&s, "non_goals", json!([])),
|
||||
"acceptance_criteria": gk(&s, "acceptance_criteria", json!([])),
|
||||
"contract": rec.get("contract").cloned().unwrap_or(Value::Null),
|
||||
"invariants": gk(&rec, "invariants", json!([])),
|
||||
"behaviour_specs": gk(&rec, "behaviour_specs", json!([])),
|
||||
"last_bump": last_bump,
|
||||
}),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ───────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: OpResult) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok((value, doc)) => {
|
||||
let mut m = Map::new();
|
||||
m.insert("ok".into(), json!(true));
|
||||
m.insert("value".into(), value);
|
||||
if let Some(d) = doc {
|
||||
m.insert("doc".into(), d);
|
||||
}
|
||||
Value::Object(m)
|
||||
}
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
macro_rules! ffi_op {
|
||||
($name:ident, $op:path) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| $op(&a)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ffi_op!(cl_new, op_new);
|
||||
ffi_op!(cl_define, op_define);
|
||||
ffi_op!(cl_get, op_get);
|
||||
ffi_op!(cl_material_change, op_material_change);
|
||||
ffi_op!(cl_move, op_move);
|
||||
ffi_op!(cl_supersede, op_supersede);
|
||||
ffi_op!(cl_retire, op_retire);
|
||||
ffi_op!(cl_active, op_active);
|
||||
ffi_op!(cl_by_component, op_by_component);
|
||||
ffi_op!(cl_context_packet, op_context_packet);
|
||||
ffi_op!(cl_save, op_save);
|
||||
ffi_op!(cl_classify_version_bump, op_classify_version_bump);
|
||||
ffi_op!(cl_bump_version, op_bump_version);
|
||||
ffi_op!(cl_attach_spec, op_attach_spec);
|
||||
ffi_op!(cl_revise_spec, op_revise_spec);
|
||||
ffi_op!(cl_spec_drift_status, op_spec_drift_status);
|
||||
ffi_op!(cl_spec_context, op_spec_context);
|
||||
|
||||
// NOTE: no cl_free_string export — sibling crates sharing the Python
|
||||
// process already export identical free symbols; a duplicate breaks the
|
||||
// cdylib link. Callers free through capability_scoring's cs_free_string.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn version_bump_parity() {
|
||||
// batch 132 (oracle-probed): precedence + strict-semver + errors
|
||||
assert_eq!(
|
||||
classify_version_bump(false, false, true, false),
|
||||
Ok("MAJOR")
|
||||
);
|
||||
assert_eq!(
|
||||
classify_version_bump(true, false, false, false),
|
||||
Ok("MAJOR")
|
||||
);
|
||||
assert_eq!(
|
||||
classify_version_bump(false, true, false, false),
|
||||
Ok("MINOR")
|
||||
);
|
||||
assert_eq!(
|
||||
classify_version_bump(false, false, false, true),
|
||||
Ok("PATCH")
|
||||
);
|
||||
assert!(classify_version_bump(false, false, false, false).is_err());
|
||||
assert_eq!(bump_version("1.2.3", "MAJOR"), Ok("2.0.0".to_string()));
|
||||
assert_eq!(bump_version("1.2.3", "MINOR"), Ok("1.3.0".to_string()));
|
||||
assert_eq!(bump_version("1.2.3", "PATCH"), Ok("1.2.4".to_string()));
|
||||
assert_eq!(bump_version("0.0.9", "PATCH"), Ok("0.0.10".to_string()));
|
||||
assert!(bump_version("1.2", "PATCH").is_err());
|
||||
assert!(bump_version("abc", "MAJOR").is_err());
|
||||
assert!(bump_version("1.2.3.4", "PATCH").is_err());
|
||||
assert!(bump_version("", "PATCH").is_err());
|
||||
assert!(bump_version("1.2.3", "BOGUS").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register() {
|
||||
let mut ledger = CapabilityLedger::new();
|
||||
ledger.register("test_cap", "test_provider");
|
||||
assert!(ledger.capabilities.contains_key("test_cap"));
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
@@ -379,7 +379,13 @@ pub fn project(inputs: &Json) -> Result<Json, String> {
|
||||
}
|
||||
}
|
||||
let manifest = obj_get(inputs, "manifest").cloned().unwrap_or(Json::Null);
|
||||
if !seeded {
|
||||
// `existing` provided as an object means carry-forward was attempted by
|
||||
// the caller (the facade routes the unseeded manifest ingest through the
|
||||
// public ingest_umbrellas seam first — isolation tests monkeypatch it).
|
||||
// Only the TRUE cold path (no existing at all) ingests the manifest
|
||||
// here, where the generated-view refusal is load-bearing.
|
||||
let carry_attempted = matches!(existing, Json::Obj(_));
|
||||
if !seeded && !carry_attempted {
|
||||
match &manifest {
|
||||
Json::Null => return Err("manifest unreadable and store unseeded".to_string()),
|
||||
m => {
|
||||
|
||||
Reference in new issue
Block a user