feat(scaffold): close PARTIAL gaps — dichotomy wave

This commit is contained in:
Nick committed 2026-10-13 12:11:09 +01:00
1 parent 589b321d52
commit 5c4ba1cc65
16 files changed
+4076 -15

No files matched your search

+198
View File
@@ -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()
+149
View File
@@ -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}
+88
View File
@@ -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")
+25 -3
View File
@@ -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.
+2
View File
@@ -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 {
+235
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
module rune/scaffold
go 1.21
go 1.24
require rune/cognition v0.0.0
+486
View File
@@ -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
}
+9 -6
View File
@@ -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:]))
}
}
+474
View File
@@ -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)
}
+338
View File
@@ -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)
}
}
}
+14 -3
View File
@@ -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"
+10 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -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
+297
View File
@@ -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 ""
}