feat(construction_interface): close PARTIAL gaps — dichotomy wave
Audit 25/27 → 27/27 FULL (main + _programme_continuation were the two unmatched fns; the port carries the full envelope surface). Freeze rig scripts/ci_freeze_seams/sitecustomize.py (PYTHONPATH auto-import, used by BOTH generator and --verify): every RUNE_*_CORE gate pinned python for the whole dependency cone, frozen clock on structural_store.now + scaffold.now + capability_ledger._now + programme_continuation.time, deterministic uuid mint (counter resets per rig helper), hermetic per-case stores under CI_FREEZE_HOME (mini-doc paths replay byte-identically), doc-carrying seam bindings for every stateful method. Truth: 85-case frozen corpus + 5-case hash corpus (obligation ids carry Python's process-randomized str hash — captured with PYTHONHASHSEED=0, SHAPE-pinned downstream, the memory_consolidation law). Regeneration byte-identical; freeze_truth --verify OK both corpora. Rust kernels (src/rust/construction_interface): kernel.rs adds the doc-in/doc-out halves of submit_need (need assembly + envelope over the construction_decision make_decision kernel + contract generation twin), verify (scope check + layout gate via the layout_contract kernel), transition (guard canary over frozen command outcomes + legality map + checkpoint/session assembly + normalize_sorted commit twin), close (close_contract + observed-impact + close_quality bucket math + contract_quality projection), _programme_continuation (load/save shape twins + continuation canary), _auto_material_change (via the capability_ledger op_material_change kernel), main (CLI dispatch + exit law), ladder_walk (full ladder in ONE process); projection.rs the pure helpers (to_stage_check, guard_specs, obligations projection with the FNV-1a stand-in id, envelope with extra merge, _match). Store-normalization law discovered: the oracle's commit path sorts lists by canonical JSON (structural_store._sorted) — the kernel applies the same normalization at its commit points or read-back projections (patterns_considered) diverge. Battery: 25 tests — the 85-case corpus replays VERBATIM through kernel::dispatch, the 5 hash cases shape-pinned, gap-fns-are-real-ports probe. clippy --all-targets -D warnings 0. Parity scripts/check_construction_interface_parity.py: FROZEN layer 85/85 + 5 through the ci_replay ctypes FFI of the built cdylib; LIVE layer fresh re-freeze byte-identical. Python 37/37 both pins (floors pinned python + live cores).
This commit is contained in:
1 parent
b433be3e33
commit
192c63b36e
12 files changed
+20943
-7
No files matched your search
@@ -2,14 +2,14 @@
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**FULL completion: 42%** (81/192 runtime modules at FULL)
|
||||
**FULL completion: 43%** (82/192 runtime modules at FULL)
|
||||
|
||||
Function-level detail: 62% (1419/2278 runtime functions matched) — secondary; includes in-flight work
|
||||
Function-level detail: 63% (1440/2278 runtime functions matched) — secondary; includes in-flight work
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 81 | 33154 | 80%+ of functions ported |
|
||||
| PARTIAL | 71 | 29497 | 20–80% — real logic exists, gaps named below |
|
||||
| FULL | 82 | 34347 | 80%+ of functions ported |
|
||||
| PARTIAL | 70 | 28304 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 40 | 11513 | no name-preserving implementation |
|
||||
| Total runtime | 192 | 74164 | |
|
||||
|
||||
@@ -64,7 +64,6 @@ Function-level detail: 62% (1419/2278 runtime functions matched) — secondary;
|
||||
|---|---|---|---|
|
||||
| rune_operator | 2746 | 25/68 | _build_model_router, _sorted_tool_names, _filter_safety_noise, _wire_auto_filer, _reconcile_guarded, get_operator |
|
||||
| memory_store | 2333 | 30/68 | _ctx_budget, now_iso, ensure_memory_type_column, _embed, _temp_json, _entities_to_json |
|
||||
| construction_interface | 1193 | 6/27 | _to_stage_check, _guard_specs_from_obligations, t_layout, _obligations_projection, _obligation_scope_files, main |
|
||||
| provider_registry | 679 | 11/28 | _ctx_budget, _format_tools_openai, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
|
||||
| warm_router | 1518 | 17/33 | _ensure_use_history_table, record_outcome, record_neuron_fire, base_level_activation, _db, _aaak |
|
||||
| cron_engine | 440 | 13/27 | get_cron_engine, run_task, start_engine, stop_engine, to_dict, fire_on_task_complete |
|
||||
@@ -140,6 +139,7 @@ Function-level detail: 62% (1419/2278 runtime functions matched) — secondary;
|
||||
|---|---|---|---|
|
||||
| curator | 1838 | 43/43 | GO |
|
||||
| construction_decision | 1211 | 21/24 | RUST |
|
||||
| construction_interface | 1193 | 27/27 | RUST |
|
||||
| sleep_time | 1056 | 26/26 | GO |
|
||||
| spine | 975 | 25/25 | RUST |
|
||||
| structural_store | 921 | 34/41 | RUST |
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: construction_interface Rust kernels vs the frozen oracle
|
||||
truth (dichotomy wave, contract factory-gap-ci). Layers:
|
||||
|
||||
1. FROZEN — the frozen truth corpora (frozen_ci_truth.json 85 cases +
|
||||
frozen_ci_truth_hash.json 5 shape-pinned cases, generated by
|
||||
scripts/gen_construction_interface_truth.py against the LIVE Python
|
||||
oracle through the scripts/ci_freeze_seams rig) replayed through
|
||||
the ci_replay ctypes FFI entry of the built cdylib, case by case.
|
||||
2. LIVE — a fresh re-freeze of the oracle through the same rig in a
|
||||
subprocess; the regenerated corpora must be byte-identical to the
|
||||
committed files (the oracle is still the parity authority while the
|
||||
strangler runs).
|
||||
|
||||
Exit 0 only at 100% parity (85 verbatim + 5 shape-pinned + byte-identical
|
||||
re-freeze).
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
CRATE = REPO / "src" / "rust" / "construction_interface"
|
||||
LIB = CRATE / "target" / "release" / "libconstruction_interface.so"
|
||||
FROZEN = CRATE / "frozen_ci_truth.json"
|
||||
FROZEN_HASH = CRATE / "frozen_ci_truth_hash.json"
|
||||
|
||||
HASH_ID_PREFIXES = (
|
||||
"contract:", "regression:", "verification:",
|
||||
"composition:", "acquisition:",
|
||||
)
|
||||
|
||||
|
||||
def normalize_hash_ids(obj):
|
||||
if isinstance(obj, str):
|
||||
kind, _, digits = obj.partition(":")
|
||||
if (f"{kind}:" in HASH_ID_PREFIXES and digits
|
||||
and len(digits) <= 8 and digits.isdigit()):
|
||||
return f"{kind}:#"
|
||||
return obj
|
||||
if isinstance(obj, list):
|
||||
return [normalize_hash_ids(v) for v in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: normalize_hash_ids(v) for k, v in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
def replay(lib, case):
|
||||
payload = json.dumps(case, separators=(",", ":")).encode()
|
||||
ptr = lib.ci_replay(payload, len(payload))
|
||||
if not ptr:
|
||||
raise RuntimeError("ci_replay returned NULL")
|
||||
text = ctypes.c_char_p(ptr).value.decode()
|
||||
lib.ci_free_string(ptr)
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
def frozen_layer():
|
||||
lib = ctypes.CDLL(str(LIB))
|
||||
lib.ci_replay.argtypes = [ctypes.c_char_p, ctypes.c_size_t]
|
||||
lib.ci_replay.restype = ctypes.c_void_p
|
||||
lib.ci_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.ci_free_string.restype = None
|
||||
ok = 0
|
||||
cases = json.loads(FROZEN.read_text())
|
||||
for case in cases:
|
||||
got = replay(lib, case)
|
||||
assert got == case["expected"], (
|
||||
f"FROZEN divergence: {case['fn']} "
|
||||
f"{case['args'][0] if case['args'] else '?'}")
|
||||
ok += 1
|
||||
hash_ok = 0
|
||||
for case in json.loads(FROZEN_HASH.read_text()):
|
||||
got = normalize_hash_ids(replay(lib, case))
|
||||
want = normalize_hash_ids(case["expected"])
|
||||
assert got == want, (
|
||||
f"HASH divergence: {case['fn']} "
|
||||
f"{case['args'][0] if case['args'] else '?'}")
|
||||
hash_ok += 1
|
||||
print(f"FROZEN: PARITY OK {ok}/{len(cases)} verbatim "
|
||||
f"+ {hash_ok} hash-shape cases")
|
||||
return ok, hash_ok
|
||||
|
||||
|
||||
def main() -> int:
|
||||
committed_main = FROZEN.read_bytes()
|
||||
committed_hash = FROZEN_HASH.read_bytes()
|
||||
n_main, n_hash = frozen_layer()
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="ci_gap_parity_") as _scratch:
|
||||
# the re-freeze must run at the CANONICAL rig home — the mini-doc
|
||||
# paths are embedded in the frozen truth, so a different
|
||||
# CI_FREEZE_HOME changes the bytes without changing behavior
|
||||
home = "/tmp/ci_gap_freeze_home"
|
||||
subprocess.run(["rm", "-rf", home], check=True)
|
||||
env = dict(
|
||||
os.environ,
|
||||
CI_FREEZE_HOME=home,
|
||||
PYTHONPATH="scripts/ci_freeze_seams:src",
|
||||
CS_RUST_LIB="/mnt/workdrive/rune/src/rust/capability_scoring/"
|
||||
"target/release/libcapability_scoring.so",
|
||||
)
|
||||
r = subprocess.run(
|
||||
[sys.executable, "scripts/gen_construction_interface_truth.py"],
|
||||
cwd=str(REPO), env=env, capture_output=True, text=True, timeout=600)
|
||||
if r.returncode != 0:
|
||||
print(r.stdout[-2000:])
|
||||
print(r.stderr[-2000:])
|
||||
print("LIVE: re-freeze FAILED")
|
||||
return 1
|
||||
same_main = FROZEN.read_bytes() == committed_main
|
||||
same_hash = FROZEN_HASH.read_bytes() == committed_hash
|
||||
if not (same_main and same_hash):
|
||||
print(f"LIVE: re-freeze NOT byte-identical "
|
||||
f"(main={same_main} hash={same_hash})")
|
||||
return 1
|
||||
print(f"LIVE: fresh re-freeze byte-identical "
|
||||
f"({n_main}+{n_hash} cases, oracle authority held)")
|
||||
print("PARITY OK construction_interface")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,562 @@
|
||||
# Freeze rig for the construction_interface dichotomy gap-closing (dichotomy
|
||||
# wave #2479 gap port). Auto-imported on interpreter start
|
||||
# (PYTHONPATH=scripts/ci_freeze_seams:src) by BOTH the truth generator
|
||||
# (scripts/gen_construction_interface_truth.py) and the shared harness's
|
||||
# --verify mode, so frozen truth is byte-reproducible in any process:
|
||||
#
|
||||
# * EVERY RUNE_*_CORE gate is pinned to "python" — the oracle runs its
|
||||
# pure-Python floors end to end (construction_interface, scaffold,
|
||||
# structural_store, construction_decision, construction_mode, capability
|
||||
# ledger, programme continuation, layout contract, ...). The shared
|
||||
# harness's _pin_oracle_cores cannot reach the transitive gates; this
|
||||
# blanket pin is the poisoned-truth law for the whole dependency cone.
|
||||
# * The wall clocks are FROZEN: rune.structural_store.now and
|
||||
# rune.scaffold.now (bound import) report CI_FROZEN_NOW. The oracle
|
||||
# stamps need ids, observed_at, checkpoints and closure records through
|
||||
# these two seams; the frozen truth carries the constant instant.
|
||||
# * uuid.uuid4 is a deterministic counter (submit_need mints contract ids
|
||||
# from uuid4().hex[:6]); the counter RESETS per rig helper call so a
|
||||
# case's ids depend only on how many mints happen inside it.
|
||||
# * CI_FREEZE_HOME (default /tmp/ci_gap_freeze_home) roots every per-case
|
||||
# hermetic store: cases/<case>/proj/.rune/scaffold — deterministic
|
||||
# per-case dirs, because envelopes that carry paths (mini_doc) must
|
||||
# replay byte-identically.
|
||||
# * DOC-CARRYING SEAMS: the Rust kernel is a pure doc-in/doc-out membrane
|
||||
# (the store stays with the facade), so every stateful seam below takes
|
||||
# the FULL input doc as an argument — the frozen args replay in the Rust
|
||||
# battery without any store. The input docs are built by the deterministic
|
||||
# builder helpers (build_doc/submit_doc/walk_doc) that the GENERATOR
|
||||
# calls; the seam injects the doc into a fresh hermetic store, runs the
|
||||
# ORACLE method, and returns the envelope (plus oracle-derived doc
|
||||
# outputs where the kernel's contract includes them).
|
||||
# * write_mini_doc is redirected into the case dir (the oracle's default
|
||||
# routes through the SOURCE TREE — a repo-dependent absolute path).
|
||||
#
|
||||
# KNOWN-NONDETERMINISTIC SEAM (memory_consolidation precedent): the
|
||||
# obligation ids in _obligations_projection carry Python's
|
||||
# process-randomized str hash. Cases whose output carries those ids live
|
||||
# in the SEPARATE frozen_ci_truth_hash.json battery (captured + verified
|
||||
# with PYTHONHASHSEED=0) and are SHAPE-pinned in the Rust battery and the
|
||||
# parity check — never value-frozen in the hash-free battery.
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid as _uuid
|
||||
from pathlib import Path
|
||||
|
||||
for _k in list(os.environ):
|
||||
if _k.startswith("RUNE_") and _k.endswith("_CORE"):
|
||||
os.environ[_k] = "python"
|
||||
|
||||
CI_FREEZE_HOME = os.environ.get("CI_FREEZE_HOME") or "/tmp/ci_gap_freeze_home"
|
||||
FROZEN_NOW = "2026-10-13T09:41:23Z"
|
||||
|
||||
import rune.structural_store as ss # noqa: E402
|
||||
import rune.scaffold as _sc # noqa: E402
|
||||
|
||||
ss.now = lambda: FROZEN_NOW
|
||||
_sc.now = lambda: FROZEN_NOW
|
||||
|
||||
# programme_continuation stamps saved_at/at through time.strftime directly
|
||||
# (not ss.now) — pin the time module floor for it.
|
||||
import rune.capability_ledger as _cl_mod # noqa: E402
|
||||
import rune.programme_continuation as _pc_mod # noqa: E402
|
||||
import time as _time_mod # noqa: E402
|
||||
|
||||
_cl_mod._now = lambda: FROZEN_NOW
|
||||
_pc_mod.time = type("_T", (), {
|
||||
"strftime": staticmethod(lambda f, _t: FROZEN_NOW),
|
||||
"gmtime": staticmethod(lambda *_a: None),
|
||||
"time": staticmethod(lambda: 0.0),
|
||||
"monotonic": staticmethod(lambda: 0.0),
|
||||
})()
|
||||
|
||||
_MINT = itertools.count(1)
|
||||
|
||||
|
||||
class _DetUUID:
|
||||
def __init__(self, n: int):
|
||||
self.hex = f"{n:06x}" + "0" * 26
|
||||
|
||||
|
||||
def _reset_mint():
|
||||
global _MINT
|
||||
_MINT = itertools.count(1)
|
||||
|
||||
|
||||
_uuid.uuid4 = lambda: _DetUUID(next(_MINT)) # type: ignore[assignment]
|
||||
|
||||
import rune.construction_interface as ci # noqa: E402
|
||||
|
||||
# write_mini_doc redirect (source-tree default → per-case dir)
|
||||
import rune.construction_projection as _cp_mod # noqa: E402
|
||||
|
||||
_orig_mini_doc = _cp_mod.write_mini_doc
|
||||
|
||||
|
||||
def _hermetic_mini_doc(scaffold, contract_id, out_dir=None):
|
||||
case = os.environ.get("CI_ACTIVE_CASE", "mini-doc")
|
||||
out = Path(CI_FREEZE_HOME) / "cases" / case / "docs"
|
||||
return _orig_mini_doc(scaffold, contract_id, out_dir=out)
|
||||
|
||||
|
||||
_cp_mod.write_mini_doc = _hermetic_mini_doc
|
||||
|
||||
# Originals captured BEFORE the seam bindings below shadow the module
|
||||
# globals the oracle itself calls internally (envelope assembly etc.) —
|
||||
# the wrappers must call the captured originals, never the shadowed name.
|
||||
_ORIG = {name: getattr(ci, name) for name in (
|
||||
"_envelope", "_to_stage_check", "_guard_specs_from_obligations",
|
||||
"t_layout", "_obligations_projection", "_obligation_scope_files",
|
||||
"main")}
|
||||
|
||||
FIXTURE_ENTITIES = [
|
||||
{"id": "capability:exporter", "kind": "capability", "identities": {},
|
||||
"maturity": "WIRED", "evidence_refs": ["f"], "relationships": [],
|
||||
"observations": []},
|
||||
{"id": "contract:csv_export", "kind": "contract", "identities": {},
|
||||
"maturity": "SEED", "evidence_refs": ["f"], "relationships": [],
|
||||
"observations": []},
|
||||
]
|
||||
FIXTURE_RELATIONSHIPS = [
|
||||
{"type": "implements", "from": "capability:exporter",
|
||||
"to": "contract:csv_export", "evidence_refs": ["f"], "observations": []},
|
||||
]
|
||||
|
||||
|
||||
def _case_dir(case: str) -> Path:
|
||||
os.environ["CI_ACTIVE_CASE"] = case
|
||||
d = Path(CI_FREEZE_HOME) / "cases" / case
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
(d / "proj" / ".rune").mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _iface(case: str, project_id: str = "gap-ci-proj"):
|
||||
_reset_mint()
|
||||
d = _case_dir(case)
|
||||
store = str(d / "proj" / ".rune" / "scaffold")
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="factory-gap-ci", project_store=store, project_id=project_id)
|
||||
iface.scaffold.init(purpose="fixture")
|
||||
doc = iface.scaffold.load()
|
||||
for e in FIXTURE_ENTITIES:
|
||||
doc["entities"].append(json.loads(json.dumps(e)))
|
||||
for r in FIXTURE_RELATIONSHIPS:
|
||||
doc["relationships"].append(json.loads(json.dumps(r)))
|
||||
iface.scaffold.save(doc)
|
||||
return iface
|
||||
|
||||
|
||||
def _iface_with_doc(case: str, doc: dict, project_id: str = "gap-ci-proj"):
|
||||
"""Fresh hermetic store with `doc` injected — the seam entry point."""
|
||||
_reset_mint()
|
||||
d = _case_dir(case)
|
||||
store = str(d / "proj" / ".rune" / "scaffold")
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="factory-gap-ci", project_store=store, project_id=project_id)
|
||||
iface.scaffold.init(purpose="fixture")
|
||||
iface.scaffold.save(json.loads(json.dumps(doc)))
|
||||
return iface
|
||||
|
||||
|
||||
def build_doc() -> dict:
|
||||
"""The fixture authority document (pure dict, no store IO)."""
|
||||
doc = ss.new_doc()
|
||||
for e in FIXTURE_ENTITIES:
|
||||
doc["entities"].append(json.loads(json.dumps(e)))
|
||||
for r in FIXTURE_RELATIONSHIPS:
|
||||
doc["relationships"].append(json.loads(json.dumps(r)))
|
||||
return doc
|
||||
|
||||
|
||||
def submit_doc(case: str, doc: dict, objective: str = "persistent project export",
|
||||
**kw) -> dict:
|
||||
"""Builder helper (NOT a frozen fn): run oracle submit_need over a
|
||||
hermetic store seeded with `doc`; return the updated doc."""
|
||||
payload = dict(
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
payload.update(kw)
|
||||
iface = _iface_with_doc(case, doc)
|
||||
r = iface.submit_need(objective, **payload)
|
||||
assert r["success"] or not payload.get("generate", True), r
|
||||
return iface.scaffold.load()
|
||||
|
||||
|
||||
def walk_doc(case: str, doc: dict, objective: str = "persistent project export",
|
||||
phases=("approved", "building", "verifying"), **kw) -> dict:
|
||||
"""Builder helper: submit + walk the legal ladder in ONE process."""
|
||||
iface = _iface_with_doc(case, doc)
|
||||
payload = dict(
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
payload.update(kw)
|
||||
r = iface.submit_need(objective, **payload)
|
||||
assert r["success"], r
|
||||
cid = r["ids"]["contract_id"]
|
||||
for ph in phases:
|
||||
t = iface.transition(cid, ph)
|
||||
assert t["success"], t
|
||||
return iface.scaffold.load()
|
||||
|
||||
|
||||
def contract_of(doc: dict, contract_id: str):
|
||||
for e in doc.get("entities", []):
|
||||
if e.get("kind") == "construction_contract" and \
|
||||
e.get("id") == f"contract:{contract_id}":
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def build_ledger_caps() -> dict:
|
||||
"""A valid capability-ledger doc via the ledger's own define API
|
||||
(builder helper; frozen clock keeps it deterministic)."""
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
|
||||
ledger = CapabilityLedger(Path(CI_FREEZE_HOME) / "cases" / "b-ledger")
|
||||
ledger.define(capability_id="capability:exporter", name="exporter",
|
||||
purpose="export fixtures", owning_project="gap-ci-proj",
|
||||
component="exporter", source_locations=["src/export.py"])
|
||||
return json.loads(json.dumps(ledger.doc))
|
||||
|
||||
|
||||
def _discover_doc(contract_id: str, observed_at: str, project: str) -> dict:
|
||||
"""A minimal store doc carrying ONE construction contract (builder
|
||||
helper for the discover_active_contract seam)."""
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id=f"contract:{contract_id}",
|
||||
kind="construction_contract",
|
||||
identities={"project": project, "scope": [],
|
||||
"affected_entities": []},
|
||||
evidence_refs=["f"])
|
||||
doc["entities"][-1]["observed_at"] = observed_at
|
||||
return doc
|
||||
|
||||
|
||||
def _submitted_cid(doc: dict) -> str:
|
||||
for e in doc.get("entities", []):
|
||||
if e.get("kind") == "construction_contract":
|
||||
return e["id"].split(":", 1)[1]
|
||||
raise AssertionError("no contract in doc")
|
||||
|
||||
|
||||
# ── pure module-level seams ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _envelope(operation, *, success, ids=None, state=None, evidence=None,
|
||||
errors=None, warnings=None, extra=None):
|
||||
return _ORIG["_envelope"](operation, success=success, ids=ids, state=state,
|
||||
evidence=evidence, errors=errors,
|
||||
warnings=warnings, **(extra or {}))
|
||||
|
||||
|
||||
def _to_stage_check(spec):
|
||||
from rune import stage_guards as _sg
|
||||
|
||||
c = _sg.StageCheck(
|
||||
name=str(spec.get("name", "guard")),
|
||||
kind=str(spec.get("kind", "assert")),
|
||||
argv=spec.get("argv"),
|
||||
output_pattern=str(spec.get("output_pattern", "")),
|
||||
path=str(spec.get("path", "")),
|
||||
check=spec.get("check"),
|
||||
description=str(spec.get("description", "")),
|
||||
)
|
||||
return {"name": c.name, "kind": c.kind, "argv": c.argv,
|
||||
"output_pattern": c.output_pattern, "path": c.path,
|
||||
"check": c.check.__name__ if callable(c.check) else None,
|
||||
"description": c.description}
|
||||
|
||||
|
||||
def _guard_specs_from_obligations(c):
|
||||
return _ORIG["_guard_specs_from_obligations"](c)
|
||||
|
||||
|
||||
def t_layout(ext):
|
||||
return _ORIG["t_layout"](ext)
|
||||
|
||||
|
||||
def _obligations_projection(c):
|
||||
return _ORIG["_obligations_projection"](c)
|
||||
|
||||
|
||||
def _obligation_scope_files(obligations):
|
||||
return _ORIG["_obligation_scope_files"](obligations)
|
||||
|
||||
|
||||
def main(case, argv):
|
||||
"""The JSON CLI transport over the hermetic rune-internal store."""
|
||||
_reset_mint()
|
||||
d = _case_dir(case)
|
||||
(d / "store" / "scaffold").mkdir(parents=True, exist_ok=True)
|
||||
prev = os.getcwd()
|
||||
os.chdir(d)
|
||||
try:
|
||||
inner = ci.ConstructionInterface(builder="factory-gap-ci")
|
||||
inner.scaffold.init(purpose="fixture")
|
||||
return _ORIG["main"](list(argv))
|
||||
finally:
|
||||
os.chdir(prev)
|
||||
|
||||
|
||||
# ── authority access ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _authorities(scaffold_doc, spine_doc=None, mode="external_project"):
|
||||
"""The oracle's authority assembly given the loaded docs (doc-carrying:
|
||||
the loads stay with the caller)."""
|
||||
auth = {"project_scaffold": scaffold_doc}
|
||||
if mode == "rune_internal" and spine_doc is not None:
|
||||
auth["rune_spine"] = spine_doc
|
||||
return auth
|
||||
|
||||
|
||||
def _record_session(doc, operation, detail, builder="factory-gap-ci",
|
||||
mode="external_project", now=None):
|
||||
iface = _iface("rec-session")
|
||||
d = json.loads(json.dumps(doc))
|
||||
iface._record_session(d, operation, detail)
|
||||
return d
|
||||
|
||||
|
||||
# ── classify / context / capabilities ───────────────────────────────────────
|
||||
|
||||
|
||||
def classify(decision, builder="factory-gap-ci"):
|
||||
"""Envelope assembly over the canonical classifier's result (the
|
||||
classifier itself is construction_mode's port surface)."""
|
||||
iface = _iface("classify")
|
||||
d = dict(decision)
|
||||
env = _ORIG["_envelope"]("classify", success=d.get("mode") != "UNKNOWN",
|
||||
state={**d, "builder": builder})
|
||||
return env
|
||||
|
||||
|
||||
def construction_context(case, doc, contract_id=None,
|
||||
builder="factory-gap-ci", project="gap-ci-proj"):
|
||||
iface = _iface_with_doc(case, doc, project_id=project)
|
||||
return iface.construction_context(contract_id)
|
||||
|
||||
|
||||
def capabilities(mode="external_project", project="gap-ci-proj"):
|
||||
iface = _iface("caps")
|
||||
iface.mode = mode
|
||||
iface.scaffold.project_id = project
|
||||
return iface.capabilities()
|
||||
|
||||
|
||||
# ── submit_need ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def submit_need(case, doc, objective, origin="builder", constraints=None,
|
||||
affected_surface=None, evidence_refs=None,
|
||||
approval_context="", task_ref="", generate=True,
|
||||
contract_id=None):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
kw = dict(origin=origin, generate=generate)
|
||||
if constraints is not None:
|
||||
kw["constraints"] = constraints
|
||||
if affected_surface is not None:
|
||||
kw["affected_surface"] = affected_surface
|
||||
if evidence_refs is not None:
|
||||
kw["evidence_refs"] = evidence_refs
|
||||
kw["approval_context"] = approval_context
|
||||
kw["task_ref"] = task_ref
|
||||
if contract_id is not None:
|
||||
kw["contract_id"] = contract_id
|
||||
return iface.submit_need(objective, **kw)
|
||||
|
||||
|
||||
# ── read projections + evidence + verification ──────────────────────────────
|
||||
|
||||
|
||||
def inspect_decision(case, doc, contract_id):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.inspect_decision(contract_id)
|
||||
|
||||
|
||||
def get_scope(case, doc, contract_id):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.get_scope(contract_id)
|
||||
|
||||
|
||||
def get_obligations(case, doc, contract_id):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.get_obligations(contract_id)
|
||||
|
||||
|
||||
def report_observation(case, doc, contract_id, observations):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.report_observation(contract_id, observations)
|
||||
|
||||
|
||||
def verify(case, doc, contract_id, changed_files=None,
|
||||
layout_observations=None):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.verify(contract_id, changed_files=changed_files,
|
||||
layout_observations=layout_observations)
|
||||
|
||||
|
||||
def transition(case, doc, contract_id, phase, evidence_ref="",
|
||||
guard_checks=None):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.transition(contract_id, phase, evidence_ref=evidence_ref,
|
||||
guard_checks=guard_checks)
|
||||
|
||||
|
||||
def close(case, doc, contract_id, observed_evidence, scope_files=None):
|
||||
"""`doc` must already carry the contract at verifying (walk_doc)."""
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.close(contract_id, observed_evidence=observed_evidence,
|
||||
scope_files=scope_files)
|
||||
|
||||
|
||||
def _programme_continuation(case, doc, contract_id, programmes=None):
|
||||
"""programmes: list of programme DOCS (generator-built via the pc API);
|
||||
the seam persists them into the case's programme area, runs the oracle,
|
||||
and returns the continuation plus the persisted-after docs."""
|
||||
from rune import programme_continuation as pc
|
||||
|
||||
iface = _iface_with_doc(case, doc)
|
||||
base = Path(iface.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
|
||||
if programmes is not None:
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
for pd in programmes:
|
||||
prog = pc.Programme(
|
||||
programme_id=pd["programme_id"], objective=pd["objective"],
|
||||
stages=[pc.ProgrammeStage(**s) for s in pd["stages"]])
|
||||
prog.save(base)
|
||||
out = iface._programme_continuation(contract_id)
|
||||
after = None
|
||||
if base.is_dir():
|
||||
after = []
|
||||
for f in sorted(base.glob("*.json")):
|
||||
after.append(json.loads(f.read_text()))
|
||||
return {"continuation": out, "programmes_after": after}
|
||||
|
||||
|
||||
def _auto_material_change(case, doc, scope_files, capabilities=None):
|
||||
"""capabilities: the ledger DOC (kernel input); the seam persists it as
|
||||
the project rune_model ledger, runs the oracle, and returns the oracle
|
||||
result plus the ledger doc after the oracle's material_change/save."""
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
|
||||
iface = _iface_with_doc(case, doc)
|
||||
if capabilities is not None:
|
||||
ledger_dir = Path(iface.scaffold.store_dir).parent / "rune_model"
|
||||
ledger_dir.mkdir(parents=True, exist_ok=True)
|
||||
(ledger_dir / "capability_ledger.json").write_text(
|
||||
json.dumps(capabilities))
|
||||
out = iface._auto_material_change(
|
||||
list(scope_files), note="contract closed (material change)")
|
||||
after = None
|
||||
led = Path(iface.scaffold.store_dir).parent / "rune_model" / \
|
||||
"capability_ledger.json"
|
||||
if led.is_file():
|
||||
after = json.loads(led.read_text())
|
||||
return {"result": out, "ledger_after": after}
|
||||
|
||||
|
||||
def discover_active_contract(case, candidates, root_name="store"):
|
||||
"""candidates: [{project: str|null, doc: {...}}] — the seam persists
|
||||
each doc at projects/<p>/current.json (or the root current.json) inside
|
||||
the case dir and runs the oracle over that store root."""
|
||||
d = _case_dir(case)
|
||||
for cand in candidates:
|
||||
proj, doc = cand["project"], cand["doc"]
|
||||
if proj is None:
|
||||
(d / "current.json").write_text(json.dumps(doc))
|
||||
else:
|
||||
p = d / "projects" / proj
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
(p / "current.json").write_text(json.dumps(doc))
|
||||
return ci.ConstructionInterface.discover_active_contract(str(d))
|
||||
|
||||
|
||||
def resume(case, doc, contract_id):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
return iface.resume(contract_id)
|
||||
|
||||
|
||||
def amend_scope(case, doc, contract_id, add_paths, reason, authority="user"):
|
||||
iface = _iface_with_doc(case, doc)
|
||||
env = iface.amend_scope(contract_id, add_paths, reason=reason,
|
||||
authority=authority)
|
||||
return {"envelope": env,
|
||||
"contract_after": contract_of(iface.scaffold.load(), contract_id)}
|
||||
|
||||
|
||||
def ladder_walk(case, objective="persistent project export"):
|
||||
"""The FULL ladder in ONE process (the shared-store clobber law):
|
||||
submit_need → approved → building → verifying → close, with the read
|
||||
projections riding the walk."""
|
||||
iface = _iface(case)
|
||||
out = []
|
||||
r = iface.submit_need(
|
||||
objective,
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[], evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority")
|
||||
out.append({"step": "submit_need", "envelope": r})
|
||||
if not r["success"]:
|
||||
return out
|
||||
cid = r["ids"]["contract_id"]
|
||||
out.append({"step": "inspect_decision", "envelope": iface.inspect_decision(cid)})
|
||||
out.append({"step": "get_scope", "envelope": iface.get_scope(cid)})
|
||||
out.append({"step": "verify", "envelope": iface.verify(cid)})
|
||||
for ph in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, ph)
|
||||
out.append({"step": f"transition:{ph}", "envelope": t})
|
||||
if not t["success"]:
|
||||
return out
|
||||
c = iface.close(cid, observed_evidence=[
|
||||
{"kind": "port_evidence", "detail": "gap closed with proof"}])
|
||||
out.append({"step": "close", "envelope": c})
|
||||
return out
|
||||
|
||||
|
||||
# Bind the seams onto the oracle module so the shared harness's
|
||||
# getattr(module, fn) resolves every frozen fn name.
|
||||
ci.ladder_walk = ladder_walk
|
||||
ci.submit_need = submit_need
|
||||
ci.classify = classify
|
||||
ci.construction_context = construction_context
|
||||
ci.capabilities = capabilities
|
||||
ci.inspect_decision = inspect_decision
|
||||
ci.get_scope = get_scope
|
||||
ci.get_obligations = get_obligations
|
||||
ci.report_observation = report_observation
|
||||
ci.verify = verify
|
||||
ci.transition = transition
|
||||
ci.close = close
|
||||
ci._programme_continuation = _programme_continuation
|
||||
ci._auto_material_change = _auto_material_change
|
||||
ci.discover_active_contract = discover_active_contract
|
||||
ci.resume = resume
|
||||
ci.amend_scope = amend_scope
|
||||
ci._authorities = _authorities
|
||||
ci._record_session = _record_session
|
||||
ci.t_layout = t_layout
|
||||
ci._obligations_projection = _obligations_projection
|
||||
ci._obligation_scope_files = _obligation_scope_files
|
||||
ci._guard_specs_from_obligations = _guard_specs_from_obligations
|
||||
ci.ci_to_stage_check = _to_stage_check
|
||||
ci._envelope = _envelope
|
||||
ci.main = main
|
||||
# NOTE: ci._to_stage_check is deliberately NOT rebound — the oracle's
|
||||
# transition() calls the module-global _to_stage_check internally and needs
|
||||
# real StageCheck objects for the readiness canary. The rig's JSON-safe
|
||||
# _to_stage_check is exposed as the ci_to_stage_check seam instead (same
|
||||
# logic, JSON-safe projection).
|
||||
# ci._obligations_projection IS rebound to the identity wrapper above
|
||||
# (pure dict in → dict out through the captured original) — internal
|
||||
# oracle calls with live contract dicts pass through unharmed.
|
||||
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the dichotomy-wave gap truth for src/rune/construction_interface
|
||||
through the SHARED harness (scripts/freeze_truth.py — one law).
|
||||
|
||||
The oracle is a stateful facade (ConstructionInterface over a Scaffold
|
||||
store) with clock/uuid nondeterminism, so this generator runs under the
|
||||
freeze rig (scripts/ci_freeze_seams/sitecustomize.py):
|
||||
pinned RUNE_*_CORE=python floors, frozen clock, deterministic uuid mint,
|
||||
hermetic per-case stores (deterministic per-case dirs — envelopes that
|
||||
carry paths, like mini_doc, replay byte-identically), and DOC-CARRYING
|
||||
seam bindings the shared harness can getattr: the Rust kernel is a pure
|
||||
doc-in/doc-out membrane, so every stateful case carries the full input
|
||||
doc in its args and replays in the battery with no store.
|
||||
|
||||
Run (from the repo root):
|
||||
rm -rf /tmp/ci_gap_freeze_home
|
||||
CI_FREEZE_HOME=/tmp/ci_gap_freeze_home \
|
||||
PYTHONPATH=scripts/ci_freeze_seams:src \
|
||||
python3 scripts/gen_construction_interface_truth.py
|
||||
|
||||
Verify (byte-identical, same rig):
|
||||
CI_FREEZE_HOME=/tmp/ci_gap_freeze_home \
|
||||
PYTHONPATH=scripts/ci_freeze_seams:src \
|
||||
python3 scripts/freeze_truth.py --verify \
|
||||
src/rust/construction_interface/frozen_ci_truth.json \
|
||||
--module rune.construction_interface
|
||||
|
||||
Every public function and branch is exercised. The obligation-id seam
|
||||
(_obligations_projection carries process-randomized str hash) lives in
|
||||
the SEPARATE frozen_ci_truth_hash.json battery (captured + verified with
|
||||
PYTHONHASHSEED=0) and is SHAPE-pinned downstream — never value-frozen
|
||||
here.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
|
||||
from freeze_truth import freeze, write # noqa: E402
|
||||
|
||||
import sitecustomize as rig # noqa: E402
|
||||
|
||||
# ── deterministic input docs (builder helpers; NOT frozen fns) ──
|
||||
DOC0 = rig.build_doc() # fixture authority, no contract
|
||||
DOC_SUBMITTED = rig.submit_doc("b-submitted", rig.build_doc())
|
||||
DOC_WALKED = rig.walk_doc("b-walked", rig.build_doc())
|
||||
DOC_WITH_LEDGER_CAPS = rig.build_ledger_caps()
|
||||
PROGRAMMES_BOUND = [{
|
||||
"programme_id": "prog-gap-ci", "objective": "close the gap",
|
||||
"stages": [
|
||||
{"stage_id": rig._submitted_cid(DOC_SUBMITTED),
|
||||
"objective": "the gap stage"},
|
||||
{"stage_id": "next-stage", "objective": "the next stage"},
|
||||
],
|
||||
}]
|
||||
|
||||
GUARD_FAIL = [{"name": "always-fail", "kind": "command", "argv": ["false"]}]
|
||||
GUARD_PASS = [{"name": "always-pass", "kind": "command", "argv": ["true"]}]
|
||||
GUARD_ASSERT = [{"name": "truthy", "kind": "assert",
|
||||
"description": "always true"}]
|
||||
|
||||
cases = [
|
||||
# ── _envelope: success + failure shapes, extra keys ──
|
||||
("_envelope", ("op",), {"success": True}),
|
||||
("_envelope", ("op",), {"success": False,
|
||||
"errors": [{"error": "X"}],
|
||||
"warnings": [{"warning": "W"}],
|
||||
"ids": {"a": "b"}, "state": {"k": 1},
|
||||
"extra": {"extra_key": "extra"}}),
|
||||
# ── _to_stage_check: full spec, defaults ──
|
||||
("ci_to_stage_check", ({"name": "g", "kind": "command",
|
||||
"argv": ["true"], "output_pattern": "ok",
|
||||
"path": "/x", "description": "d"},), {}),
|
||||
("ci_to_stage_check", ({},), {}),
|
||||
# ── _guard_specs_from_obligations: guard rows, junk, bad JSON ──
|
||||
("_guard_specs_from_obligations",
|
||||
({"identities": {"verification_obligations": [
|
||||
'guard:{"name": "g1", "kind": "command", "argv": ["true"]}',
|
||||
"free-text obligation",
|
||||
"guard:not-json{{{",
|
||||
'["not", "a", "dict"]',
|
||||
'guard:{"name": "g2"}',
|
||||
]}},), {}),
|
||||
("_guard_specs_from_obligations", ({},), {}),
|
||||
# ── t_layout: present + absent ──
|
||||
("t_layout", ({"decision_trace": {"layout_obligations": [
|
||||
{"component": "c", "obligation": "o"}]}},), {}),
|
||||
("t_layout", ({},), {}),
|
||||
# ── _obligations_projection: synthetic hash-free fixtures (empty,
|
||||
# pattern-only, layout-only — the contract/structural id branches
|
||||
# carry process-randomized hash → hash battery) ──
|
||||
("_obligations_projection", ({},), {}),
|
||||
("_obligations_projection",
|
||||
({"ext": {"decision_trace": {"obligations": {"pattern": [
|
||||
{"id": "pat-1", "pattern": "seed_loop", "revision": 3,
|
||||
"obligation": "keep the seed loop closed", "verdict": "SATISFIED",
|
||||
"reason": "evidence row 1", "pattern_applicability": "always"}]}}}},),
|
||||
{}),
|
||||
("_obligations_projection",
|
||||
({"ext": {"decision_trace": {"layout_obligations": [
|
||||
{"component": "ui:header", "obligation": "stay in region top",
|
||||
"verification": "adapter observation"}]}}},), {}),
|
||||
# ── _obligation_scope_files: passthrough stub ──
|
||||
("_obligation_scope_files", ([{"id": "x"}],), {}),
|
||||
("_obligation_scope_files", ([],), {}),
|
||||
# ── _authorities: external mode, rune-internal w/ + w/o spine ──
|
||||
("_authorities", (rig.build_doc(),), {}),
|
||||
("_authorities", (rig.build_doc(), {"schema_version": 1,
|
||||
"entities": []}),
|
||||
{"mode": "rune_internal"}),
|
||||
("_authorities", (rig.build_doc(), None), {"mode": "rune_internal"}),
|
||||
# ── _record_session: append + long-detail truncation ──
|
||||
("_record_session", (rig.build_doc(), "submit_need", "short detail"), {}),
|
||||
("_record_session", (rig.build_doc(), "close", "d" * 500), {}),
|
||||
# ── classify: envelope assembly over classifier results ──
|
||||
("classify", ({"mode": "UNKNOWN", "evidence": ["probe: which surface?"],
|
||||
"probe_questions": ["q1"]},), {}),
|
||||
("classify", ({"mode": "EXISTING_PROJECT_CHANGE", "evidence": ["e1"],
|
||||
"probe_questions": []},), {}),
|
||||
("classify", ({"mode": "LOCAL_CHANGE"},),
|
||||
{"builder": "other-builder"}),
|
||||
# ── construction_context: none active, explicit unknown id (the
|
||||
# ACTIVE projection carries obligation ids → hash battery) ──
|
||||
("construction_context", ("cc-none", rig.build_doc()), {}),
|
||||
("construction_context", ("cc-unknown", DOC_SUBMITTED, "NOPE"), {}),
|
||||
# ── capabilities: the full discovery envelope, both modes ──
|
||||
("capabilities", (), {"mode": "external_project"}),
|
||||
("capabilities", (), {"mode": "rune_internal",
|
||||
"project": "issue161-harness"}),
|
||||
# ── submit_need: happy, empty objective, MATERIAL_GAP, approval,
|
||||
# no-generate, custom contract id, task_ref ──
|
||||
("submit_need", ("sn-ok", rig.build_doc(), "persistent project export"), {}),
|
||||
("submit_need", ("sn-empty", rig.build_doc(), ""), {}),
|
||||
("submit_need", ("sn-gap", rig.build_doc(),
|
||||
"change the exporter widget colors"),
|
||||
{"affected_surface": ["capability:exporter"], "constraints": {},
|
||||
"evidence_refs": ["x"]}),
|
||||
("submit_need", ("sn-appr", rig.build_doc(),
|
||||
"start a brand new repository for billing"),
|
||||
{"constraints": {"new_repository": True}, "evidence_refs": ["x"]}),
|
||||
("submit_need", ("sn-nogen", rig.build_doc(), "persistent project export"),
|
||||
{"generate": False}),
|
||||
("submit_need", ("sn-cid", rig.build_doc(), "persistent project export"),
|
||||
{"contract_id": "MY-CID"}),
|
||||
("submit_need", ("sn-task", rig.build_doc(), "persistent project export"),
|
||||
{"task_ref": "task-1"}),
|
||||
# ── inspect_decision: ok + unknown ──
|
||||
("inspect_decision", ("in-ok", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
("inspect_decision", ("in-no", DOC_SUBMITTED, "NOPE"), {}),
|
||||
# ── get_scope: ok + unknown ──
|
||||
("get_scope", ("gs-ok", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
("get_scope", ("gs-no", DOC_SUBMITTED, "NOPE"), {}),
|
||||
# ── get_obligations: unknown (ok carries hash ids → hash battery) ──
|
||||
("get_obligations", ("go-no", DOC_SUBMITTED, "NOPE"), {}),
|
||||
# ── report_observation: ok, bare-dict, missing entity, unknown ──
|
||||
("report_observation", ("ro-ok", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED),
|
||||
[{"entity": "contract:csv_export",
|
||||
"detail": "observed while building"}]), {}),
|
||||
("report_observation", ("ro-bare", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED),
|
||||
{"entity": "x"}), {}),
|
||||
("report_observation", ("ro-noent", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED),
|
||||
[{"detail": "x"}]), {}),
|
||||
("report_observation", ("ro-no", DOC_SUBMITTED, "NOPE",
|
||||
[{"entity": "x"}]), {}),
|
||||
# ── verify: unknown, scope unknown/green/red, layout PASS/FAIL/UNKNOWN,
|
||||
# walked-contract contract-quality projection ──
|
||||
("verify", ("vf-no", DOC_SUBMITTED, "NOPE"), {}),
|
||||
("verify", ("vf-base", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
("verify", ("vf-green", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)),
|
||||
{"changed_files": ["src/allowed.py"]}),
|
||||
("verify", ("vf-red", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)),
|
||||
{"changed_files": ["src/evil.py"]}),
|
||||
("verify", ("vf-pass", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)),
|
||||
{"layout_observations": {"components": []}}),
|
||||
("verify", ("vf-fail", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)),
|
||||
{"layout_observations": {"header": {"region": "bottom"}}}),
|
||||
("verify", ("vf-unk", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)),
|
||||
{"layout_observations": {"header": {"region": "top"}}}),
|
||||
# ── transition: happy edge, illegal edge, unknown, guard block/pass ──
|
||||
("transition", ("tr-appr", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), "approved"), {}),
|
||||
("transition", ("tr-ill", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), "closed"), {}),
|
||||
("transition", ("tr-no", DOC_SUBMITTED, "NOPE", "approved"), {}),
|
||||
("transition", ("tr-block", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), "approved"),
|
||||
{"guard_checks": GUARD_FAIL}),
|
||||
("transition", ("tr-pass", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), "approved"),
|
||||
{"guard_checks": GUARD_PASS}),
|
||||
("transition", ("tr-assert", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), "approved"),
|
||||
{"guard_checks": GUARD_ASSERT}),
|
||||
# ── close: full ladder doc, unknown, custom scope files ──
|
||||
("close", ("cl-ok", DOC_WALKED, rig._submitted_cid(DOC_WALKED),
|
||||
[{"kind": "port_evidence", "detail": "gap closed"}]), {}),
|
||||
("close", ("cl-scope", DOC_WALKED, rig._submitted_cid(DOC_WALKED),
|
||||
[{"kind": "port_evidence", "detail": "gap closed"}]),
|
||||
{"scope_files": ["src/export.py"]}),
|
||||
("close", ("cl-no", DOC_WALKED, "NOPE", [{"entity": "x"}]), {}),
|
||||
# ── _programme_continuation: bound, unbound stage, no programme dir ──
|
||||
("_programme_continuation",
|
||||
("pc-hit", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)),
|
||||
{"programmes": PROGRAMMES_BOUND}),
|
||||
("_programme_continuation",
|
||||
("pc-miss", DOC_SUBMITTED, "other-stage"),
|
||||
{"programmes": PROGRAMMES_BOUND}),
|
||||
("_programme_continuation",
|
||||
("pc-none", DOC_SUBMITTED, rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
# ── _auto_material_change: hit, miss, no ledger ──
|
||||
("_auto_material_change",
|
||||
("mc-hit", DOC_SUBMITTED, ["src/export.py"]),
|
||||
{"capabilities": DOC_WITH_LEDGER_CAPS}),
|
||||
("_auto_material_change",
|
||||
("mc-miss", DOC_SUBMITTED, ["src/zzz.py"]),
|
||||
{"capabilities": DOC_WITH_LEDGER_CAPS}),
|
||||
("_auto_material_change",
|
||||
("mc-nol", DOC_SUBMITTED, ["src/export.py"]), {}),
|
||||
# ── discover_active_contract: seeded store, empty store ──
|
||||
("discover_active_contract", ("dc-seed",
|
||||
[{"project": "alpha", "doc": rig._discover_doc("old", "2026-10-01T00:00:00Z", "alpha")},
|
||||
{"project": None, "doc": rig._discover_doc("main-latest", "2026-10-03T00:00:00Z", "gamma")}]),
|
||||
{}),
|
||||
("discover_active_contract", ("dc-empty", []), {}),
|
||||
# ── resume: unknown (ok carries hash ids → hash battery) ──
|
||||
("resume", ("rs-no", DOC_SUBMITTED, "NOPE"), {}),
|
||||
# ── amend_scope: ok, non-user, unknown ──
|
||||
("amend_scope", ("am-ok", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), ["src/a.py"]),
|
||||
{"reason": "user asked"}),
|
||||
("amend_scope", ("am-nouser", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED), ["src/a.py"]),
|
||||
{"reason": "x", "authority": "builder"}),
|
||||
("amend_scope", ("am-no", DOC_SUBMITTED, "NOPE", ["a"]),
|
||||
{"reason": "x"}),
|
||||
# ── main: every CLI command ──
|
||||
("main", ("m-help",), {"argv": ["--help"]}),
|
||||
("main", ("m-unk",), {"argv": ["frobnicate"]}),
|
||||
("main", ("m-caps",), {"argv": ["capabilities"]}),
|
||||
("main", ("m-cls",), {"argv": ["classify",
|
||||
'{"objective": "fix typo"}']}),
|
||||
("main", ("m-ctx",), {"argv": ["context"]}),
|
||||
("main", ("m-sub",), {"argv": ["submit-need",
|
||||
'{"objective": "x"}']}),
|
||||
("main", ("m-dec",), {"argv": ["decision", "NOPE"]}),
|
||||
("main", ("m-scope",), {"argv": ["scope", "NOPE"]}),
|
||||
("main", ("m-obl",), {"argv": ["obligations", "NOPE"]}),
|
||||
("main", ("m-obs",), {"argv": ["observe", "NOPE", "[]"]}),
|
||||
("main", ("m-ver",), {"argv": ["verify", "NOPE"]}),
|
||||
("main", ("m-tr",), {"argv": ["transition", "NOPE", "approved"]}),
|
||||
("main", ("m-close",), {"argv": ["close", "NOPE", "[]"]}),
|
||||
("main", ("m-res",), {"argv": ["resume", "NOPE"]}),
|
||||
# ── the FULL ladder in ONE process (shared-store clobber law) ──
|
||||
("ladder_walk", ("walk1",), {}),
|
||||
("ladder_walk", ("walk2", "start a brand new repository for billing"), {}),
|
||||
]
|
||||
|
||||
truth = freeze("rune.construction_interface", cases,
|
||||
oracle_module="rune.construction_interface")
|
||||
write(truth, REPO / "src" / "rust" / "construction_interface"
|
||||
/ "frozen_ci_truth.json")
|
||||
print(f"froze {len(truth)} cases")
|
||||
|
||||
# ── §2 hash battery: every envelope carrying obligation ids. Captured +
|
||||
# verified with PYTHONHASHSEED=0 (set at INTERPRETER start — the seed
|
||||
# cannot be changed mid-process, so the main run re-execs itself for
|
||||
# this part); the Rust battery SHAPE-pins these ids (prefix before ":",
|
||||
# stable description/kind/provenance/status) — never value-freezes the
|
||||
# randomized suffix. ──
|
||||
HASH_CASES = [
|
||||
("_obligations_projection", (rig.contract_of(DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)),), {}),
|
||||
("construction_context", ("hc-active", DOC_SUBMITTED), {}),
|
||||
("construction_context", ("hc-explicit", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
("get_obligations", ("ho-ok", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
("resume", ("hr-ok", DOC_SUBMITTED,
|
||||
rig._submitted_cid(DOC_SUBMITTED)), {}),
|
||||
]
|
||||
|
||||
if os.environ.get("CI_GEN_HASH_ONLY") == "1":
|
||||
assert os.environ.get("PYTHONHASHSEED") == "0", \
|
||||
"hash battery requires PYTHONHASHSEED=0 at interpreter start"
|
||||
hash_truth = freeze("rune.construction_interface", HASH_CASES,
|
||||
oracle_module="rune.construction_interface")
|
||||
write(hash_truth, REPO / "src" / "rust" / "construction_interface"
|
||||
/ "frozen_ci_truth_hash.json")
|
||||
print(f"froze {len(hash_truth)} hash cases (PYTHONHASHSEED=0)")
|
||||
else:
|
||||
env = dict(os.environ, PYTHONHASHSEED="0", CI_GEN_HASH_ONLY="1",
|
||||
PYTHONPATH="scripts/ci_freeze_seams:src")
|
||||
subprocess.run([sys.executable, str(Path(__file__).resolve())],
|
||||
check=True, env=env, cwd=str(REPO))
|
||||
+101
-1
@@ -3,19 +3,107 @@
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "construction_interface"
|
||||
name = "capability_fit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "capability_ledger"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "construction_decision"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_fit",
|
||||
"capability_scoring",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toolchain",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "construction_interface"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_ledger",
|
||||
"construction_decision",
|
||||
"construction_mode",
|
||||
"layout_contract",
|
||||
"scaffold_core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "construction_mode"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "construction_pattern"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "construction_patterns_builtin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_scoring",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "layout_contract"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -40,6 +128,10 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scaffold_core"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -76,6 +168,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -94,6 +187,13 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toolchain"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
|
||||
@@ -8,4 +8,9 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
construction_decision = { path = "../construction_decision" }
|
||||
construction_mode = { path = "../construction_mode" }
|
||||
capability_ledger = { path = "../capability_ledger" }
|
||||
layout_contract = { path = "../layout_contract" }
|
||||
scaffold_core = { path = "../scaffold_core" }
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,125 @@
|
||||
//! frozen_battery — replay the FROZEN oracle truth VERBATIM
|
||||
//! (frozen_ci_truth.json + frozen_ci_truth_hash.json, generated by
|
||||
//! scripts/gen_construction_interface_truth.py against the LIVE Python
|
||||
//! oracle through the scripts/ci_freeze_seams rig: pinned
|
||||
//! RUNE_*_CORE=python floors, frozen clock, deterministic uuid mint,
|
||||
//! hermetic per-case stores).
|
||||
//!
|
||||
//! The battery never imports the Python oracle — parity stays provable
|
||||
//! after the strangler exits. The main corpus (85 cases) replays
|
||||
//! byte-exact; the hash corpus (5 cases) replays SHAPE-pinned: Python's
|
||||
//! str-hash obligation ids are process-randomized, so both sides are
|
||||
//! normalized (kind:<digits> → kind:#) before compare — the memory_
|
||||
//! consolidation hash-id law.
|
||||
|
||||
use crate::kernel;
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_ci_truth.json");
|
||||
const HASH_TRUTH: &str = include_str!("../frozen_ci_truth_hash.json");
|
||||
|
||||
fn cases(text: &str) -> Vec<Value> {
|
||||
serde_json::from_str(text).expect("frozen truth parses")
|
||||
}
|
||||
|
||||
fn case_id(c: &Value) -> String {
|
||||
c.get("args")
|
||||
.and_then(|a| a.as_array())
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("?")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let mut n = 0;
|
||||
for case in cases(TRUTH) {
|
||||
let got = kernel::dispatch(&case);
|
||||
assert_eq!(
|
||||
got,
|
||||
case.get("expected").cloned().unwrap_or(Value::Null),
|
||||
"case {} ({}) diverged",
|
||||
case.get("fn").and_then(|f| f.as_str()).unwrap_or("?"),
|
||||
case_id(&case),
|
||||
);
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(n, 85, "the frozen corpus carries 85 cases");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_truth_replays_shape_pinned() {
|
||||
let mut n = 0;
|
||||
for case in cases(HASH_TRUTH) {
|
||||
let got = kernel::dispatch(&case);
|
||||
let expected = case.get("expected").cloned().unwrap_or(Value::Null);
|
||||
assert_eq!(
|
||||
normalize_hash_ids(got),
|
||||
normalize_hash_ids(expected),
|
||||
"hash case {} ({}) diverged beyond the id shape",
|
||||
case.get("fn").and_then(|f| f.as_str()).unwrap_or("?"),
|
||||
case_id(&case),
|
||||
);
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(n, 5, "the hash corpus carries 5 cases");
|
||||
}
|
||||
|
||||
/// kind:<up-to-8 digits> → kind:# — the obligation-id seam (Python
|
||||
/// str-hash ids are process-randomized; the shape is the contract).
|
||||
fn normalize_hash_ids(v: Value) -> Value {
|
||||
match v {
|
||||
Value::String(s) => {
|
||||
if let Some((kind, digits)) = s.split_once(':') {
|
||||
if matches!(
|
||||
kind,
|
||||
"contract" | "regression" | "verification" | "composition" | "acquisition"
|
||||
) && !digits.is_empty()
|
||||
&& digits.len() <= 8
|
||||
&& digits.chars().all(|c| c.is_ascii_digit())
|
||||
{
|
||||
return Value::String(format!("{}:#", kind));
|
||||
}
|
||||
}
|
||||
Value::String(s)
|
||||
}
|
||||
Value::Array(a) => Value::Array(a.into_iter().map(normalize_hash_ids).collect()),
|
||||
Value::Object(o) => Value::Object(
|
||||
o.into_iter()
|
||||
.map(|(k, v)| (k, normalize_hash_ids(v)))
|
||||
.collect(),
|
||||
),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_missing_fns_are_real_ports() {
|
||||
// the two audit-named gap fns exist as name-preserving kernels with
|
||||
// exercised behavior (not stubs): main (CLI transport) and
|
||||
// _programme_continuation (programme ladder completion).
|
||||
let argv: Vec<String> = vec!["--help".to_string()];
|
||||
let d = kernel::main_dispatch(&argv);
|
||||
assert_eq!(d["exit"], serde_json::json!(0));
|
||||
let progs = vec![serde_json::json!({
|
||||
"programme_id": "p1", "objective": "o",
|
||||
"stages": [
|
||||
{"stage_id": "S1", "objective": "a", "state": "PENDING",
|
||||
"depends_on": [], "priority": 0},
|
||||
{"stage_id": "S2", "objective": "b", "state": "PENDING",
|
||||
"depends_on": [], "priority": 0},
|
||||
],
|
||||
"fallbacks": [],
|
||||
})];
|
||||
let (cont, after) = kernel::programme_continuation(&progs, "S1", "2026-10-13T09:41:23Z");
|
||||
assert_eq!(cont["stage_marked_complete"], serde_json::json!("S1"));
|
||||
assert_eq!(cont["canary"]["verdict"], serde_json::json!("PROGRAMME_OPEN"));
|
||||
assert_eq!(cont["canary"]["next_action"], serde_json::json!(["start", "S2"]));
|
||||
assert_eq!(cont["canary"]["reason"], serde_json::json!("1/2 complete; 1 eligible — continue immediately"));
|
||||
let after = after.as_array().unwrap();
|
||||
assert_eq!(after[0]["stages"][0]["state"], serde_json::json!("COMPLETE"));
|
||||
// save drops the in-memory result — the persisted shape is 5-key stages
|
||||
assert!(after[0]["stages"][0].get("result").is_none());
|
||||
assert_eq!(after[0]["saved_at"], serde_json::json!("2026-10-13T09:41:23Z"));
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,18 @@
|
||||
//! IllegalPhaseTransition legality map. The 16-edge matrix truth battery
|
||||
//! lives in tests (8 legal, 8 illegal — verified against the Python
|
||||
//! scaffold authority 2026-10-07).
|
||||
//!
|
||||
//! kernel.rs + projection.rs carry the gap-closing doc-in/doc-out kernels
|
||||
//! (dichotomy wave #2479): the envelope-assembly and doc-mutation halves of
|
||||
//! the ConstructionInterface methods whose inputs are fully carried by the
|
||||
//! contract doc + caller args. Stores/clocks/guard subprocesses stay behind
|
||||
//! the facade's injectable seams.
|
||||
|
||||
pub mod kernel;
|
||||
pub mod projection;
|
||||
|
||||
#[cfg(test)]
|
||||
mod frozen_battery;
|
||||
|
||||
/// The legal phase transitions. This map IS the scaffold authority's
|
||||
/// twin — changing one side without the other fails the parity battery.
|
||||
@@ -285,6 +297,32 @@ fn to_cstring(s: String) -> *mut u8 {
|
||||
ptr
|
||||
}
|
||||
|
||||
/// ci_replay(case_json) -> result_json — the FFI replay surface over the
|
||||
/// frozen truth rows ({fn, args, kwargs}); the parity script and the Rust
|
||||
/// battery share kernel::dispatch, so both prove the same computation.
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)] // pinned: extern "C" ABI — the Survivor loader requires this signature
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ci_replay(arg: *const u8, arg_len: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
let slice = std::slice::from_raw_parts(arg, arg_len);
|
||||
let text = std::str::from_utf8(slice).unwrap_or("{}");
|
||||
let case: serde_json::Value =
|
||||
serde_json::from_str(text).unwrap_or(serde_json::json!({}));
|
||||
let result = kernel::dispatch(&case);
|
||||
to_cstring(result.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// ci_free_string(ptr) — release a string returned by ci_replay.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ci_free_string(ptr: *mut u8) {
|
||||
if !ptr.is_null() {
|
||||
unsafe {
|
||||
drop(std::ffi::CString::from_raw(ptr as *mut i8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ffi_battery {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
//! projection.rs — the pure projection helpers of the construction
|
||||
//! interface (dichotomy wave #2479 gap port). Name-preserving 1:1 ports
|
||||
//! of construction_interface.py's module-level helpers; the contract doc
|
||||
//! travels across the boundary (kernel is pure, the facade owns stores).
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
/// Python `str()` over a JSON value (the common frozen-case shapes).
|
||||
pub fn py_str(v: &Value) -> String {
|
||||
match v {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Null => "None".to_string(),
|
||||
Value::Bool(b) => (if *b { "True" } else { "False" }).to_string(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python slice `s[:n]` semantics for a string.
|
||||
pub fn py_truncate(s: &str, n: usize) -> String {
|
||||
if s.chars().count() <= n {
|
||||
s.to_string()
|
||||
} else {
|
||||
s.chars().take(n).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// `_to_stage_check(spec)` — one guard spec → the StageCheck wire dict
|
||||
/// (name/kind defaults "guard"/"assert", path/output_pattern/description
|
||||
/// default ""). The callable `check` rides as JSON null across the FFI.
|
||||
pub fn to_stage_check(spec: &Value) -> Value {
|
||||
let get = |k: &str, d: &str| -> String {
|
||||
match spec.get(k) {
|
||||
Some(v) if !v.is_null() => py_str(v),
|
||||
_ => d.to_string(),
|
||||
}
|
||||
};
|
||||
json!({
|
||||
"name": get("name", "guard"),
|
||||
"kind": get("kind", "assert"),
|
||||
"argv": spec.get("argv").cloned().unwrap_or(Value::Null),
|
||||
"output_pattern": get("output_pattern", ""),
|
||||
"path": get("path", ""),
|
||||
"check": spec.get("check").cloned().unwrap_or(Value::Null),
|
||||
"description": get("description", ""),
|
||||
})
|
||||
}
|
||||
|
||||
/// `_guard_specs_from_obligations(c)` — the contract's guard declarations:
|
||||
/// identities.verification_obligations rows shaped `guard:<json-spec>`.
|
||||
/// JSON-decode failures and non-dict payloads are skipped — no invented
|
||||
/// facts.
|
||||
pub fn guard_specs_from_obligations(c: &Value) -> Vec<Value> {
|
||||
let mut specs = Vec::new();
|
||||
let Some(rows) = c
|
||||
.get("identities")
|
||||
.filter(|i| !i.is_null())
|
||||
.and_then(|i| i.get("verification_obligations"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.as_slice())
|
||||
else {
|
||||
return specs;
|
||||
};
|
||||
for item in rows {
|
||||
let Some(text) = item.as_str() else {
|
||||
continue;
|
||||
};
|
||||
if !text.starts_with("guard:") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(Value::Object(spec)) = serde_json::from_str::<Value>(&text[6..]) {
|
||||
specs.push(Value::Object(spec));
|
||||
}
|
||||
}
|
||||
specs
|
||||
}
|
||||
|
||||
/// `t_layout(ext)` — layout obligations from a contract's decision trace.
|
||||
pub fn t_layout(ext: &Value) -> Value {
|
||||
ext.get("decision_trace")
|
||||
.and_then(|t| t.get("layout_obligations"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!([]))
|
||||
}
|
||||
|
||||
/// The obligation-id seam. Python mints
|
||||
/// `f"{kind}:{abs(hash(str(item))) % 10**8}"` — str hash is
|
||||
/// process-randomized (PYTHONHASHSEED), so the value is NOT portable; the
|
||||
/// battery and parity check pin the SHAPE (`kind:` + up-to-8 digits),
|
||||
/// mirroring the memory_consolidation hash-id law. The kernel computes a
|
||||
/// deterministic stand-in id of the identical shape.
|
||||
pub fn obligation_id(kind: &str, item: &str) -> String {
|
||||
// FNV-1a 64 — deterministic stand-in, same shape as the Python hash id
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for b in item.as_bytes() {
|
||||
h ^= u64::from(*b);
|
||||
h = h.wrapping_mul(0x0000_0100_0000_01b3);
|
||||
}
|
||||
format!("{}:{}", kind, (h % 100_000_000))
|
||||
}
|
||||
|
||||
/// `_obligations_projection(c)` — the builder-facing obligations list:
|
||||
/// CONTRACT rows (identities.verification/regression_obligations),
|
||||
/// STRUCTURAL rows (trace obligations regression/verification/composition/
|
||||
/// acquisition), PATTERN rows and LAYOUT rows.
|
||||
pub fn obligations_projection(c: &Value) -> Vec<Value> {
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
let ident = c.get("identities").cloned().unwrap_or(json!({}));
|
||||
let t = c
|
||||
.get("ext")
|
||||
.and_then(|e| e.get("decision_trace"))
|
||||
.cloned()
|
||||
.unwrap_or(json!({}));
|
||||
let obl = t.get("obligations").cloned().unwrap_or(json!({}));
|
||||
|
||||
for (field_name, prov) in [
|
||||
("verification_obligations", "CONTRACT"),
|
||||
("regression_obligations", "CONTRACT"),
|
||||
] {
|
||||
if let Some(items) = ident.get(field_name).and_then(|v| v.as_array()) {
|
||||
for item in items {
|
||||
let text = py_str(item);
|
||||
out.push(json!({
|
||||
"id": obligation_id(&prov.to_lowercase(), &text),
|
||||
"description": py_truncate(&text, 200),
|
||||
"provenance": prov,
|
||||
"kind": field_name,
|
||||
"status": "REQUIRED",
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
for kind in ["regression", "verification", "composition", "acquisition"] {
|
||||
if let Some(items) = obl.get(kind).and_then(|v| v.as_array()) {
|
||||
for item in items {
|
||||
let text = py_str(item);
|
||||
out.push(json!({
|
||||
"id": obligation_id(kind, &text),
|
||||
"description": py_truncate(&text, 200),
|
||||
"provenance": "STRUCTURAL",
|
||||
"kind": kind,
|
||||
"status": "REQUIRED",
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(patterns) = obl.get("pattern").and_then(|v| v.as_array()) {
|
||||
for p in patterns {
|
||||
out.push(json!({
|
||||
"id": p.get("id").cloned().unwrap_or(Value::Null),
|
||||
"description": p.get("obligation").cloned().unwrap_or(Value::Null),
|
||||
"provenance": "PATTERN",
|
||||
"pattern": format!(
|
||||
"{}@r{}",
|
||||
p.get("pattern").and_then(|x| x.as_str()).unwrap_or(""),
|
||||
p.get("revision").and_then(|x| x.as_i64()).unwrap_or(0)
|
||||
),
|
||||
"pattern_applicability": p
|
||||
.get("pattern_applicability").and_then(|x| x.as_str()).unwrap_or(""),
|
||||
"evidence": p.get("reason").and_then(|x| x.as_str()).unwrap_or(""),
|
||||
"status": p.get("verdict").and_then(|x| x.as_str()).unwrap_or("REQUIRED"),
|
||||
}));
|
||||
}
|
||||
}
|
||||
if let Some(los) = t.get("layout_obligations").and_then(|v| v.as_array()) {
|
||||
for lo in los {
|
||||
out.push(json!({
|
||||
"id": format!(
|
||||
"layout:{}",
|
||||
lo.get("component").and_then(|x| x.as_str()).unwrap_or("")
|
||||
),
|
||||
"description": lo.get("obligation").cloned().unwrap_or(Value::Null),
|
||||
"provenance": "LAYOUT_CONTRACT",
|
||||
"status": "REQUIRED",
|
||||
"verification": lo.get("verification").cloned().unwrap_or(Value::Null),
|
||||
}));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `_obligation_scope_files(obligations)` — obligation ids referenced by
|
||||
/// closure scope checks (stub twin of the Python stub).
|
||||
pub fn obligation_scope_files(_obligations: &Value) -> Vec<Value> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// `_match(cap, f)` — capability source-location matching (the nested
|
||||
/// helper of _auto_material_change, literal name preserved).
|
||||
pub fn _match(cap: &Value, f: &str) -> bool {
|
||||
let Some(locs) = cap.get("source_locations").and_then(|v| v.as_array()) else {
|
||||
return false;
|
||||
};
|
||||
for loc in locs {
|
||||
if let Some(loc) = loc.as_str() {
|
||||
if f == loc || f.ends_with(&format!("/{}", loc)) || loc.ends_with(&format!("/{}", f)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `_envelope(operation, ...)` — the stable §12 output envelope with the
|
||||
/// `extra` merge.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn envelope(
|
||||
operation: &str,
|
||||
success: bool,
|
||||
ids: Value,
|
||||
state: Value,
|
||||
evidence: Value,
|
||||
errors: Value,
|
||||
warnings: Value,
|
||||
extra: Value,
|
||||
) -> Value {
|
||||
let mut env = Map::new();
|
||||
env.insert("schema".into(), json!(1));
|
||||
env.insert("operation".into(), json!(operation));
|
||||
env.insert("success".into(), json!(success));
|
||||
env.insert("ids".into(), if ids.is_null() { json!({}) } else { ids });
|
||||
env.insert("state".into(), if state.is_null() { json!({}) } else { state });
|
||||
env.insert(
|
||||
"evidence".into(),
|
||||
if evidence.is_null() { json!([]) } else { evidence },
|
||||
);
|
||||
env.insert("errors".into(), if errors.is_null() { json!([]) } else { errors });
|
||||
env.insert(
|
||||
"warnings".into(),
|
||||
if warnings.is_null() { json!([]) } else { warnings },
|
||||
);
|
||||
if let Value::Object(extra_map) = extra {
|
||||
for (k, v) in extra_map {
|
||||
env.insert(k, v);
|
||||
}
|
||||
}
|
||||
Value::Object(env)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_stage_check_defaults() {
|
||||
let c = to_stage_check(&json!({}));
|
||||
assert_eq!(c["name"], json!("guard"));
|
||||
assert_eq!(c["kind"], json!("assert"));
|
||||
assert_eq!(c["argv"], json!(null));
|
||||
assert_eq!(c["description"], json!(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_specs_skip_junk() {
|
||||
let c = json!({"identities": {"verification_obligations": [
|
||||
"guard:{\"name\": \"g1\"}", "free text", "guard:not-json{{",
|
||||
"[1]", "guard:{\"name\": \"g2\"}"]}});
|
||||
let specs = guard_specs_from_obligations(&c);
|
||||
assert_eq!(specs.len(), 2);
|
||||
assert_eq!(specs[0]["name"], json!("g1"));
|
||||
assert!(guard_specs_from_obligations(&json!({})).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_kinds() {
|
||||
let c = json!({
|
||||
"identities": {"verification_obligations": ["v-ob"]},
|
||||
"ext": {"decision_trace": {"obligations": {
|
||||
"pattern": [{"id": "p1", "pattern": "seed", "revision": 2,
|
||||
"obligation": "ob", "verdict": "SATISFIED",
|
||||
"reason": "r"}]},
|
||||
"layout_obligations": [{"component": "ui:h",
|
||||
"obligation": "o",
|
||||
"verification": "adapter"}]}}});
|
||||
let out = obligations_projection(&c);
|
||||
assert_eq!(out.len(), 3);
|
||||
assert_eq!(out[0]["provenance"], json!("CONTRACT"));
|
||||
assert_eq!(out[1]["provenance"], json!("PATTERN"));
|
||||
assert_eq!(out[1]["pattern"], json!("seed@r2"));
|
||||
assert_eq!(out[2]["id"], json!("layout:ui:h"));
|
||||
// shape law: the contract row id is kind:<up-to-8 digits>
|
||||
let id = out[0]["id"].as_str().unwrap();
|
||||
let num = id.split(':').nth(1).unwrap();
|
||||
assert!(num.len() <= 8 && num.chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_location_matching() {
|
||||
let cap = json!({"source_locations": ["src/export.py"]});
|
||||
assert!(_match(&cap, "src/export.py"));
|
||||
assert!(_match(&cap, "repo/src/export.py"));
|
||||
// oracle: "src/export.py/sub".endswith("/src/export.py") is False —
|
||||
// only exact, suffix-dir, and dir-suffix forms match
|
||||
assert!(!_match(&cap, "src/export.py/sub"));
|
||||
assert!(!_match(&cap, "src/other.py"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user