Files
rune/scripts/scaffold_oracle.py

149 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""scaffold_oracle.py — freeze/verify wrapper for the scaffold dichotomy
wave (contract BUILDER-2026-10-13T10:42:56Z-179836-factory-gap-sc).
The shared harness (scripts/freeze_truth.py) imports ONE module and replays
JSON-native cases against it. rune.scaffold's clock lives in the module-local
`now` bindings (sc.now from structural_store import; ss.now for the
structural-store internals) — no module-global `datetime` — so the harness's
built-in clock pin can't reach it. This wrapper pins BOTH bindings from
RUNE_FROZEN_NOW (set by the envelope's own frozen_now during --verify), pins
the structural core to the Python oracle (RUNE_STRUCTURAL_CORE=python), and
exposes tiny JSON-native capture helpers:
* init_case — Scaffold.init through a relative store path (IO-bearing;
the store file is written under the caller's CWD).
* prd/maintenance/bind/observe/reconcile_case — the mutating Scaffold methods
on a doc passed by value; result + mutated doc captured
(the harness expected carries the wrapper's return).
* repo_state_from_git_case — the git subprocess seam REPLACED by a scripted
fake (arg): no real IO, byte-deterministic.
* *_error_case — the oracle's error texts (unknown placement,
unbound repo observation) captured without raising.
The CLI-level operator surface (project-init/project-show/contract-*) and the
nested argv helpers (_scaffold_from_args/_cmd_args) are covered by the live
parity sweep + Go arg-convention tests instead (nested functions are not
module attributes the harness can resolve).
"""
import os
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
os.environ["RUNE_STRUCTURAL_CORE"] = "python" # deterministic freeze/verify mode
FROZEN = os.environ.get("RUNE_FROZEN_NOW", "")
if FROZEN:
import rune.scaffold as sc
import rune.structural_store as ss
sc.now = lambda: FROZEN
ss.now = lambda: FROZEN
from rune import scaffold as sc # noqa: E402 (Scaffold re-export for harness recv)
import json # noqa: E402
class _ShellResult:
def __init__(self, stdout, returncode):
self.stdout = stdout
self.returncode = returncode
self.stderr = ""
class _FakeGit:
"""subprocess module replacement scripted per call ({"stdout",
"returncode", "raise"} — "timeout"/"oserror" raise exactly the branches
the oracle catches). run() is the attribute scaffold.py uses; the
TimeoutExpired attribute must exist too — the oracle's except clause
references subprocess.TimeoutExpired THROUGH the patched binding."""
import subprocess as _sp # noqa: E402
TimeoutExpired = _sp.TimeoutExpired
def __init__(self, script):
self.steps = list(script)
def run(self, *args, **kwargs):
spec = self.steps.pop(0)
if spec.get("raise") == "timeout":
raise self._sp.TimeoutExpired(args[0] if args else "git", 15)
if spec.get("raise") == "oserror":
raise OSError("git unavailable")
return _ShellResult(spec.get("stdout", ""), spec.get("returncode", 0))
def _fresh_doc():
return {
"schema_version": 1,
"generated_at": FROZEN,
"entities": [],
"relationships": [],
"observations": [],
}
def init_case(store_dir, project_id, purpose="truth battery", requirement=""):
"""Scaffold.init on a relative store path (writes <store_dir>/current.json
under the caller's CWD — the verify run happens in a scratch CWD)."""
s = sc.Scaffold(store_dir, project_id)
doc = s.init(purpose=purpose, requirement=requirement)
return {"result": doc}
def set_prd_case(doc, project_id, prd_ref):
s = sc.Scaffold(".", project_id)
e = s.set_prd(doc, prd_ref)
return {"result": e, "doc": doc}
def add_maintenance_item_case(doc, name, **kw):
"""kwargs pass through to the oracle — its signature defaults
(approval=user, priority=normal, status=open) stay the authority."""
s = sc.Scaffold(".", "gap-project")
try:
e = s.add_maintenance_item(doc, name, **kw)
except TypeError as ex:
return {"result": None, "error": str(ex), "doc": doc}
return {"result": e, "doc": doc}
def bind_repo_case(doc, **kw):
"""kwargs pass through to the oracle — its signature defaults
(placement=EXISTING_PROJECT_REPO, worktree_strategy=per-contract) stay
the authority."""
s = sc.Scaffold(".", "gap-project")
try:
e = s.bind_repo(doc, **kw)
except ValueError as ex:
return {"result": None, "error": str(ex), "doc": doc}
return {"result": e, "doc": doc}
def observe_repo_case(doc, owner, name, **kw):
s = sc.Scaffold(".", "gap-project")
try:
e = s.observe_repo(doc, owner, name, **kw)
except ValueError as ex:
return {"result": None, "error": str(ex), "doc": doc}
return {"result": e, "doc": doc}
def repo_state_from_git_case(doc, owner, name, repo_root, script):
s = sc.Scaffold(".", "gap-project")
sc.subprocess = _FakeGit(script)
try:
e = s.repo_state_from_git(doc, owner, name, repo_root)
except ValueError as ex:
return {"result": None, "error": str(ex), "doc": doc}
return {"result": e, "doc": doc}
def reconcile_case(doc):
s = sc.Scaffold(".", "gap-project")
out = s.reconcile(doc)
return {"result": out, "doc": doc}