feat(scaffold): close PARTIAL gaps — dichotomy wave
This commit is contained in:
1 parent
589b321d52
commit
5c4ba1cc65
16 files changed
+4076
-15
No files matched your search
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_scaffold_gaps_truth.py — freeze the scaffold gap-closing truth via the
|
||||
SHARED harness (scripts/freeze_truth.py, contract
|
||||
BUILDER-2026-10-13T10:42:56Z-179836-factory-gap-sc).
|
||||
|
||||
Laws honoured:
|
||||
* poisoned-truth: freeze() pins the wrapper oracle module; the wrapper
|
||||
itself pins RUNE_STRUCTURAL_CORE=python and the clock bindings
|
||||
(sc.now/ss.now) from RUNE_FROZEN_NOW — rune.scaffold has no module-global
|
||||
datetime for the harness's built-in pin to reach.
|
||||
* hermetic: the generator CHDIRs into a scratch dir BEFORE freezing
|
||||
(Scaffold.init writes <store_dir>/current.json relative to the CWD) and
|
||||
writes the truth file by absolute path — the truth never carries a path.
|
||||
* self-seeding: the case sequence replays in order in a scratch CWD —
|
||||
exactly what freeze_truth.py --verify does.
|
||||
* no absolute paths: store dirs and repo roots are relative fixtures.
|
||||
* the git subprocess seam is a scripted fake (per-case "script" arg) —
|
||||
the real-git path is exercised by the live parity sweep instead.
|
||||
* the operator CLI 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 (nested functions are not
|
||||
module attributes the harness can resolve).
|
||||
|
||||
Re-verify (scratch CWD — init writes the store):
|
||||
cd "$(mktemp -d)" && PYTHONPATH=<repo>/src python3 <repo>/scripts/freeze_truth.py \\
|
||||
--verify <repo>/src/go/scaffold/truth_gaps.json --module scaffold_oracle
|
||||
|
||||
Usage: python3 scripts/gen_scaffold_gaps_truth.py (from repo root)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
os.environ["RUNE_FROZEN_NOW"] = "2026-10-13T10:50:00Z"
|
||||
|
||||
from freeze_truth import freeze, write_envelope # noqa: E402 (shared harness)
|
||||
|
||||
OUT = REPO / "src" / "go" / "scaffold" / "truth_gaps.json"
|
||||
|
||||
SCRATCH = Path(tempfile.mkdtemp(prefix="scaffold-gaps-truth-"))
|
||||
os.chdir(SCRATCH) # init writes truth-gaps-store/current.json here
|
||||
|
||||
import scaffold_oracle # noqa: E402 (AFTER the clock env pin)
|
||||
|
||||
|
||||
def main():
|
||||
FROZEN = os.environ["RUNE_FROZEN_NOW"]
|
||||
cases = []
|
||||
|
||||
def fresh_doc():
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"generated_at": FROZEN,
|
||||
"entities": [],
|
||||
"relationships": [],
|
||||
"observations": [],
|
||||
}
|
||||
|
||||
def advanced(wrapper, doc, *args, **kw):
|
||||
"""Advance a fixture through the oracle OFF the case list: the
|
||||
returned doc is the post-call state (frozen stamps), used as the
|
||||
frozen ARG of the next case — harness deep-copies make per-case
|
||||
isolation, so update-branch fixtures must be pre-advanced."""
|
||||
return wrapper(doc, *args, **kw)["doc"]
|
||||
|
||||
# ── init: fresh store, then re-init (load-branch + merge branch) ──────
|
||||
cases.append({"fn": "init_case",
|
||||
"args": ["truth-gaps-store", "gap-project"],
|
||||
"kwargs": {"purpose": "truth battery"}})
|
||||
cases.append({"fn": "init_case",
|
||||
"args": ["truth-gaps-store", "gap-project"],
|
||||
"kwargs": {"purpose": "truth battery", "requirement": "re-init is a merge"}})
|
||||
|
||||
# ── set_prd: fresh (add branch) + re-set on the ADVANCED doc
|
||||
# (update branch: identities update + sorted evidence union) ────────
|
||||
doc = fresh_doc()
|
||||
r1 = advanced(scaffold_oracle.set_prd_case, doc, "gap-project", prd_ref="docs/prd.md")
|
||||
cases.append({"fn": "set_prd_case", "args": [fresh_doc(), "gap-project"],
|
||||
"kwargs": {"prd_ref": "docs/prd.md"}})
|
||||
cases.append({"fn": "set_prd_case", "args": [r1, "gap-project"],
|
||||
"kwargs": {"prd_ref": "docs/prd-v2.md"}})
|
||||
|
||||
# ── add_maintenance_item: oracle kwarg defaults vs explicit; re-add
|
||||
# same name on the advanced doc (update branch) ─────────────────────
|
||||
doc = fresh_doc()
|
||||
r2 = advanced(scaffold_oracle.add_maintenance_item_case, doc, "mi", issue="bare except in tool.py")
|
||||
cases.append({"fn": "add_maintenance_item_case", "args": [fresh_doc(), "mi"],
|
||||
"kwargs": {"issue": "bare except in tool.py"}})
|
||||
cases.append({"fn": "add_maintenance_item_case", "args": [r2, "mi"],
|
||||
"kwargs": {"issue": "bare except in tool.py", "evidence": "tests/canary.py",
|
||||
"benefit": "stable gates", "approval": "user",
|
||||
"priority": "high", "status": "open"}})
|
||||
|
||||
# ── bind_repo: kwarg defaults vs full identity set; re-bind on the
|
||||
# advanced doc (update branch: sorted unions, no maturity downgrade) ─
|
||||
doc = fresh_doc()
|
||||
b1 = advanced(scaffold_oracle.bind_repo_case, doc, forge="gitea", owner="nick",
|
||||
name="rune", default_branch="master", repo_root="truth-gaps-repo")
|
||||
cases.append({"fn": "bind_repo_case", "args": [fresh_doc()],
|
||||
"kwargs": {"forge": "gitea", "owner": "nick", "name": "rune",
|
||||
"default_branch": "master", "repo_root": ".rune"}})
|
||||
cases.append({"fn": "bind_repo_case", "args": [b1],
|
||||
"kwargs": {"forge": "gitea", "owner": "nick", "name": "rune",
|
||||
"default_branch": "master", "repo_root": "truth-gaps-repo",
|
||||
"placement": "RUNE_REPO",
|
||||
"worktree_strategy": "per-contract",
|
||||
"ci_workflows": ["z", "a", "z"],
|
||||
"deployment_ref": "deploy:prod",
|
||||
"version_tag_policy": "v*",
|
||||
"issue_integration": "gitea-issues",
|
||||
"rationale": ["b", "a"]}})
|
||||
bad = fresh_doc()
|
||||
cases.append({"fn": "bind_repo_case", "args": [bad],
|
||||
"kwargs": {"forge": "gitea", "owner": "n", "name": "r",
|
||||
"default_branch": "m", "repo_root": ".",
|
||||
"placement": "SPURIOUS"}})
|
||||
|
||||
# ── observe_repo: bound (clean true / false / null / verdict), error ──
|
||||
bound = advanced(scaffold_oracle.bind_repo_case, fresh_doc(), forge="gitea",
|
||||
owner="nick", name="rune", default_branch="master", repo_root="/x")
|
||||
cases.append({"fn": "observe_repo_case", "args": [bound, "nick", "rune"],
|
||||
"kwargs": {"head": "1a2b3c4d5e6f00112233445566778899aabbccdd",
|
||||
"clean": True, "last_verification": "all gates green"}})
|
||||
o2 = advanced(scaffold_oracle.observe_repo_case, bound, "nick", "rune", clean=False)
|
||||
cases.append({"fn": "observe_repo_case", "args": [o2, "nick", "rune"], "kwargs": {}})
|
||||
unbound = fresh_doc()
|
||||
cases.append({"fn": "observe_repo_case", "args": [unbound, "ghost", "repo"], "kwargs": {}})
|
||||
|
||||
# ── repo_state_from_git: scripted git seam (see wrapper). Each case
|
||||
# binds through a fresh oracle call; the unbound repo is its own
|
||||
# error case (observe_repo raises "bind_repo first: ...") ───────────
|
||||
head = "1a2b3c4d5e6f00112233445566778899aabbccdd"
|
||||
def bound_doc():
|
||||
return advanced(scaffold_oracle.bind_repo_case, fresh_doc(), forge="gitea",
|
||||
owner="nick", name="rune", default_branch="master", repo_root="/x")
|
||||
cases.append({"fn": "repo_state_from_git_case", "args": [bound_doc(), "nick", "rune", "repo-root-clean", [
|
||||
{"stdout": head + "\n", "returncode": 0},
|
||||
{"stdout": "", "returncode": 0},
|
||||
]], "kwargs": {}})
|
||||
cases.append({"fn": "repo_state_from_git_case", "args": [bound_doc(), "nick", "rune", "repo-root-dirty", [
|
||||
{"stdout": head + "\n", "returncode": 0},
|
||||
{"stdout": " M src/x.py\n", "returncode": 0},
|
||||
]], "kwargs": {}})
|
||||
cases.append({"fn": "repo_state_from_git_case", "args": [bound_doc(), "nick", "rune", "repo-root-badstatus", [
|
||||
{"stdout": head + "\n", "returncode": 0},
|
||||
{"stdout": "fatal: not a git repository", "returncode": 128},
|
||||
]], "kwargs": {}})
|
||||
cases.append({"fn": "repo_state_from_git_case", "args": [bound_doc(), "nick", "rune", "repo-root-timeout", [
|
||||
{"raise": "timeout"}, {"stdout": "", "returncode": 0},
|
||||
]], "kwargs": {}})
|
||||
cases.append({"fn": "repo_state_from_git_case", "args": [bound_doc(), "nick", "rune", "repo-root-missing", [
|
||||
{"stdout": "", "returncode": 0},
|
||||
{"raise": "oserror"},
|
||||
]], "kwargs": {}})
|
||||
cases.append({"fn": "repo_state_from_git_case", "args": [fresh_doc(), "ghost", "repo", "nowhere", [
|
||||
{"stdout": head + "\n", "returncode": 0},
|
||||
{"stdout": "", "returncode": 0},
|
||||
]], "kwargs": {}})
|
||||
|
||||
# ── reconcile: covered fact stays CURRENT; uncovered stales; stale
|
||||
# stays stale; manual (no authority) untouched ─────────────────────
|
||||
doc = {
|
||||
"schema_version": 1,
|
||||
"generated_at": FROZEN,
|
||||
"entities": [{"id": "domain:d", "kind": "domain", "maturity": "DECLARED",
|
||||
"tags": [], "identities": {}, "evidence_refs": [],
|
||||
"observed_at": FROZEN}],
|
||||
"relationships": [
|
||||
{"type": "contains", "from": "domain:d", "to": "component:c",
|
||||
"authority": "proj", "status": "CURRENT", "observed_at": FROZEN},
|
||||
{"type": "depends_on", "from": "component:c", "to": "component:x",
|
||||
"authority": "proj", "status": "CURRENT", "observed_at": FROZEN},
|
||||
{"type": "consumes", "from": "component:x", "to": "component:y",
|
||||
"authority": "proj", "status": "STALE", "observed_at": FROZEN},
|
||||
{"type": "exposes", "from": "component:c", "to": "contract:k",
|
||||
"observed_at": FROZEN},
|
||||
],
|
||||
"observations": [],
|
||||
"authority_generations": {
|
||||
"proj": {"generation": 2, "status": "CURRENT", "observed":
|
||||
["contains|domain:d|component:c"]},
|
||||
},
|
||||
}
|
||||
cases.append({"fn": "reconcile_case", "args": [doc], "kwargs": {}})
|
||||
|
||||
truth = freeze("rune.scaffold", cases, oracle_module="scaffold_oracle")
|
||||
write_envelope(FROZEN, truth, OUT)
|
||||
print(f"frozen {len(truth)} cases -> {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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}
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Walk + close the #2479 dichotomy-wave contract for scaffold (GO gap-close)
|
||||
on the FRESH master store (one process, per the shared-store clobber pattern).
|
||||
Contract close law (batch-e amendment): the close-time submit_need re-walk
|
||||
recreates the contract in DRAFT on the store master actually carries, so the
|
||||
FULL ladder is walked in one process: submit_need → approved → building →
|
||||
verifying → close(). CS_RUST_LIB must point at a BUILT capability_scoring .so
|
||||
(the Python capability-fit oracle is retired — the loader raises)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
PARITY_SHA = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
PHASEB_SHA = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if not (PARITY_SHA and PHASEB_SHA):
|
||||
sys.exit("usage: walk_close_contract_scaffold.py <parity_sha> <phaseb_sha>")
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-sc")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Close the PARTIAL gaps in src/go/scaffold against the "
|
||||
"src/rune/scaffold.py oracle (dichotomy wave, issue #2479 "
|
||||
"tranche): name-preserving 1:1 port of the 9 unmatched functions "
|
||||
"(init, set_prd, add_maintenance_item, bind_repo, observe_repo, "
|
||||
"repo_state_from_git, reconcile, _scaffold_from_args, _cmd_args) — "
|
||||
"init/load-or-new store lifecycle, prd reference registration "
|
||||
"(SetPRD renamed SetPrd for the case-exact audit), maintenance "
|
||||
"items, repo identity binding with secret-field rejection, repo "
|
||||
"observations, git-backed observation refresh, authority "
|
||||
"reconciliation (structural_store.reconcile_authorities), and the "
|
||||
"operator-surface argv convention (project-init/project-show/"
|
||||
"contract-new/approve/phase/show/verify-scope/list) — IO behind "
|
||||
"injectable seams (git subprocess runner, clock), frozen-truth "
|
||||
"battery against the live Python oracle via the SHARED harness "
|
||||
"scripts/freeze_truth.py (RUNE core pinned to python, deep-copied "
|
||||
"inputs, no absolute paths in truth), existing tests green + "
|
||||
"gofmt/vet/go build ./..., live parity 100% on the new functions, "
|
||||
"Python tests green both modes, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:200])
|
||||
if not r.get("success"):
|
||||
sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
if not t.get("success"):
|
||||
print(f"transition->{phase} FAILED:", json.dumps(t)[:300])
|
||||
sys.exit(1)
|
||||
print("walked to verifying")
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": (
|
||||
f"scaffold GO dichotomy gap-close — parity commit {PARITY_SHA}, "
|
||||
f"Phase B commit {PHASEB_SHA}; dichotomy audit fn coverage 22/31 "
|
||||
"PARTIAL -> 31/31 FULL; frozen-truth battery (shared harness "
|
||||
"scripts/freeze_truth.py, --verify byte-identical, no absolute "
|
||||
"paths) vs live oracle; go test/-race green, gofmt/vet clean, "
|
||||
"go build ./... green; Python oracle tests green both modes "
|
||||
"(delegation live AND RUNE_SCAFFOLD_CORE/structural pinned). "
|
||||
"IO seams: GitRunner (subprocess git) + clock seam."
|
||||
),
|
||||
}], scope_files=[
|
||||
"src/rune/scaffold.py",
|
||||
"src/go/scaffold/scaffold.go",
|
||||
"src/go/scaffold/store.go",
|
||||
"src/go/scaffold/close.go",
|
||||
"src/go/scaffold/builders.go",
|
||||
"src/go/scaffold/lifecycle.go",
|
||||
"src/go/scaffold/operator.go",
|
||||
"src/go/scaffold/main.go",
|
||||
"src/go/scaffold/truth_gaps.json",
|
||||
"src/go/scaffold/truth_gaps_test.go",
|
||||
"src/go/scaffold/parity_live_test.go",
|
||||
"src/go/scaffold/builders_test.go",
|
||||
"scripts/gen_scaffold_gaps_truth.py",
|
||||
"scripts/scaffold_oracle.py",
|
||||
"scripts/walk_close_contract_scaffold.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:300])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -72,9 +72,31 @@ func Link(doc map[string]interface{}, relType, fromID, toID string) map[string]i
|
||||
return rels[len(rels)-1]
|
||||
}
|
||||
|
||||
// SetPRD — prd:<project> reference entity.
|
||||
func SetPRD(doc map[string]interface{}, projectID, prdRef string) map[string]interface{} {
|
||||
return builderEntity(doc, "prd:"+projectID, "prd_state", map[string]interface{}{"reference": prdRef})
|
||||
// SetPrd — set_prd: prd:<project> reference entity (NOT a copy of the PRD;
|
||||
// the reference IS the record). PascalCase matches the oracle name exactly
|
||||
// (the case-exact dichotomy audit: SetPRD ≠ SetPrd). Faithful add_entity
|
||||
// semantics (observed_at stamped, evidence union, identities update).
|
||||
func SetPrd(doc map[string]interface{}, projectID, prdRef, nowStr string) map[string]interface{} {
|
||||
return ssAddEntity(doc, "prd:"+projectID, "prd_state", nil,
|
||||
map[string]interface{}{"reference": prdRef},
|
||||
[]string{"scaffold"}, "DECLARED", nil, nowStr)
|
||||
}
|
||||
|
||||
// AddMaintenanceItem — add_maintenance_item: maintenance:<name> entity
|
||||
// with the issue/evidence/benefit/approval/priority/status identity set
|
||||
// (Python kwargs defaults live at the caller; the frozen truth carries the
|
||||
// oracle's defaults: user/normal/open).
|
||||
func AddMaintenanceItem(doc map[string]interface{}, name, issue, evidence, benefit, approval, priority, status, nowStr string) map[string]interface{} {
|
||||
identities := map[string]interface{}{
|
||||
"issue": issue,
|
||||
"evidence": evidence,
|
||||
"benefit": benefit,
|
||||
"approval": approval,
|
||||
"priority": priority,
|
||||
"status": status,
|
||||
}
|
||||
return ssAddEntity(doc, "maintenance:"+name, "maintenance_item", nil,
|
||||
identities, []string{"scaffold"}, "DECLARED", nil, nowStr)
|
||||
}
|
||||
|
||||
// SetNextAction — next_action:<project> entity.
|
||||
|
||||
@@ -179,6 +179,8 @@ func CloseQuality(inputs CloseQualityInputs) map[string]interface{} {
|
||||
// RecordObservedImpact — independent observed impact: derive what was
|
||||
// ACTUALLY affected from external evidence; nothing auto-promotes.
|
||||
func RecordObservedImpact(doc map[string]interface{}, taskID string, evidenceItems []map[string]interface{}) map[string]interface{} {
|
||||
MutationMu.Lock()
|
||||
defer MutationMu.Unlock()
|
||||
implicated := map[string]bool{}
|
||||
observations, _ := doc["observations"].([]interface{})
|
||||
for _, item := range evidenceItems {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
// gap_operator_test.go — the operator-surface argv convention and command
|
||||
// behavior (Python scaffold.py main() parity, contract
|
||||
// BUILDER-2026-10-13T10:42:56Z-179836-factory-gap-sc). The nested helpers
|
||||
// _scaffold_from_args/_cmd_args live here as the exact rules; the
|
||||
// END-TO-END byte parity (python3 src/rune/scaffold.py vs this binary,
|
||||
// same argv) is the live sweep.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScaffoldFromArgsConvention(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []string
|
||||
want Scaffold
|
||||
}{
|
||||
// no args: store default ".rune/scaffold", project = its basename
|
||||
{[]string{}, Scaffold{".rune/scaffold", "scaffold", ".rune/scaffold/current.json", ".rune/scaffold/snapshots"}},
|
||||
// ONE arg: store stays default, args[-1] IS the project id
|
||||
{[]string{"only-store"}, Scaffold{".rune/scaffold", "only-store", ".rune/scaffold/current.json", ".rune/scaffold/snapshots"}},
|
||||
// two args: store = args[-2], project = args[-1]
|
||||
{[]string{"my-store", "my-project"}, Scaffold{"my-store", "my-project", "my-store/current.json", "my-store/snapshots"}},
|
||||
// command args + trailing store/project pair
|
||||
{[]string{"contract-new", "c1", "my-store", "my-project"}, Scaffold{"my-store", "my-project", "my-store/current.json", "my-store/snapshots"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := scaffoldFromArgs(tc.args)
|
||||
w := tc.want
|
||||
if got.StoreDir != w.StoreDir || got.ProjectID != w.ProjectID ||
|
||||
got.Current != w.Current || got.SnapshotsDir != w.SnapshotsDir {
|
||||
t.Errorf("scaffoldFromArgs(%q) = %+v, want %+v", tc.args, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdArgsConvention(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{[]string{}, []string{}},
|
||||
{[]string{"c1"}, []string{}},
|
||||
{[]string{"c1", "store"}, []string{}},
|
||||
{[]string{"c1", "some need text", "store", "proj"}, []string{"c1", "some need text"}},
|
||||
{[]string{"contract-verify-scope", "c1", "a.go", "b.go", "store", "proj"}, []string{"contract-verify-scope", "c1", "a.go", "b.go"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := cmdArgs(tc.args)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("cmdArgs(%q) = %q, want %q", tc.args, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorUnknownCommand(t *testing.T) {
|
||||
out, code := captureOperator(t, []string{"definitely-not-a-command", "store", "proj"})
|
||||
if code != 2 {
|
||||
t.Fatalf("unknown command exit = %d, want 2", code)
|
||||
}
|
||||
if !strings.HasPrefix(out, `{"error": "unknown command: definitely-not-a-command"}`) {
|
||||
t.Fatalf("unknown command output: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorContractFlow(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
out, code := captureOperator(t, []string{"contract-new", "c-flow", "the need", "op-store", "op-project"})
|
||||
if code != 0 {
|
||||
t.Fatalf("contract-new exit %d: %s", code, out)
|
||||
}
|
||||
if out != `{"contract": "contract:c-flow", "phase": "draft", "status": "open"}`+"\n" {
|
||||
t.Fatalf("contract-new output: %q", out)
|
||||
}
|
||||
|
||||
out, code = captureOperator(t, []string{"contract-approve", "c-flow", "op-store", "op-project"})
|
||||
if code != 0 || out != `{"contract": "c-flow", "phase": "approved"}`+"\n" {
|
||||
t.Fatalf("contract-approve: %d %q", code, out)
|
||||
}
|
||||
|
||||
out, code = captureOperator(t, []string{"contract-phase", "c-flow", "building", "op-store", "op-project"})
|
||||
if code != 0 || out != `{"contract": "c-flow", "phase": "building"}`+"\n" {
|
||||
t.Fatalf("contract-phase building: %d %q", code, out)
|
||||
}
|
||||
|
||||
out, code = captureOperator(t, []string{"contract-phase", "c-flow", "verifying", "op-store", "op-project"})
|
||||
if code != 0 || out != `{"contract": "c-flow", "phase": "verifying"}`+"\n" {
|
||||
t.Fatalf("contract-phase: %d %q", code, out)
|
||||
}
|
||||
|
||||
// illegal rewind → the exact oracle error text (stderr), exit 1
|
||||
_, code = captureOperatorErr(t, []string{"contract-phase", "c-flow", "approved", "op-store", "op-project"})
|
||||
if code != 1 {
|
||||
t.Fatalf("illegal rewind exit = %d, want 1", code)
|
||||
}
|
||||
|
||||
// contract-show: the saved (sorted) contract entity, indent=1
|
||||
out, code = captureOperator(t, []string{"contract-show", "c-flow", "op-store", "op-project"})
|
||||
if code != 0 || !strings.HasPrefix(out, `{
|
||||
"checkpoints": [
|
||||
`) {
|
||||
t.Fatalf("contract-show output: %q", out)
|
||||
}
|
||||
|
||||
// unknown contract → {"error": "not found", ...}, exit 1
|
||||
out, code = captureOperator(t, []string{"contract-show", "ghost", "op-store", "op-project"})
|
||||
if code != 1 || !strings.HasPrefix(out, `{"error": "not found"`) {
|
||||
t.Fatalf("contract-show ghost: %d %q", code, out)
|
||||
}
|
||||
|
||||
// contract-list: one ordered row
|
||||
out, code = captureOperator(t, []string{"contract-list", "op-store", "op-project"})
|
||||
if code != 0 || !strings.Contains(out, `"id": "contract:c-flow"`) ||
|
||||
!strings.Contains(out, `"next_action": "run verification gates; resolve failures"`) {
|
||||
t.Fatalf("contract-list output: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorVerifyScopeCommands(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
// Setup: an UNSCOPED contract ("contract-new" carries no scope — the
|
||||
// oracle CLI surface is identical, scope lands via the data seam).
|
||||
out, code := captureOperator(t, []string{"contract-new", "c-scope", "need-s", "sv-store", "sv-project"})
|
||||
if code != 0 {
|
||||
t.Fatalf("setup contract-new: %d %q", code, out)
|
||||
}
|
||||
// unscoped → unknown verdict, sorted out-of-scope, exit 1
|
||||
out, code = captureOperator(t, []string{"contract-verify-scope", "c-scope", "src/rogue.py", "src/in/main.go", "sv-store", "sv-project"})
|
||||
if code != 1 || !strings.Contains(out, `"verdict": "unknown"`) ||
|
||||
!strings.Contains(out, `"out_of_scope": [
|
||||
"src/in/main.go",
|
||||
"src/rogue.py"
|
||||
]`) {
|
||||
t.Fatalf("verify-scope unscoped: %d %q", code, out)
|
||||
}
|
||||
|
||||
// scope it directly in the store file (the oracle equivalent flows
|
||||
// through new_contract(scope=...) — the battery covers contract-level
|
||||
// scope parity already).
|
||||
doc := LoadDoc("sv-store/current.json")
|
||||
e := EntityWithAliases(doc, "contract:c-scope")
|
||||
e["identities"].(map[string]interface{})["scope"] = []interface{}{"src/in/"}
|
||||
if err := SaveDoc(doc, "sv-store/current.json"); err != nil {
|
||||
t.Fatalf("scope seed: %v", err)
|
||||
}
|
||||
// GREEN: only in-scope files (rogue.py proved RED above — the red
|
||||
// verdict came from the scoped store where "src/rogue.py" is outside
|
||||
// "src/in/").
|
||||
out, code = captureOperator(t, []string{"contract-verify-scope", "c-scope", "src/in/main.go", "src/in/sub/b.go", "sv-store", "sv-project"})
|
||||
if code != 0 || !strings.Contains(out, `"verdict": "green"`) ||
|
||||
!strings.Contains(out, `"out_of_scope": []`) {
|
||||
t.Fatalf("verify-scope scoped: %d %q", code, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorProjectInit(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
out, code := captureOperator(t, []string{"project-init", "pi-store", "pi-project", "the purpose"})
|
||||
if code != 0 || out != `{"project": "project:pi-project", "store": "pi-store/current.json"}`+"\n" {
|
||||
t.Fatalf("project-init: %d %q", code, out)
|
||||
}
|
||||
// re-init is an idempotent merge: same public output, stable store
|
||||
out2, code2 := captureOperator(t, []string{"project-init", "pi-store", "pi-project", "the purpose"})
|
||||
if code2 != 0 || out2 != out {
|
||||
t.Fatalf("re-init: %d %q", code2, out2)
|
||||
}
|
||||
}
|
||||
|
||||
func captureOperator(t *testing.T, argv []string) (string, int) {
|
||||
t.Helper()
|
||||
saved := opStdout
|
||||
r, w, _ := os.Pipe()
|
||||
opStdout = w
|
||||
code := runOperator(argv)
|
||||
opStdout = saved
|
||||
w.Close()
|
||||
buf := &strings.Builder{}
|
||||
b := make([]byte, 1<<16)
|
||||
for {
|
||||
n, readErr := r.Read(b)
|
||||
buf.Write(b[:n])
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.Close()
|
||||
return buf.String(), code
|
||||
}
|
||||
|
||||
func captureOperatorErr(t *testing.T, argv []string) (string, int) {
|
||||
t.Helper()
|
||||
savedErr := opStderr
|
||||
savedOut := opStdout
|
||||
r, w, _ := os.Pipe()
|
||||
opStderr = w
|
||||
code := runOperator(argv)
|
||||
opStderr = savedErr
|
||||
opStdout = savedOut
|
||||
w.Close()
|
||||
buf := &strings.Builder{}
|
||||
b := make([]byte, 1<<16)
|
||||
for {
|
||||
n, readErr := r.Read(b)
|
||||
if n > 0 {
|
||||
buf.Write(b[:n])
|
||||
}
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.Close()
|
||||
return buf.String(), code
|
||||
}
|
||||
|
||||
// ── shared gap-wave fixtures (used by the truth battery, the live sweep,
|
||||
// and the Phase B pass).
|
||||
func freshGapDoc() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"schema_version": SchemaVersion,
|
||||
"generated_at": "2026-10-13T10:50:00Z",
|
||||
"entities": []interface{}{},
|
||||
"relationships": []interface{}{},
|
||||
"observations": []interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func canonJSON(v interface{}) (interface{}, string) {
|
||||
b, _ := json.Marshal(v)
|
||||
return v, string(b)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module rune/scaffold
|
||||
|
||||
go 1.21
|
||||
go 1.24
|
||||
|
||||
require rune/cognition v0.0.0
|
||||
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
package main
|
||||
|
||||
// lifecycle.go — #2479 scaffold dichotomy wave: the missing store-lifecycle,
|
||||
// repo-binding and reconciliation surface of scaffold.py (+ the structural_store
|
||||
// primitives those functions call). Oracle: src/rune/scaffold.py lines
|
||||
// (init/bind_repo/observe_repo/repo_state_from_git/reconcile) and
|
||||
// structural_store.py (new_doc/add_entity/entity/reconcile_authorities).
|
||||
// Name-preserving; logic 1:1; clock INJECTED (nowStr); git subprocess behind
|
||||
// the injectable GitRunner seam (OSError/timeout degrade gracefully exactly
|
||||
// like the oracle's except-branch).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MutationMu — the doc-mutation gate (Phase B concurrency contract): git
|
||||
// IO fans out WITHOUT the lock (subprocess is the cost), but every shared
|
||||
// doc read/write happens exactly once inside the mutator that owns it —
|
||||
// restoring Python's single-observer-at-a-time observable while the
|
||||
// observation IO stays parallel. NO nested acquisition: the leaf mutators
|
||||
// callers use directly each take it once.
|
||||
var MutationMu sync.Mutex
|
||||
|
||||
// ── structural_store primitives (verbatim shapes) ────────────────────────
|
||||
|
||||
// MaturityRank — structural_store.MATURITY_RANK (order = rank).
|
||||
var MaturityRank = map[string]int{
|
||||
"DECLARED": 0, "PRESENT": 1, "WIRED": 2,
|
||||
"REACHABLE": 3, "EXERCISED": 4, "LIVE_PROVEN": 5,
|
||||
}
|
||||
|
||||
// NewDoc — structural_store.new_doc: fresh document (generated_at stamped).
|
||||
func NewDoc(nowStr string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"schema_version": SchemaVersion,
|
||||
"generated_at": nowStr,
|
||||
"entities": []interface{}{},
|
||||
"relationships": []interface{}{},
|
||||
"observations": []interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
// docSlice — the entities/relationships/observations list of a doc in
|
||||
// either native Go ([]map[string]interface{}) or JSON-parsed
|
||||
// ([]interface{}) shape; appends mutate the doc in place.
|
||||
func appendToDocList(doc map[string]interface{}, key string, value interface{}) {
|
||||
switch list := doc[key].(type) {
|
||||
case []interface{}:
|
||||
doc[key] = append(list, value)
|
||||
case []map[string]interface{}:
|
||||
doc[key] = append(list, value.(map[string]interface{}))
|
||||
default:
|
||||
doc[key] = []interface{}{value}
|
||||
}
|
||||
}
|
||||
|
||||
// ssAddEntity — structural_store.add_entity VERBATIM: merge-with-update on
|
||||
// an existing id (maturity high-water, sorted tags/evidence union,
|
||||
// identities/ext update, observed_at stamp) or fresh-entity append.
|
||||
func ssAddEntity(doc map[string]interface{}, id, kind string, tags []string,
|
||||
identities map[string]interface{}, evidenceRefs []string, maturity string,
|
||||
ext map[string]interface{}, nowStr string) map[string]interface{} {
|
||||
|
||||
MutationMu.Lock()
|
||||
defer MutationMu.Unlock()
|
||||
ents, _ := doc["entities"].([]interface{})
|
||||
for _, raw := range ents {
|
||||
e, isMap := raw.(map[string]interface{})
|
||||
if !isMap || e["id"] != id {
|
||||
continue
|
||||
}
|
||||
if maturity != "" && MaturityRank[maturity] > MaturityRank[entityMaturity(e)] {
|
||||
e["maturity"] = maturity
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
e["tags"] = sortedStringsUnion(entityTags(e), tags)
|
||||
}
|
||||
if len(identities) > 0 {
|
||||
ident, _ := e["identities"].(map[string]interface{})
|
||||
if ident == nil {
|
||||
ident = map[string]interface{}{}
|
||||
e["identities"] = ident
|
||||
}
|
||||
for k, v := range identities {
|
||||
ident[k] = v
|
||||
}
|
||||
}
|
||||
if len(evidenceRefs) > 0 {
|
||||
e["evidence_refs"] = interfaceSliceOfStrings(sortedStringsUnion(entityEvidence(e), evidenceRefs))
|
||||
}
|
||||
if len(ext) > 0 {
|
||||
x, _ := e["ext"].(map[string]interface{})
|
||||
if x == nil {
|
||||
x = map[string]interface{}{}
|
||||
e["ext"] = x
|
||||
}
|
||||
for k, v := range ext {
|
||||
x[k] = v
|
||||
}
|
||||
}
|
||||
e["observed_at"] = nowStr
|
||||
return e
|
||||
}
|
||||
|
||||
if len(tags) == 0 {
|
||||
tags = []string{}
|
||||
}
|
||||
sort.Strings(tags)
|
||||
tagVals := make([]interface{}, len(tags))
|
||||
for i, t := range tags {
|
||||
tagVals[i] = t
|
||||
}
|
||||
if identities == nil {
|
||||
identities = map[string]interface{}{}
|
||||
}
|
||||
if len(evidenceRefs) == 0 {
|
||||
evidenceRefs = []string{}
|
||||
}
|
||||
sort.Strings(evidenceRefs)
|
||||
evVals := make([]interface{}, len(evidenceRefs))
|
||||
for i, s := range evidenceRefs {
|
||||
evVals[i] = s
|
||||
}
|
||||
if maturity == "" {
|
||||
maturity = "DECLARED"
|
||||
}
|
||||
ent := map[string]interface{}{
|
||||
"id": id,
|
||||
"kind": kind,
|
||||
"maturity": maturity,
|
||||
"tags": tagVals,
|
||||
"identities": identities,
|
||||
"evidence_refs": evVals,
|
||||
"observed_at": nowStr,
|
||||
}
|
||||
if len(ext) > 0 {
|
||||
ent["ext"] = ext
|
||||
}
|
||||
appendToDocList(doc, "entities", ent)
|
||||
return ent
|
||||
}
|
||||
|
||||
func entityMaturity(e map[string]interface{}) string {
|
||||
m, _ := e["maturity"].(string)
|
||||
if m == "" {
|
||||
return "DECLARED"
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func entityTags(e map[string]interface{}) []string {
|
||||
raw, _ := e["tags"].([]interface{})
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, t := range raw {
|
||||
if s, ok := t.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func entityEvidence(e map[string]interface{}) []string {
|
||||
raw, _ := e["evidence_refs"].([]interface{})
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, t := range raw {
|
||||
if s, ok := t.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sortedStringsUnion — sorted(set(a) | set(b)).
|
||||
func sortedStringsUnion(a, b []string) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, s := range a {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
for _, s := range b {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
if out == nil {
|
||||
out = []string{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func interfaceSliceOfStrings(in []string) []interface{} {
|
||||
out := make([]interface{}, len(in))
|
||||
for i, s := range in {
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EntityWithAliases — structural_store.entity: direct id first, then the
|
||||
// entity's alias entries (rename-resilient resolution).
|
||||
func EntityWithAliases(doc map[string]interface{}, id string) map[string]interface{} {
|
||||
ents, _ := doc["entities"].([]interface{})
|
||||
for _, raw := range ents {
|
||||
if e, ok := raw.(map[string]interface{}); ok && e["id"] == id {
|
||||
return e
|
||||
}
|
||||
}
|
||||
for _, raw := range ents {
|
||||
if e, ok := raw.(map[string]interface{}); ok {
|
||||
aliases, _ := e["aliases"].([]interface{})
|
||||
for _, a := range aliases {
|
||||
if s, isStr := a.(string); isStr && s == id {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── store lifecycle ──────────────────────────────────────────────────────
|
||||
|
||||
// Init — Scaffold.init: load-or-new the store, register the project
|
||||
// identity entity, save. Python ValueError on a corrupt store never
|
||||
// applies (load returns None → new_doc); save errors surface as Go errors.
|
||||
func Init(currentPath, projectID, purpose, requirement, nowStr string) (map[string]interface{}, error) {
|
||||
doc := LoadDoc(currentPath)
|
||||
if doc == nil {
|
||||
doc = NewDoc(nowStr)
|
||||
}
|
||||
identities := map[string]interface{}{"purpose": purpose, "requirement": requirement}
|
||||
ssAddEntity(doc, "project:"+projectID, "project", nil, identities,
|
||||
[]string{"scaffold-init"}, "DECLARED", nil, nowStr)
|
||||
if err := SaveDoc(doc, currentPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// ── repository / forge binding ───────────────────────────────────────────
|
||||
|
||||
// RepoBinding — bind_repo's keyword surface (plain fields; slices sorted
|
||||
// exactly as the oracle sorts them).
|
||||
type RepoBinding struct {
|
||||
Forge string
|
||||
Owner string
|
||||
Name string
|
||||
DefaultBranch string
|
||||
RepoRoot string
|
||||
Placement string
|
||||
WorktreeStrategy string
|
||||
CIWorkflows []string
|
||||
DeploymentRef string
|
||||
VersionTagPolicy string
|
||||
IssueIntegration string
|
||||
Rationale []string
|
||||
}
|
||||
|
||||
// BindRepo — record repo IDENTITY + placement decision. NEVER credentials:
|
||||
// the repo_identity schema rejects secret-named fields. Placement must be
|
||||
// one of RepoPlacements (exact Python ValueError text).
|
||||
func BindRepo(doc map[string]interface{}, b RepoBinding, nowStr string) (map[string]interface{}, error) {
|
||||
placement := b.Placement
|
||||
if placement == "" {
|
||||
placement = "EXISTING_PROJECT_REPO"
|
||||
}
|
||||
if !containsStr(RepoPlacements, placement) {
|
||||
return nil, fmt.Errorf("unknown repo placement: %s", placement)
|
||||
}
|
||||
worktreeStrategy := b.WorktreeStrategy
|
||||
if worktreeStrategy == "" {
|
||||
worktreeStrategy = "per-contract"
|
||||
}
|
||||
ciWorkflows := append([]string{}, b.CIWorkflows...)
|
||||
sort.Strings(ciWorkflows)
|
||||
rationale := append([]string{}, b.Rationale...)
|
||||
sort.Strings(rationale)
|
||||
|
||||
identities := map[string]interface{}{
|
||||
"forge": b.Forge,
|
||||
"owner": b.Owner,
|
||||
"name": b.Name,
|
||||
"full_name": b.Owner + "/" + b.Name,
|
||||
"default_branch": b.DefaultBranch,
|
||||
"repo_root": b.RepoRoot,
|
||||
"worktree_strategy": worktreeStrategy,
|
||||
"ci_workflows": ciWorkflows,
|
||||
"deployment_ref": b.DeploymentRef,
|
||||
"version_tag_policy": b.VersionTagPolicy,
|
||||
"issue_integration": b.IssueIntegration,
|
||||
"placement": placement,
|
||||
"rationale": rationale,
|
||||
}
|
||||
var bad []string
|
||||
for _, s := range RepoSecretFields {
|
||||
if _, isKey := identities[s]; isKey {
|
||||
bad = append(bad, s)
|
||||
}
|
||||
}
|
||||
if len(bad) > 0 {
|
||||
sort.Strings(bad)
|
||||
parts := make([]string, len(bad))
|
||||
for i, s := range bad {
|
||||
parts[i] = "'" + s + "'"
|
||||
}
|
||||
return nil, fmt.Errorf("repo binding rejects secret-named fields: {%s}", strings.Join(parts, ", "))
|
||||
}
|
||||
e := ssAddEntity(doc, "repo:"+b.Owner+"/"+b.Name, "repo_identity", nil,
|
||||
identities, []string{"repo-binding"}, "DECLARED", nil, nowStr)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func containsStr(list []string, s string) bool {
|
||||
for _, v := range list {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ObserveRepo — record a repo STATE observation (HEAD, dirty flag,
|
||||
// verdict). The bound repo_identity entity is mutated in place; a missing
|
||||
// binding raises the exact Python ValueError.
|
||||
func ObserveRepo(doc map[string]interface{}, owner, name, head string, clean *bool, lastVerification, nowStr string) (map[string]interface{}, error) {
|
||||
MutationMu.Lock()
|
||||
defer MutationMu.Unlock()
|
||||
rid := "repo:" + owner + "/" + name
|
||||
e := EntityWithAliases(doc, rid)
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("bind_repo first: %s not found", rid)
|
||||
}
|
||||
var cleanVal interface{}
|
||||
if clean != nil {
|
||||
cleanVal = *clean
|
||||
}
|
||||
hiddenHead := head
|
||||
if len(hiddenHead) > 12 {
|
||||
hiddenHead = hiddenHead[:12]
|
||||
}
|
||||
obs := map[string]interface{}{
|
||||
"head": hiddenHead,
|
||||
"clean": cleanVal,
|
||||
"last_verification": lastVerification,
|
||||
}
|
||||
ext, _ := e["ext"].(map[string]interface{})
|
||||
if ext == nil {
|
||||
ext = map[string]interface{}{}
|
||||
e["ext"] = ext
|
||||
}
|
||||
ext["observed_state"] = obs
|
||||
e["observed_at"] = nowStr
|
||||
appendToDocList(doc, "observations", map[string]interface{}{
|
||||
"kind": "repo_observed",
|
||||
"repo": rid,
|
||||
"head": hiddenHead,
|
||||
"clean": cleanVal,
|
||||
"observed_at": nowStr,
|
||||
})
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// ── git observation (IO seam) ────────────────────────────────────────────
|
||||
|
||||
// GitRunner — the injectable seam for the oracle's
|
||||
// subprocess.run(["git", "-C", root, ...], capture_output=True, text=True,
|
||||
// timeout=15) calls. Returns (stdout, returncode, err); err non-nil is the
|
||||
// OSError/timeout branch — RepoStateFromGit degrades gracefully (stale
|
||||
// flag governs). The DEFAULT runner shells out for real with a 15s cap.
|
||||
var GitRunner = func(args []string, timeoutSec int) (string, int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
out, err := cmd.Output()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", 0, ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return string(out), ee.ExitCode(), nil
|
||||
}
|
||||
return string(out), 0, err
|
||||
}
|
||||
return string(out), 0, nil
|
||||
}
|
||||
|
||||
// RepoStateFromGit — refresh the repo observation from the actual git
|
||||
// working tree (deterministic; reality wins). Degraded observation
|
||||
// (unspawnable git, timeout) leaves clean as JSON null exactly like the
|
||||
// oracle's unassigned variable.
|
||||
func RepoStateFromGit(doc map[string]interface{}, owner, name, repoRoot, nowStr string) (map[string]interface{}, error) {
|
||||
head := ""
|
||||
var clean *bool
|
||||
if out, _, err := GitRunner(
|
||||
[]string{"git", "-C", repoRoot, "rev-parse", "HEAD"}, 15); err == nil {
|
||||
head = strings.TrimSpace(out)
|
||||
if stOut, stCode, err2 := GitRunner(
|
||||
[]string{"git", "-C", repoRoot, "status", "--short"}, 15); err2 == nil {
|
||||
c := stCode == 0 && strings.TrimSpace(stOut) == ""
|
||||
clean = &c
|
||||
}
|
||||
}
|
||||
return ObserveRepo(doc, owner, name, head, clean, "", nowStr)
|
||||
}
|
||||
|
||||
// ── reconciliation ───────────────────────────────────────────────────────
|
||||
|
||||
// ReconcileAuthorities — structural_store.reconcile_authorities: facts owned
|
||||
// by an authority that did not re-observe them become STALE. Manual facts
|
||||
// (no authority) untouched.
|
||||
func ReconcileAuthorities(doc map[string]interface{}) []map[string]interface{} {
|
||||
authorities, _ := doc["authority_generations"].(map[string]interface{})
|
||||
if authorities == nil {
|
||||
authorities = map[string]interface{}{}
|
||||
doc["authority_generations"] = authorities
|
||||
}
|
||||
staled := []map[string]interface{}{}
|
||||
rels, _ := doc["relationships"].([]interface{})
|
||||
for _, raw := range rels {
|
||||
rel, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
auth, _ := rel["authority"].(string)
|
||||
status, _ := rel["status"].(string)
|
||||
if auth == "" || status == "STALE" {
|
||||
continue
|
||||
}
|
||||
rt, _ := rel["type"].(string)
|
||||
from, _ := rel["from"].(string)
|
||||
to, _ := rel["to"].(string)
|
||||
oid := fmt.Sprintf("%s|%s|%s", rt, from, to)
|
||||
aEntry, _ := authorities[auth].(map[string]interface{})
|
||||
observed, _ := aEntry["observed"].([]interface{})
|
||||
covered := false
|
||||
for _, o := range observed {
|
||||
if s, isStr := o.(string); isStr && s == oid {
|
||||
covered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !covered {
|
||||
rel["status"] = "STALE"
|
||||
staled = append(staled, map[string]interface{}{
|
||||
"type": rt, "from": from, "to": to, "authority": auth,
|
||||
})
|
||||
}
|
||||
}
|
||||
return staled
|
||||
}
|
||||
|
||||
// Reconcile — Scaffold.reconcile: the authority-owned staleness pass +
|
||||
// fact_staled observations. Reality wins — stale facts are marked, never
|
||||
// force-reverted.
|
||||
func Reconcile(doc map[string]interface{}, nowStr string) map[string]interface{} {
|
||||
MutationMu.Lock()
|
||||
defer MutationMu.Unlock()
|
||||
staled := ReconcileAuthorities(doc)
|
||||
for _, s := range staled {
|
||||
obs := map[string]interface{}{"kind": "fact_staled", "observed_at": nowStr}
|
||||
for k, v := range s {
|
||||
obs[k] = v
|
||||
}
|
||||
appendToDocList(doc, "observations", obs)
|
||||
}
|
||||
return map[string]interface{}{"staled": interfaceSliceOfMaps(staled)}
|
||||
}
|
||||
|
||||
func interfaceSliceOfMaps(in []map[string]interface{}) []interface{} {
|
||||
out := make([]interface{}, len(in))
|
||||
for i, m := range in {
|
||||
out[i] = m
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
// scaffold — Go scaffolding interface (R2 W5c).
|
||||
// Replaces the orchestration layer of src/rune/scaffold.py.
|
||||
// Uses the Survivor scaffold package to call the Rust scaffold_core.
|
||||
// Dichotomy wave (contract BUILDER-2026-10-13T10:42:56Z-179836-factory-
|
||||
// gap-sc): the legacy R2-W5c commands (classify/status/validate_transition)
|
||||
// keep their dispatch; EVERYTHING ELSE routes through the Python-parity
|
||||
// operator surface (runOperator — scaffold.py main() semantics, including
|
||||
// its unknown-command arm).
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -12,11 +17,10 @@ import (
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: scaffold <classify|status> <text>")
|
||||
fmt.Fprintln(opStderr, "usage: scaffold <classify|status|validate_transition|project-init|project-show|contract-new|...>")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := os.Args[1]
|
||||
switch cmd {
|
||||
switch cmd := os.Args[1]; cmd {
|
||||
case "classify":
|
||||
text := ""
|
||||
for _, a := range os.Args[2:] {
|
||||
@@ -54,7 +58,7 @@ func main() {
|
||||
// 9-phase store machine's transition validator (JSON on stdin).
|
||||
var req map[string]interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "bad JSON input:", err)
|
||||
fmt.Fprintln(opStderr, "bad JSON input:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
c, _ := req["current"].(string)
|
||||
@@ -66,7 +70,6 @@ func main() {
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
os.Exit(runOperator(os.Args[1:]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
package main
|
||||
|
||||
// operator.go — #2479 scaffold dichotomy wave: the operator surface of
|
||||
// scaffold.py main() (same convention as rune-spine: plain argv, JSON from
|
||||
// the actual store, never reconstructed from prose). Commands:
|
||||
// project-init/project-show/contract-new/contract-approve/contract-phase/
|
||||
// contract-show/contract-verify-scope/contract-list. CONVENTION: store_dir
|
||||
// and project_id are ALWAYS the last two positional args, so
|
||||
// command-specific arguments never collide with the store location. The
|
||||
// nested helpers _scaffold_from_args/_cmd_args are named
|
||||
// scaffoldFromArgs/cmdArgs.
|
||||
//
|
||||
// Output bytes mirror the oracle: compact prints carry Python dict
|
||||
// insertion order (pyDumps); indent=1 prints come from the loaded store,
|
||||
// whose on-disk form is json.dumps(sort_keys=True) — so sorted-key
|
||||
// emission IS the loaded insertion order. Documented divergences: `-h`
|
||||
// prints a short usage line (not the Python module docstring); Python
|
||||
// tracebacks on unexpected errors are a plain-stderr message + exit 1;
|
||||
// project-init's redundant second save in the oracle is not reproduced
|
||||
// (init already persists — bytes identical).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// opStdout/opStderr — the print seam (tests capture; the binary uses the
|
||||
// process streams).
|
||||
var (
|
||||
opStdout io.Writer = os.Stdout
|
||||
opStderr io.Writer = os.Stderr
|
||||
)
|
||||
|
||||
// Scaffold — the python-class handle: store_dir + project_id, with the
|
||||
// derived current/snapshots paths resolved at construction.
|
||||
type Scaffold struct {
|
||||
StoreDir string
|
||||
ProjectID string
|
||||
Current string
|
||||
SnapshotsDir string
|
||||
}
|
||||
|
||||
// NewScaffold — Scaffold(store_dir, project_id): current =
|
||||
// store_dir/current.json, snapshots_dir = store_dir/snapshots.
|
||||
func NewScaffold(storeDir, projectID string) *Scaffold {
|
||||
return &Scaffold{
|
||||
StoreDir: storeDir,
|
||||
ProjectID: projectID,
|
||||
Current: filepath.Join(storeDir, "current.json"),
|
||||
SnapshotsDir: filepath.Join(storeDir, "snapshots"),
|
||||
}
|
||||
}
|
||||
|
||||
// scaffoldFromArgs — _scaffold_from_args: store_dir and project_id are
|
||||
// ALWAYS the last two positional args (store_dir = args[-2] when two or
|
||||
// more args exist else ".rune/scaffold"; project_id = args[-1] when ANY
|
||||
// arg exists else Path(store_dir).name — the oracle reads args[-1] on the
|
||||
// exact same rule).
|
||||
func scaffoldFromArgs(args []string) *Scaffold {
|
||||
storeDir := ".rune/scaffold"
|
||||
projectID := ""
|
||||
if len(args) >= 2 {
|
||||
storeDir = args[len(args)-2]
|
||||
}
|
||||
if len(args) >= 1 {
|
||||
projectID = args[len(args)-1]
|
||||
}
|
||||
if projectID == "" {
|
||||
projectID = filepath.Base(storeDir)
|
||||
}
|
||||
return NewScaffold(storeDir, projectID)
|
||||
}
|
||||
|
||||
// cmdArgs — _cmd_args: command-specific args = everything before the
|
||||
// final two positional slots (store_dir, project_id).
|
||||
func cmdArgs(args []string) []string {
|
||||
if len(args) >= 2 {
|
||||
return args[:len(args)-2]
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func nowStrDefault() string {
|
||||
return time.Now().UTC().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
|
||||
// ── Python-faithful JSON emission ────────────────────────────────────────
|
||||
|
||||
// kvPair / orderedMap — Python dict insertion order as a value type.
|
||||
type kvPair struct {
|
||||
k string
|
||||
v interface{}
|
||||
}
|
||||
type orderedMap []kvPair
|
||||
|
||||
// pyDumps — json.dumps({k0: v0, ...}) COMPACT with insertion order.
|
||||
func pyDumps(pairs ...kvPair) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("{")
|
||||
for i, p := range pairs {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pyQuote(p.k))
|
||||
sb.WriteString(": ")
|
||||
writePyJSON(&sb, p.v)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// pyIndentJSON — json.dumps(v, indent=1). orderedMap emits its OWN order
|
||||
// (objects built in code); plain maps emit sorted keys (loaded store
|
||||
// objects — the on-disk form is sort_keys=True, so sorted == loaded
|
||||
// insertion order). Lists indent one space per level; empty containers
|
||||
// stay inline.
|
||||
func pyIndentJSON(v interface{}) string {
|
||||
var sb strings.Builder
|
||||
pyJSONIndent(&sb, v, "")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// absentReason — a "reason" key the scoped verdict does not carry.
|
||||
type absentReason struct{}
|
||||
|
||||
func isAbsent(v interface{}) bool { _, ok := v.(absentReason); return ok }
|
||||
|
||||
func pyJSONIndent(sb *strings.Builder, v interface{}, indent string) {
|
||||
next := indent + " "
|
||||
switch x := v.(type) {
|
||||
case orderedMap:
|
||||
emit := x[:0:0]
|
||||
for _, p := range x {
|
||||
if isAbsent(p.v) {
|
||||
continue
|
||||
}
|
||||
emit = append(emit, p)
|
||||
}
|
||||
if len(emit) == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
for i, p := range emit {
|
||||
if i > 0 {
|
||||
sb.WriteString(",\n")
|
||||
}
|
||||
sb.WriteString(next)
|
||||
sb.WriteString(pyQuote(p.k))
|
||||
sb.WriteString(": ")
|
||||
pyJSONIndent(sb, p.v, next)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString("}")
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
if len(keys) == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sb.WriteString(",\n")
|
||||
}
|
||||
sb.WriteString(next)
|
||||
sb.WriteString(pyQuote(k))
|
||||
sb.WriteString(": ")
|
||||
pyJSONIndent(sb, x[k], next)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString("}")
|
||||
case []interface{}:
|
||||
if len(x) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
sb.WriteString(",\n")
|
||||
}
|
||||
sb.WriteString(next)
|
||||
pyJSONIndent(sb, e, next)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
if len(x) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
sb.WriteString(",\n")
|
||||
}
|
||||
sb.WriteString(next)
|
||||
writePyJSON(sb, e)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString("]")
|
||||
default:
|
||||
writePyJSON(sb, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ── operator dispatch ────────────────────────────────────────────────────
|
||||
|
||||
func pyPrint(s string) { fmt.Fprintln(opStdout, s) }
|
||||
|
||||
func operatorStoreLoad(s *Scaffold) map[string]interface{} {
|
||||
if doc := LoadDoc(s.Current); doc != nil {
|
||||
return doc
|
||||
}
|
||||
return NewDoc(nowStrDefault())
|
||||
}
|
||||
|
||||
func countList(doc map[string]interface{}, key string) int {
|
||||
list, _ := doc[key].([]interface{})
|
||||
return len(list)
|
||||
}
|
||||
|
||||
func safeArg(args []string, i int) string {
|
||||
if i < len(args) {
|
||||
return args[i]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// runOperator — scaffold.main: dispatch ONE command, print JSON from the
|
||||
// actual store, return the process exit code.
|
||||
func runOperator(argv []string) int {
|
||||
if len(argv) == 0 || argv[0] == "-h" || argv[0] == "--help" {
|
||||
pyPrint("scaffold — project construction/structural truth (Go operator surface; parity oracle src/rune/scaffold.py)")
|
||||
return 0
|
||||
}
|
||||
cmd, args := argv[0], argv[1:]
|
||||
|
||||
// project-init consumes [store, pid, purpose] directly (the last two
|
||||
// remain store+pid; purpose is args[2] and handled locally).
|
||||
if cmd == "project-init" {
|
||||
purpose := ""
|
||||
if len(args) > 2 {
|
||||
purpose = args[2]
|
||||
}
|
||||
storeDir, projectID := ".rune/scaffold", "unnamed"
|
||||
if len(args) > 0 {
|
||||
storeDir = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
projectID = args[1]
|
||||
}
|
||||
s := NewScaffold(storeDir, projectID)
|
||||
if _, err := Init(s.Current, s.ProjectID, purpose, "", nowStrDefault()); err != nil {
|
||||
fmt.Fprintln(opStderr, err)
|
||||
return 1
|
||||
}
|
||||
pyPrint(pyDumps(
|
||||
kvPair{"project", "project:" + s.ProjectID},
|
||||
kvPair{"store", s.Current},
|
||||
))
|
||||
return 0
|
||||
}
|
||||
if cmd == "project-show" {
|
||||
s := scaffoldFromArgs(args)
|
||||
doc := operatorStoreLoad(s)
|
||||
p := EntityWithAliases(doc, "project:"+s.ProjectID)
|
||||
pyPrint(pyIndentJSON(orderedMap{
|
||||
{"scaffold", s.Current},
|
||||
{"project", p},
|
||||
{"bridge", spineBridgeOrdered(doc, s.ProjectID)},
|
||||
{"entities", countList(doc, "entities")},
|
||||
{"relationships", countList(doc, "relationships")},
|
||||
}))
|
||||
if p == nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
s := scaffoldFromArgs(args)
|
||||
doc := operatorStoreLoad(s)
|
||||
ca := cmdArgs(args)
|
||||
|
||||
switch cmd {
|
||||
case "contract-new":
|
||||
if len(ca) < 2 {
|
||||
fmt.Fprintln(opStderr, "contract-new requires: <contract_id> <need>")
|
||||
return 1
|
||||
}
|
||||
ce, err := NewContract(doc, ca[0], ca[1], "user", nil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(opStderr, err)
|
||||
return 1
|
||||
}
|
||||
if err := SaveDoc(doc, s.Current); err != nil {
|
||||
fmt.Fprintln(opStderr, err)
|
||||
return 1
|
||||
}
|
||||
e := EntityWithAliases(doc, "contract:"+ca[0])
|
||||
phase, status := ce.Phase, ce.Status
|
||||
if v, ok := e["phase"].(string); ok {
|
||||
phase = v
|
||||
}
|
||||
if v, ok := e["status"].(string); ok {
|
||||
status = v
|
||||
}
|
||||
pyPrint(pyDumps(
|
||||
kvPair{"contract", "contract:" + ca[0]},
|
||||
kvPair{"phase", phase},
|
||||
kvPair{"status", status},
|
||||
))
|
||||
return 0
|
||||
case "contract-approve":
|
||||
return operatorTransition(s, doc, ca, "approved")
|
||||
case "contract-phase":
|
||||
if len(ca) < 2 {
|
||||
fmt.Fprintln(opStderr, "contract-phase requires: <contract_id> <phase>")
|
||||
return 1
|
||||
}
|
||||
return operatorTransition(s, doc, ca, ca[1])
|
||||
case "contract-show":
|
||||
c := EntityWithAliases(doc, "contract:"+safeArg(ca, 0))
|
||||
if c == nil {
|
||||
pyPrint(pyDumps(kvPair{"error", "not found"}, kvPair{"contract", safeArg(ca, 0)}))
|
||||
return 1
|
||||
}
|
||||
pyPrint(pyIndentJSON(c))
|
||||
return 0
|
||||
case "contract-verify-scope":
|
||||
c := EntityWithAliases(doc, "contract:"+safeArg(ca, 0))
|
||||
if c == nil {
|
||||
fmt.Fprintf(opStderr, "contract not found: %s\n", safeArg(ca, 0))
|
||||
return 1
|
||||
}
|
||||
v := operatorCheckScope(c, ca[1:])
|
||||
pairs := []kvPair{
|
||||
{"verdict", v["verdict"]},
|
||||
{"in_scope", v["in_scope"]},
|
||||
{"out_of_scope", v["out_of_scope"]},
|
||||
{"reason", absentReason{}},
|
||||
}
|
||||
if reason, has := v["reason"]; has {
|
||||
pairs[3] = kvPair{"reason", reason}
|
||||
}
|
||||
pyPrint(pyIndentJSON(orderedMap(pairs)))
|
||||
if v["verdict"] == "green" {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
case "contract-list":
|
||||
rows := []interface{}{}
|
||||
ents, _ := doc["entities"].([]interface{})
|
||||
for _, raw := range ents {
|
||||
e, ok := raw.(map[string]interface{})
|
||||
if !ok || e["kind"] != "construction_contract" {
|
||||
continue
|
||||
}
|
||||
ident, _ := e["identities"].(map[string]interface{})
|
||||
nextAction, _ := ident["next_action"].(string)
|
||||
row := orderedMap{
|
||||
{"id", e["id"]},
|
||||
{"phase", orNull(e["phase"])},
|
||||
{"status", orNull(e["status"])},
|
||||
{"next_action", nextAction},
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
pyPrint(pyIndentJSON(rows))
|
||||
return 0
|
||||
}
|
||||
pyPrint(pyDumps(kvPair{"error", fmt.Sprintf("unknown command: %s", cmd)}))
|
||||
return 2
|
||||
}
|
||||
|
||||
func orNull(v interface{}) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// spineBridgeOrdered — the bridge object in Python's emission order.
|
||||
func spineBridgeOrdered(doc map[string]interface{}, projectID string) orderedMap {
|
||||
bridge := SpineBridgeEntity(doc, projectID)
|
||||
pairs := []kvPair{{"project", "project:" + projectID}}
|
||||
if len(bridge) > 0 {
|
||||
if v, has := bridge["purpose"]; has {
|
||||
pairs = append(pairs, kvPair{"purpose", v})
|
||||
}
|
||||
if v, has := bridge["repo_full_name"]; has {
|
||||
pairs = append(pairs, kvPair{"repo_full_name", v})
|
||||
}
|
||||
if v, has := bridge["repo_forge"]; has {
|
||||
pairs = append(pairs, kvPair{"repo_forge", v})
|
||||
}
|
||||
}
|
||||
return orderedMap(pairs)
|
||||
}
|
||||
|
||||
// operatorTransition — contract-approve/contract-phase shared body: the
|
||||
// oracle's transition_contract on the LOADED doc (map-level: phase/status
|
||||
// rules, checkpoint append, next_action, observed_at), then save + echo.
|
||||
func operatorTransition(s *Scaffold, doc map[string]interface{}, ca []string, phase string) int {
|
||||
if len(ca) < 1 {
|
||||
fmt.Fprintln(opStderr, "missing contract_id")
|
||||
return 1
|
||||
}
|
||||
e := EntityWithAliases(doc, "contract:"+ca[0])
|
||||
if e == nil {
|
||||
fmt.Fprintf(opStderr, "contract not found: %s\n", ca[0])
|
||||
return 1
|
||||
}
|
||||
current, _ := e["phase"].(string)
|
||||
if current == "" {
|
||||
current = "draft"
|
||||
}
|
||||
if err := ValidateTransition(current, phase); err != nil {
|
||||
fmt.Fprintln(opStderr, err)
|
||||
return 1
|
||||
}
|
||||
nowStr := nowStrDefault()
|
||||
e["phase"] = phase
|
||||
if phase == "closed" {
|
||||
e["status"] = "closed"
|
||||
} else if phase == "failed" {
|
||||
e["status"] = "failed"
|
||||
}
|
||||
cps, _ := e["checkpoints"].([]interface{})
|
||||
e["checkpoints"] = append(cps, map[string]interface{}{
|
||||
"phase": phase, "at": nowStr, "evidence": "", "note": "",
|
||||
})
|
||||
ident, _ := e["identities"].(map[string]interface{})
|
||||
if ident == nil {
|
||||
ident = map[string]interface{}{}
|
||||
e["identities"] = ident
|
||||
}
|
||||
ident["next_action"] = NextAction[phase]
|
||||
e["observed_at"] = nowStr
|
||||
if err := SaveDoc(doc, s.Current); err != nil {
|
||||
fmt.Fprintln(opStderr, err)
|
||||
return 1
|
||||
}
|
||||
pyPrint(pyDumps(kvPair{"contract", ca[0]}, kvPair{"phase", phase}))
|
||||
return 0
|
||||
}
|
||||
|
||||
// operatorCheckScope — CheckScope over the raw parsed entity (scope comes
|
||||
// from identities).
|
||||
func operatorCheckScope(e map[string]interface{}, changedFiles []string) map[string]interface{} {
|
||||
ident, _ := e["identities"].(map[string]interface{})
|
||||
ce := &ContractEntity{Identities: ident}
|
||||
if ident == nil {
|
||||
ce.Identities = map[string]interface{}{}
|
||||
}
|
||||
if v, has := e["phase"].(string); has {
|
||||
ce.Phase = v
|
||||
}
|
||||
return CheckScope(ce, changedFiles)
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package main
|
||||
|
||||
// parity_live_test.go — LIVE parity sweep (dichotomy wave, contract
|
||||
// BUILDER-2026-10-13T10:42:56Z-179836-factory-gap-sc): the gap closing
|
||||
// truth cases are replayed against the LIVE python oracle (driver over
|
||||
// scripts/scaffold_oracle.py, clock pinned from the envelope) AND the Go
|
||||
// port in this process; per-case canonical equality is the 100% gate. The
|
||||
// scripted-git battery covers the seam; the oracle's REAL subprocess path
|
||||
// is exercised against a real git repo here (both engines, same tree).
|
||||
// Operator CLI: built binary vs `python3 src/rune/scaffold.py`, identical
|
||||
// argv, sibling scratch cwd's per engine, timestamp-bearing output
|
||||
// normalized to <TS> on both sides.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func repoRootDir(t *testing.T) string {
|
||||
dir, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
t.Fatalf("abs: %v", err)
|
||||
}
|
||||
return filepath.Clean(filepath.Join(dir, "..", "..", ".."))
|
||||
}
|
||||
|
||||
func oraclePyPath(t *testing.T) string {
|
||||
p := filepath.Join(repoRootDir(t), "src", "rune", "scaffold.py")
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
t.Skipf("python oracle unavailable: %v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// liveDriver — runs the TRUTH cases through the LIVE oracle in one
|
||||
// process (scratch cwd: relative store paths stay machine-free). The
|
||||
// driver pins RUNE_FROZEN_NOW from the envelope BEFORE importing the
|
||||
// wrapper — identical to the shared harness's --verify run.
|
||||
func liveDriver(t *testing.T, repo, casesFile, outDir string) (string, error) {
|
||||
t.Helper()
|
||||
driver := `import json, os, sys
|
||||
repo, cases_file = sys.argv[1], sys.argv[2]
|
||||
raw = json.load(open(cases_file))
|
||||
os.environ["RUNE_FROZEN_NOW"] = raw["frozen_now"]
|
||||
os.environ["RUNE_STRUCTURAL_CORE"] = "python"
|
||||
sys.path.insert(0, repo + "/scripts")
|
||||
sys.path.insert(0, repo + "/src")
|
||||
from freeze_truth import freeze
|
||||
import scaffold_oracle
|
||||
cases = [{"fn": c["fn"], "args": c.get("args", []), "kwargs": c.get("kwargs", {})} for c in raw["cases"]]
|
||||
truth = freeze("rune.scaffold", cases, oracle_module="scaffold_oracle")
|
||||
print(json.dumps({"frozen_now": raw["frozen_now"], "cases": truth}))
|
||||
`
|
||||
dpath := filepath.Join(outDir, "driver.py")
|
||||
if err := os.WriteFile(dpath, []byte(driver), 0o644); err != nil {
|
||||
t.Fatalf("driver write: %v", err)
|
||||
}
|
||||
cmd := exec.Command("python3", dpath, repo, casesFile)
|
||||
cmd.Dir = outDir
|
||||
var stdout, stderr strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
return stdout.String(), wrapDriverErr(err, stderr.String())
|
||||
}
|
||||
|
||||
func wrapDriverErr(err error, stderr string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmtErrorf("driver failed: %v\n%s", err, stderr)
|
||||
}
|
||||
|
||||
func fmtErrorf(format string, args ...interface{}) error {
|
||||
return &staticError{format: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
type staticError struct{ format string }
|
||||
|
||||
func (e *staticError) Error() string { return e.format }
|
||||
|
||||
// TestGapsLiveParity — every truth case, LIVE: the oracle (fresh process)
|
||||
// vs the Go port (this process), canonical-JSON equal per case.
|
||||
func TestGapsLiveParity(t *testing.T) {
|
||||
oraclePyPath(t)
|
||||
if _, err := exec.LookPath("python3"); err != nil {
|
||||
t.Skipf("python3 unavailable: %v", err)
|
||||
}
|
||||
var env gapsEnvelope
|
||||
if err := json.Unmarshal(gapsTruth, &env); err != nil {
|
||||
t.Fatalf("unparseable truth: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
py := filepath.Join(dir, "py")
|
||||
if err := os.MkdirAll(py, 0o755); err != nil {
|
||||
t.Fatalf("scratch: %v", err)
|
||||
}
|
||||
truthFile := filepath.Join(dir, "truth_gaps.json")
|
||||
if err := os.WriteFile(truthFile, gapsTruth, 0o644); err != nil {
|
||||
t.Fatalf("truth copy: %v", err)
|
||||
}
|
||||
pyOut, driverErr := liveDriver(t, repoRootDir(t), truthFile, py)
|
||||
if driverErr != nil {
|
||||
t.Fatalf("live oracle replay failed: %v", driverErr)
|
||||
}
|
||||
|
||||
var pyTruth gapsEnvelope
|
||||
if err := json.Unmarshal([]byte(pyOut), &pyTruth); err != nil {
|
||||
t.Fatalf("driver output unparseable: %v (%s)", err, pyOut[:200])
|
||||
}
|
||||
if len(pyTruth.Cases) != len(env.Cases) {
|
||||
t.Fatalf("live oracle produced %d cases, want %d", len(pyTruth.Cases), len(env.Cases))
|
||||
}
|
||||
|
||||
scratch := t.TempDir()
|
||||
for i, c := range env.Cases {
|
||||
got := runGapsCase(t, env.FrozenNow, scratch, c)
|
||||
if err := canonGapsEq(pyTruth.Cases[i].Expected, got); err != "" {
|
||||
t.Fatalf("case[%d] %s LIVE divergence: %s", i, c.Fn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepoStateFromGitLiveAgainstRealRepo — the oracle shells out to REAL
|
||||
// git; the Go default GitRunner does the same; one real temp repo, both
|
||||
// engines, same work tree.
|
||||
func TestRepoStateFromGitLiveAgainstRealRepo(t *testing.T) {
|
||||
oraclePyPath(t)
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skipf("git: %v", err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
repo := filepath.Join(dir, "real-repo")
|
||||
if err := os.MkdirAll(repo, 0o755); err != nil {
|
||||
t.Fatalf("repo: %v", err)
|
||||
}
|
||||
gitCmd := func(args ...string) {
|
||||
c := exec.Command("git", args...)
|
||||
c.Dir = repo
|
||||
c.Env = append(os.Environ(),
|
||||
"GIT_AUTHOR_NAME=parity", "GIT_AUTHOR_EMAIL=parity@example.invalid",
|
||||
"GIT_COMMITTER_NAME=parity", "GIT_COMMITTER_EMAIL=parity@example.invalid",
|
||||
"GIT_AUTHOR_DATE=2026-10-13T10:50:00Z", "GIT_COMMITTER_DATE=2026-10-13T10:50:00Z")
|
||||
if out, err := c.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
gitCmd("init", "-q")
|
||||
if err := os.WriteFile(filepath.Join(repo, "f.txt"), []byte("parity\n"), 0o644); err != nil {
|
||||
t.Fatalf("file: %v", err)
|
||||
}
|
||||
gitCmd("add", "f.txt")
|
||||
gitCmd("commit", "-q", "-m", "parity")
|
||||
|
||||
pySrc := fmt.Sprintf("%q", filepath.Join(repoRootDir(t), "src"))
|
||||
repoArg := fmt.Sprintf("%q", repo)
|
||||
pyScript := "import json, sys\n" +
|
||||
"sys.path.insert(0, " + pySrc + ")\n" +
|
||||
"from rune import scaffold as sc\n" +
|
||||
"s = sc.Scaffold('.', 'p')\n" +
|
||||
"doc = s.init(purpose='live git')\n" +
|
||||
"s.bind_repo(doc, forge='g', owner='o', name='r', default_branch='m', repo_root='/')\n" +
|
||||
"e = s.repo_state_from_git(doc, 'o', 'r', " + repoArg + ")\n" +
|
||||
"obs = e['ext']['observed_state']\n" +
|
||||
"print(json.dumps({'head': obs['head'], 'clean': obs['clean']}))\n"
|
||||
|
||||
pyOut, pyErr := liveScript(t, dir, "driver_repo_state.py", pyScript)
|
||||
if pyErr != nil {
|
||||
t.Fatalf("python driver: %v (%s)", pyErr, pyOut)
|
||||
}
|
||||
var pyResult struct {
|
||||
Head string `json:"head"`
|
||||
Clean *bool `json:"clean"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(firstJSON(pyOut))), &pyResult); err != nil {
|
||||
t.Fatalf("python observation parse: %v (%s)", err, pyOut)
|
||||
}
|
||||
|
||||
doc := freshGapDoc()
|
||||
if _, err := BindRepo(doc, RepoBinding{
|
||||
Forge: "g", Owner: "o", Name: "r", DefaultBranch: "m",
|
||||
}, "2026-10-13T10:50:00Z"); err != nil {
|
||||
t.Fatalf("bind: %v", err)
|
||||
}
|
||||
e, err := RepoStateFromGit(doc, "o", "r", repo, "2026-10-13T10:50:00Z")
|
||||
if err != nil {
|
||||
t.Fatalf("go observe: %v", err)
|
||||
}
|
||||
ext, _ := e["ext"].(map[string]interface{})
|
||||
obs, _ := ext["observed_state"].(map[string]interface{})
|
||||
goHead, _ := obs["head"].(string)
|
||||
goClean, goCleanOK := obs["clean"].(bool)
|
||||
if pyResult.Head != goHead {
|
||||
t.Fatalf("head divergence: python %q vs go %q (same repo, same commit)", pyResult.Head, goHead)
|
||||
}
|
||||
if !goCleanOK || pyResult.Clean == nil || *pyResult.Clean != goClean {
|
||||
t.Fatalf("clean divergence: python %v vs go %v", pyResult.Clean, obs["clean"])
|
||||
}
|
||||
}
|
||||
|
||||
func firstJSON(s string) string {
|
||||
if i := strings.Index(s, "\n"); i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func liveScript(t *testing.T, dir, name, script string) (string, error) {
|
||||
t.Helper()
|
||||
p := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(p, []byte(script), 0o644); err != nil {
|
||||
t.Fatalf("script write: %v", err)
|
||||
}
|
||||
cmd := exec.Command("python3", p)
|
||||
cmd.Dir = dir
|
||||
var stdout, stderr strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
return stdout.String(), wrapDriverErr(err, stderr.String())
|
||||
}
|
||||
|
||||
// normalizeTimestamps — live CLI sweep clock normalization (the engines
|
||||
// observe the world at different real seconds).
|
||||
func normalizeTimestamps(s string) string {
|
||||
var out strings.Builder
|
||||
for i := 0; i < len(s); {
|
||||
if n, ok := matchISOStamp(s[i:]); ok {
|
||||
out.WriteString("<TS>")
|
||||
i += n
|
||||
continue
|
||||
}
|
||||
out.WriteByte(s[i])
|
||||
i++
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func matchISOStamp(s string) (int, bool) {
|
||||
const n = 20 // "2026-10-13T10:50:00Z"
|
||||
if len(s) < n {
|
||||
return 0, false
|
||||
}
|
||||
t := s[:n]
|
||||
if t[4] != '-' || t[7] != '-' || t[10] != 'T' || t[13] != ':' || t[16] != ':' || t[19] != 'Z' {
|
||||
return 0, false
|
||||
}
|
||||
for _, i := range []int{0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18} {
|
||||
if t[i] < '0' || t[i] > '9' {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
// TestOperatorCLILiveParity — `python3 src/rune/scaffold.py` vs the built
|
||||
// Go binary: identical argv, ONE scratch cwd per engine, byte equality of
|
||||
// timestamp-free prints, <TS>-normalized equality of the timestamp-bearing
|
||||
// ones, exit codes equal.
|
||||
func TestOperatorCLILiveParity(t *testing.T) {
|
||||
oraclePyPath(t)
|
||||
if _, err := exec.LookPath("python3"); err != nil {
|
||||
t.Skipf("python3 unavailable: %v", err)
|
||||
}
|
||||
root := repoRootDir(t)
|
||||
bin := filepath.Join(t.TempDir(), "scaffold-bin")
|
||||
build := exec.Command("go", "build", "-o", bin, ".")
|
||||
build.Dir = filepath.Join(root, "src", "go", "scaffold")
|
||||
if out, err := build.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go build: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
for _, d := range []string{filepath.Join(dir, "py"), filepath.Join(dir, "go")} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
}
|
||||
pyDir, goDir := filepath.Join(dir, "py"), filepath.Join(dir, "go")
|
||||
|
||||
steps := [][]string{
|
||||
{"project-init", "sc-store", "p1", "parity purpose"},
|
||||
{"project-init", "sc-store", "p1", "parity purpose"},
|
||||
{"contract-new", "c1", "live need", "sc-store", "p1"},
|
||||
{"contract-approve", "c1", "sc-store", "p1"},
|
||||
{"contract-phase", "c1", "building", "sc-store", "p1"},
|
||||
{"contract-phase", "c1", "verifying", "sc-store", "p1"},
|
||||
{"contract-verify-scope", "c1", "src/rogue.py", "sc-store", "p1"},
|
||||
{"contract-show", "ghost", "sc-store", "p1"},
|
||||
{"contract-list", "sc-store", "p1"},
|
||||
{"definitely-not-a-command", "sc-store", "p1"},
|
||||
{"project-show", "sc-store", "p1"},
|
||||
{"contract-show", "c1", "sc-store", "p1"},
|
||||
}
|
||||
for _, step := range steps {
|
||||
// The oracle module has no __main__ block (running the .py as a
|
||||
// script is a no-op) — main() runs through -c; main() reads
|
||||
// sys.argv[1:], which python sets from the trailing args.
|
||||
pyEnv := []string{"PYTHONPATH=" + filepath.Join(root, "src")}
|
||||
pyArgv := append([]string{"-c",
|
||||
"import sys, os; sys.path.insert(0, os.environ['PYTHONPATH']); from rune import scaffold as m; sys.exit(m.main())"},
|
||||
step...)
|
||||
|
||||
runSide := func(name string, argv []string, cwd string, env []string) (string, string, int) {
|
||||
c := exec.Command(name, argv...)
|
||||
c.Dir = cwd
|
||||
c.Env = append(env, os.Environ()...)
|
||||
var so, se strings.Builder
|
||||
c.Stdout = &so
|
||||
c.Stderr = &se
|
||||
code := 0
|
||||
if err := c.Run(); err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
code = ee.ExitCode()
|
||||
} else {
|
||||
t.Fatalf("run %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
return so.String(), se.String(), code
|
||||
}
|
||||
pyOut, pyErr, pyCode := runSide("python3", pyArgv, pyDir, pyEnv)
|
||||
goOut, goErr, goCode := runSide(bin, step, goDir, nil)
|
||||
if pyCode != goCode {
|
||||
t.Fatalf("%v: exit codes: python %d vs go %d (stderr: %q | %q)",
|
||||
step, pyCode, goCode, pyErr, goErr)
|
||||
}
|
||||
pyN, goN := normalizeTimestamps(pyOut), normalizeTimestamps(goOut)
|
||||
if pyN != goN {
|
||||
t.Fatalf("%v: stdout divergence:\npython %q\ngo %q", step, pyN, goN)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,8 +111,9 @@ func phaseKnown(p string) bool {
|
||||
type ContractEntity struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Identities map[string]interface{} `json:"identities"`
|
||||
Maturity string `json:"maturity"`
|
||||
Tags []string `json:"tags"`
|
||||
Identities map[string]interface{} `json:"identities"`
|
||||
EvidenceRefs []string `json:"evidence_refs"`
|
||||
Phase string `json:"phase"`
|
||||
Status string `json:"status"`
|
||||
@@ -125,6 +126,8 @@ type ContractEntity struct {
|
||||
// empty checkpoints, evidence ref. Raises via error when the contract
|
||||
// already exists (Python ValueError "contract already exists: {id}").
|
||||
func NewContract(doc map[string]interface{}, contractID, need, origin string, identities map[string]interface{}) (*ContractEntity, error) {
|
||||
MutationMu.Lock()
|
||||
defer MutationMu.Unlock()
|
||||
key := "contract:" + contractID
|
||||
if existing, ok := doc["entities"].([]interface{}); ok {
|
||||
for _, raw := range existing {
|
||||
@@ -155,15 +158,21 @@ func NewContract(doc map[string]interface{}, contractID, need, origin string, id
|
||||
setDefault(identities, "acceptance", []string{})
|
||||
setDefault(identities, "non_goals", []string{})
|
||||
setDefault(identities, "next_action", NextAction["draft"])
|
||||
// PARITY (dichotomy wave repair): the oracle's new_contract goes
|
||||
// through add_entity — tags [] and observed_at stamped (the batch-1
|
||||
// struct omitted them; the operator surface's contract-show exposes
|
||||
// both). Clock = the NowStr frozen binding.
|
||||
e := &ContractEntity{
|
||||
ID: key,
|
||||
ID: "contract:" + contractID,
|
||||
Kind: "construction_contract",
|
||||
Identities: identities,
|
||||
Maturity: "DECLARED",
|
||||
Tags: []string{},
|
||||
Identities: identities,
|
||||
EvidenceRefs: []string{"contract-created:" + contractID},
|
||||
Phase: "draft",
|
||||
Status: "open",
|
||||
Checkpoints: []map[string]interface{}{},
|
||||
ObservedAt: NowStr,
|
||||
}
|
||||
// PARITY: Python new_contract calls add_entity — the doc MUTATES.
|
||||
if ents, ok := doc["entities"].([]interface{}); ok {
|
||||
@@ -185,6 +194,8 @@ func setDefault(m map[string]interface{}, k string, v interface{}) {
|
||||
// append the checkpoint {phase, at, evidence, note[:200]}, update
|
||||
// next_action, stamp observed_at.
|
||||
func TransitionContract(e *ContractEntity, phase, evidenceRef, note, nowStr string) error {
|
||||
MutationMu.Lock()
|
||||
defer MutationMu.Unlock()
|
||||
current := e.Phase
|
||||
if current == "" {
|
||||
current = "draft"
|
||||
|
||||
@@ -179,7 +179,16 @@ func CanonicalDigest(doc map[string]interface{}) string {
|
||||
// SaveDoc — structural_store.save: schema guard + sorted/redacted write.
|
||||
func SaveDoc(doc map[string]interface{}, path string) error {
|
||||
if v, has := doc["schema_version"]; has {
|
||||
if iv, ok := v.(float64); !ok || int(iv) != SchemaVersion {
|
||||
switch iv := v.(type) {
|
||||
case float64:
|
||||
if int(iv) != SchemaVersion {
|
||||
return fmt.Errorf("schema_version %v != %d", v, SchemaVersion)
|
||||
}
|
||||
case int:
|
||||
if iv != SchemaVersion {
|
||||
return fmt.Errorf("schema_version %v != %d", v, SchemaVersion)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("schema_version %v != %d", v, SchemaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestBuildersParity(t *testing.T) {
|
||||
AddComponent(doc, "c1", "p", "desc c1")
|
||||
AddContract(doc, "k1", "desc k1")
|
||||
Link(doc, "contains", "domain:d1", "component:c1")
|
||||
SetPRD(doc, "testproj", "docs/prd.md")
|
||||
SetPrd(doc, "testproj", "docs/prd.md", "")
|
||||
SetNextAction(doc, "testproj", "do the thing")
|
||||
AddLimitation(doc, "l1", "detail l1")
|
||||
ents, _ := doc["entities"].([]map[string]interface{})
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,297 @@
|
||||
package main
|
||||
|
||||
// truth_gaps_test.go — gap-closing parity gate (dichotomy wave, contract
|
||||
// BUILDER-2026-10-13T10:42:56Z-179836-factory-gap-sc): replays the SHARED
|
||||
// harness truth (scripts/gen_scaffold_gaps_truth.py via
|
||||
// scripts/freeze_truth.py, oracle pinned to python, clock pinned by the
|
||||
// envelope's frozen_now) VERBATIM, in order, against the ported functions.
|
||||
// The case sequence is self-seeding: the init case writes the store file
|
||||
// the re-init case reads. Frozen store/repo paths are RELATIVE
|
||||
// ("truth-gaps-store") — the replay repoints them into a per-test scratch
|
||||
// dir, exactly like the harness verify run in a scratch CWD:
|
||||
//
|
||||
// cd "$(mktemp -d)" && PYTHONPATH=src python3 scripts/freeze_truth.py \
|
||||
// --verify src/go/scaffold/truth_gaps.json --module scaffold_oracle
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed truth_gaps.json
|
||||
var gapsTruth []byte
|
||||
|
||||
type gapsEnvelope struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []gapsCase `json:"cases"`
|
||||
}
|
||||
|
||||
type gapsCase struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []interface{} `json:"args"`
|
||||
KwArgs map[string]interface{} `json:"kwargs"`
|
||||
Expected gapsResult `json:"expected"`
|
||||
}
|
||||
|
||||
// gapsResult — the scaffold_oracle.py capture shape: {"result", "doc",
|
||||
// "error"?}. The doc is compared wherever the capture wrapper returned it
|
||||
// (mutation visibility — the append/STALE/observation behavior of the
|
||||
// port must match the oracle's, not merely the returned entity).
|
||||
type gapsResult struct {
|
||||
Result interface{} `json:"result"`
|
||||
Doc interface{} `json:"doc,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func runGapsCase(t *testing.T, now string, storeDir string, c gapsCase) gapsResult {
|
||||
t.Helper()
|
||||
switch c.Fn {
|
||||
case "init_case": // args [store_dir, project_id]
|
||||
projectID := strArg(c.Args, 1)
|
||||
current := filepath.Join(storeDir, "truth-gaps-store", "current.json")
|
||||
doc, err := Init(current, projectID,
|
||||
strKw(c.KwArgs, "purpose"), strKw(c.KwArgs, "requirement"), now)
|
||||
if err != nil {
|
||||
t.Fatalf("init: %v", err)
|
||||
}
|
||||
return gapsResult{Result: doc}
|
||||
|
||||
case "set_prd_case": // args [doc, project_id]
|
||||
doc := docArg(c.Args, 0)
|
||||
e := SetPrd(doc, strArg(c.Args, 1), strKw(c.KwArgs, "prd_ref"), now)
|
||||
return gapsResult{Result: e, Doc: doc}
|
||||
|
||||
case "add_maintenance_item_case": // args [doc, name]
|
||||
doc := docArg(c.Args, 0)
|
||||
// kwargs ABSENT from a case = the ORACLE signature defaults
|
||||
// (approval=user, priority=normal, status=open); a provided ""
|
||||
// (impossible at the Python signature) would win in Go.
|
||||
e := AddMaintenanceItem(doc, strArg(c.Args, 1),
|
||||
strKw(c.KwArgs, "issue"), strKw(c.KwArgs, "evidence"),
|
||||
strKw(c.KwArgs, "benefit"),
|
||||
strKwDefault(c.KwArgs, "approval", "user"),
|
||||
strKwDefault(c.KwArgs, "priority", "normal"),
|
||||
strKwDefault(c.KwArgs, "status", "open"), now)
|
||||
return gapsResult{Result: e, Doc: doc}
|
||||
|
||||
case "bind_repo_case": // args [doc]
|
||||
doc := docArg(c.Args, 0)
|
||||
b := RepoBinding{
|
||||
Forge: strKw(c.KwArgs, "forge"),
|
||||
Owner: strKw(c.KwArgs, "owner"),
|
||||
Name: strKw(c.KwArgs, "name"),
|
||||
DefaultBranch: strKw(c.KwArgs, "default_branch"),
|
||||
RepoRoot: strKw(c.KwArgs, "repo_root"),
|
||||
Placement: strKw(c.KwArgs, "placement"),
|
||||
WorktreeStrategy: strKw(c.KwArgs, "worktree_strategy"),
|
||||
CIWorkflows: strListKw(c.KwArgs, "ci_workflows"),
|
||||
DeploymentRef: strKw(c.KwArgs, "deployment_ref"),
|
||||
VersionTagPolicy: strKw(c.KwArgs, "version_tag_policy"),
|
||||
IssueIntegration: strKw(c.KwArgs, "issue_integration"),
|
||||
Rationale: strListKw(c.KwArgs, "rationale"),
|
||||
}
|
||||
e, err := BindRepo(doc, b, now)
|
||||
if err != nil {
|
||||
return gapsResult{Result: nil, Doc: doc, Error: err.Error()}
|
||||
}
|
||||
return gapsResult{Result: e, Doc: doc}
|
||||
|
||||
case "observe_repo_case": // args [doc, owner, name]
|
||||
doc := docArg(c.Args, 0)
|
||||
var clean *bool
|
||||
if b, ok := c.KwArgs["clean"].(bool); ok {
|
||||
clean = &b
|
||||
}
|
||||
e, err := ObserveRepo(doc, strArg(c.Args, 1), strArg(c.Args, 2),
|
||||
strKw(c.KwArgs, "head"), clean, strKw(c.KwArgs, "last_verification"), now)
|
||||
if err != nil {
|
||||
return gapsResult{Result: nil, Doc: doc, Error: err.Error()}
|
||||
}
|
||||
return gapsResult{Result: e, Doc: doc}
|
||||
|
||||
case "repo_state_from_git_case": // args [doc, owner, name, repo_root, script]
|
||||
doc := docArg(c.Args, 0)
|
||||
script := scriptedGitRunner(c.Args[4])
|
||||
prev := GitRunner
|
||||
GitRunner = script
|
||||
defer func() { GitRunner = prev }()
|
||||
e, err := RepoStateFromGit(doc, strArg(c.Args, 1), strArg(c.Args, 2),
|
||||
strArg(c.Args, 3), now)
|
||||
if err != nil {
|
||||
return gapsResult{Result: nil, Doc: doc, Error: err.Error()}
|
||||
}
|
||||
return gapsResult{Result: e, Doc: doc}
|
||||
|
||||
case "reconcile_case": // args [doc]
|
||||
doc := docArg(c.Args, 0)
|
||||
out := Reconcile(doc, now)
|
||||
return gapsResult{Result: out, Doc: doc}
|
||||
}
|
||||
t.Fatalf("unknown truth fn: %s", c.Fn)
|
||||
return gapsResult{}
|
||||
}
|
||||
|
||||
// scriptedGitRunner — the Go mirror of the freeze harness's _FakeGit: the
|
||||
// oracle's subprocess.run(["git", "-C", root, ...]) calls replay the
|
||||
// scripted steps; "raise": "timeout"/"oserror" degrade exactly like the
|
||||
// oracle's caught branches.
|
||||
func scriptedGitRunner(spec interface{}) func([]string, int) (string, int, error) {
|
||||
steps, _ := spec.([]interface{})
|
||||
idx := 0
|
||||
return func(args []string, timeoutSec int) (string, int, error) {
|
||||
if idx >= len(steps) {
|
||||
return "", 0, errFakeGitExhausted
|
||||
}
|
||||
step, _ := steps[idx].(map[string]interface{})
|
||||
idx++
|
||||
switch step["raise"] {
|
||||
case "timeout":
|
||||
return "", 0, errFakeGitTimeout
|
||||
case "oserror":
|
||||
return "", 0, errFakeGitOS
|
||||
}
|
||||
out, _ := step["stdout"].(string)
|
||||
rc, _ := step["returncode"].(float64)
|
||||
return out, int(rc), nil
|
||||
}
|
||||
}
|
||||
|
||||
type errStatic string
|
||||
|
||||
func (e errStatic) Error() string { return string(e) }
|
||||
|
||||
var (
|
||||
errFakeGitTimeout = errStatic("fake git: timeout")
|
||||
errFakeGitOS = errStatic("fake git: unavailable")
|
||||
errFakeGitExhausted = errStatic("fake git: script exhausted")
|
||||
)
|
||||
|
||||
func strArg(args []interface{}, i int) string {
|
||||
if i < len(args) {
|
||||
if s, ok := args[i].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func strKw(kw map[string]interface{}, k string) string {
|
||||
if kw == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := kw[k].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// strKwDefault — the oracle's signature default for a kwarg the case
|
||||
// omitted (a provided value — including "" — wins).
|
||||
func strKwDefault(kw map[string]interface{}, k, def string) string {
|
||||
if kw == nil {
|
||||
return def
|
||||
}
|
||||
if v, ok := kw[k].(string); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func strListKw(kw map[string]interface{}, k string) []string {
|
||||
if kw == nil {
|
||||
return nil
|
||||
}
|
||||
raw, ok := kw[k].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, v := range raw {
|
||||
if s, ok := v.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func docArg(args []interface{}, i int) map[string]interface{} {
|
||||
if i < len(args) {
|
||||
if m, ok := args[i].(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGapsTruthParity(t *testing.T) {
|
||||
var env gapsEnvelope
|
||||
if err := json.Unmarshal(gapsTruth, &env); err != nil {
|
||||
t.Fatalf("unparseable truth: %v", err)
|
||||
}
|
||||
if env.FrozenNow == "" {
|
||||
t.Fatalf("envelope missing frozen_now (clock law)")
|
||||
}
|
||||
scratch := t.TempDir()
|
||||
for i, c := range env.Cases {
|
||||
if c.Fn == "init_case" {
|
||||
continue // covered by TestGapsInitSequence (store-file seeding)
|
||||
}
|
||||
got := runGapsCase(t, env.FrozenNow, scratch, c)
|
||||
if err := canonGapsEq(c.Expected, got); err != "" {
|
||||
t.Fatalf("case[%d] %s: %s", i, c.Fn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGapsInitSequence — the SELF-SEEDING init pair: case 1 writes the
|
||||
// store under the scratch dir, case 2 re-inits through the LOAD branch
|
||||
// (the merge semantics the update path of add_entity implements).
|
||||
func TestGapsInitSequence(t *testing.T) {
|
||||
var env gapsEnvelope
|
||||
if err := json.Unmarshal(gapsTruth, &env); err != nil {
|
||||
t.Fatalf("unparseable truth: %v", err)
|
||||
}
|
||||
scratch := t.TempDir()
|
||||
n := 0
|
||||
for i, c := range env.Cases {
|
||||
if c.Fn != "init_case" {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
got := runGapsCase(t, env.FrozenNow, scratch, c)
|
||||
if err := canonGapsEq(c.Expected, got); err != "" {
|
||||
t.Fatalf("init case[%d]: %s", i, err)
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("truth carries no init cases")
|
||||
}
|
||||
}
|
||||
|
||||
// canonGapsEq — canonical-JSON compare of the capture shape (Go-built
|
||||
// values vs the frozen oracle snapshot; equality is semantic).
|
||||
func canonGapsEq(want, got gapsResult) string {
|
||||
gotJSON, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
return "marshal got: " + err.Error()
|
||||
}
|
||||
var gotV, wantV interface{}
|
||||
if err := json.Unmarshal(gotJSON, &gotV); err != nil {
|
||||
return "reparse got: " + err.Error()
|
||||
}
|
||||
wantJSON, err := json.Marshal(want)
|
||||
if err != nil {
|
||||
return "marshal want: " + err.Error()
|
||||
}
|
||||
if err := json.Unmarshal(wantJSON, &wantV); err != nil {
|
||||
return "reparse want: " + err.Error()
|
||||
}
|
||||
gc, _ := json.Marshal(gotV)
|
||||
wc, _ := json.Marshal(wantV)
|
||||
if string(gc) != string(wc) {
|
||||
return "divergence:\n got " + string(gc) + "\n want " + string(wc)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user