Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4aa87ca4 | ||
|
|
61432632f7 | ||
|
|
5601fee02a | ||
|
|
cdca988d00 | ||
|
|
d05a9e6741 | ||
|
|
ddb7490cc6 | ||
|
|
d12fbab06a | ||
|
|
19db782906 | ||
|
|
e0d7d7bb03 | ||
|
|
5dda590588 | ||
|
|
953197823a | ||
|
|
eba579f99f | ||
|
|
9fd3d8b15d | ||
|
|
5171cc3ef8 | ||
|
|
64f77e82ff | ||
|
|
d64df3d011 | ||
|
|
4d1064449b | ||
|
|
1e9ddea492 | ||
|
|
c5ecbacf98 | ||
|
|
892fb4cafe | ||
|
|
c2a25d187b | ||
|
|
9189e5083c | ||
|
|
4c78ac0cfe | ||
|
|
d6028ca928 | ||
|
|
3aeecf5142 | ||
|
|
723836e27a | ||
|
|
a7b13099aa | ||
|
|
c205c25598 | ||
|
|
a8feb3eee4 | ||
|
|
574902926e | ||
|
|
70389268a2 | ||
|
|
3199e36cd4 | ||
|
|
df6f52e652 | ||
|
|
bd2137fc4f | ||
|
|
060a2e65ec | ||
|
|
fe6e9dfbbc | ||
|
|
360fbb5f94 | ||
|
|
fbb74b83d3 | ||
|
|
ceffe7d2e4 | ||
|
|
63bee210eb | ||
|
|
d5fba689de | ||
|
|
39d83fc388 | ||
|
|
1b5f774bac | ||
|
|
98cde56413 | ||
|
|
e267c51293 | ||
|
|
5694193959 | ||
|
|
7667c2937c | ||
|
|
b537018ab2 | ||
|
|
eac67a24da | ||
|
|
a804cb6436 | ||
|
|
883554a2db | ||
|
|
daba0e3294 | ||
|
|
c4825c2d66 | ||
|
|
02f7d1ca42 | ||
|
|
cc52bfb69f | ||
|
|
e0d0923488 | ||
|
|
47c2e56393 | ||
|
|
004d168157 | ||
|
|
87e1320175 | ||
|
|
7a27ed557c | ||
|
|
ec20649e35 | ||
|
|
303848a0fb | ||
|
|
74d6b7393b | ||
|
|
f30b62c457 | ||
|
|
38fd57846c | ||
|
|
9628587bfa | ||
|
|
5421bf931b | ||
|
|
292c511ab5 | ||
|
|
654b89d84f | ||
|
|
19b80c95a5 | ||
|
|
6ba8302bc8 |
No files matched your search
@@ -183,6 +183,13 @@ jobs:
|
||||
cargo build --release --manifest-path src/rust/route_risk_flags/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/construction_interface/Cargo.toml
|
||||
- name: prune stale /tmp cargo target dirs (tmpfs guard)
|
||||
run: |
|
||||
# /tmp is a 32G tmpfs; every CI run leaves a ~218MB
|
||||
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
|
||||
# the tmpfs (2026-10-07: venv creation failed disk-full across
|
||||
# every job). Keep the 3 newest; drop the rest.
|
||||
cd /tmp && ls -dt rune-ci-target-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
|
||||
- name: go vet + go test + go build
|
||||
run: |
|
||||
for dir in src/go/*/; do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.108.2**
|
||||
**Current release: v0.123.0**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine — compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-16T18:23:38Z",
|
||||
"generated_at": "2026-10-07T16:54:42Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"canaries": [
|
||||
@@ -187,11 +187,13 @@
|
||||
"modules": [
|
||||
"abstraction",
|
||||
"cache",
|
||||
"drift-ledger",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"ffi_query_memory_smoke",
|
||||
"graph_memory",
|
||||
"graph_shard_bridge",
|
||||
"lexicon_evolution",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"memory_selector",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.108.2"
|
||||
version = "0.123.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_build_mode_truth.py — build-mode batch 1 truth battery
|
||||
(contract BUILDER-2026-10-07T17:08:00Z-6e177d-unknown).
|
||||
|
||||
The LIVE BuildManager runs with the clock FROZEN (module-patched datetime)
|
||||
and create_worktree no-op'd (reactive piece, deferred to batch 2). Cases
|
||||
capture: the start_build id format, the ledger flow across phases
|
||||
(including a failure + correction), resume-point scans at several states,
|
||||
and the status shape. The Go replay compares state dicts key-for-key.
|
||||
|
||||
Usage: python3 scripts/gen_build_mode_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_mode as bm # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T16:08:00"
|
||||
FROZEN_DT = datetime(2026, 10, 7, 16, 8, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return FROZEN_DT
|
||||
|
||||
|
||||
def main():
|
||||
# Freeze the clock (the manager reads datetime.now at two call sites)
|
||||
bm.datetime = FrozenDatetime
|
||||
|
||||
def flow(name, steps):
|
||||
"""Run a phase flow with the worktree op no-op'd; record the CALLS."""
|
||||
mgr = bm.BuildManager()
|
||||
mgr.create_worktree = lambda: "/nonexistent-worktree" # reactive no-op
|
||||
build_id = mgr.start_build("truth battery build")
|
||||
calls = []
|
||||
for fn, args in steps:
|
||||
fn(mgr, **args)
|
||||
calls.append({"fn": fn.__name__, "args": args})
|
||||
return {
|
||||
"name": name,
|
||||
"build_id": build_id,
|
||||
"calls": calls,
|
||||
"state": _jsonify(mgr.state),
|
||||
"resume_point": mgr.get_resume_point(),
|
||||
"can_resume": mgr.can_resume(),
|
||||
"status": mgr.get_status(),
|
||||
}
|
||||
|
||||
def step(fn, **args):
|
||||
return (fn, args)
|
||||
|
||||
cases = [
|
||||
flow("fresh_start_assess_in_progress", []),
|
||||
flow("assess_complete", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={"complexity": 3}),
|
||||
]),
|
||||
flow("through_plan", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={"complexity": 3}),
|
||||
step(bm.BuildManager.complete_research, findings={"findings": "dichotomy applies"}),
|
||||
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
|
||||
step(bm.BuildManager.complete_consult, patterns=["oracle-battery pattern"]),
|
||||
step(bm.BuildManager.complete_consent, approved=True),
|
||||
step(bm.BuildManager.complete_plan, batches=[{"batch": 1, "fns": 11}]),
|
||||
]),
|
||||
flow("verify_with_failures", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={}),
|
||||
step(bm.BuildManager.complete_research, findings={}),
|
||||
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
|
||||
step(bm.BuildManager.complete_consult, patterns=[]),
|
||||
step(bm.BuildManager.complete_consent, approved=True),
|
||||
step(bm.BuildManager.complete_plan, batches=[]),
|
||||
step(bm.BuildManager.complete_build_phase, results=[{"ok": True}]),
|
||||
step(bm.BuildManager.complete_verify, passed=False, failures=["test_x failed"]),
|
||||
]),
|
||||
flow("correction_cycle", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={}),
|
||||
step(bm.BuildManager.complete_research, findings={}),
|
||||
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
|
||||
step(bm.BuildManager.complete_consult, patterns=[]),
|
||||
step(bm.BuildManager.complete_consent, approved=True),
|
||||
step(bm.BuildManager.complete_plan, batches=[{"batch": 1}]),
|
||||
step(bm.BuildManager.start_correct),
|
||||
step(bm.BuildManager.complete_correct, fixed=True),
|
||||
]),
|
||||
]
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/build_mode.py (live BuildManager, clock frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/build-mode" / "truth_buildmode.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} flow cases)")
|
||||
|
||||
|
||||
def _jsonify(state):
|
||||
"""asdict + the worktree path the no-op returned."""
|
||||
from dataclasses import asdict
|
||||
d = asdict(state)
|
||||
return d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_build_mode_wt_truth.py — build-mode batch 2 truth battery
|
||||
(contract BUILDER-2026-10-07T18:26:40Z-8be89a).
|
||||
|
||||
The LIVE BuildManager drives a REAL tmp git repo: create → state captured,
|
||||
merge → state captured, delete → state captured. Cases carry the exact
|
||||
argv sequences (from monkeypatched subprocess.run) + the state after each
|
||||
op + the error texts. The Go plan layer must produce the same argv; the
|
||||
Go exec harness proves the plans drive real git.
|
||||
|
||||
Usage: python3 scripts/gen_build_mode_wt_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_mode as bm # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T18:28:00"
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return datetime(2026, 10, 7, 18, 28, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def main():
|
||||
bm.datetime = FrozenDatetime
|
||||
bm.REPO_DIR = str(make_git_repo())
|
||||
|
||||
calls = []
|
||||
|
||||
def spy_run(cmd, **kwargs):
|
||||
calls.append(list(cmd))
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
bm.subprocess.run = spy_run
|
||||
|
||||
mgr = bm.BuildManager()
|
||||
build_id = mgr.start_build("worktree battery") # create_worktree inside
|
||||
create_call = calls[0]
|
||||
after_create = mgr.state.worktree_path
|
||||
|
||||
n_before_merge = len(calls)
|
||||
mgr.merge_worktree()
|
||||
merge_call = calls[n_before_merge] # the merge itself
|
||||
after_merge = mgr.state.worktree_path # None: delete follows the merge
|
||||
delete_call = calls[n_before_merge + 1], calls[n_before_merge + 2]
|
||||
|
||||
cases = [
|
||||
{
|
||||
"fn": "plan", "name": "create",
|
||||
"expected": {"argv": create_call},
|
||||
},
|
||||
{
|
||||
"fn": "plan", "name": "merge",
|
||||
"expected": {"argv": merge_call},
|
||||
},
|
||||
{
|
||||
"fn": "plan", "name": "delete_pair",
|
||||
"expected": {"argv": [list(delete_call[0]), list(delete_call[1])]},
|
||||
},
|
||||
{
|
||||
"fn": "state", "name": "round_trip",
|
||||
"expected": {
|
||||
"build_id": build_id,
|
||||
"after_create_is_set": after_create is not None,
|
||||
"after_merge_is_none": after_merge is None,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# error texts: merge failure mapping
|
||||
def fail_run(cmd, **kwargs):
|
||||
if cmd[1] == "merge":
|
||||
return subprocess.CompletedProcess(cmd, 128, "", "fatal: not something we can merge")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
bm.subprocess.run = fail_run
|
||||
mgr2 = bm.BuildManager()
|
||||
mgr2.state = bm.BuildState(
|
||||
build_id="20261007-182800", description="error battery",
|
||||
created_at=FROZEN)
|
||||
wt = "/home/nick/.rune/worktrees/20261007-182800"
|
||||
mgr2.state.worktree_path = wt # merge_worktree requires a live worktree path
|
||||
try:
|
||||
mgr2.merge_worktree()
|
||||
merge_err = None
|
||||
except RuntimeError as ex:
|
||||
merge_err = str(ex)
|
||||
cases.append({
|
||||
"fn": "error", "name": "merge_failure_text",
|
||||
"expected": {"error": merge_err},
|
||||
})
|
||||
|
||||
def fail_create(cmd, **kwargs):
|
||||
if cmd[1] == "worktree":
|
||||
return subprocess.CompletedProcess(cmd, 128, "", "'x' already exists")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
bm.subprocess.run = fail_create
|
||||
mgr3 = bm.BuildManager()
|
||||
mgr3.state = bm.BuildState(
|
||||
build_id="20261007-182800", description="error battery 2",
|
||||
created_at=FROZEN)
|
||||
mgr3._save_state()
|
||||
try:
|
||||
mgr3.create_worktree()
|
||||
create_err = None
|
||||
except RuntimeError as ex:
|
||||
create_err = str(ex)
|
||||
cases.append({
|
||||
"fn": "error", "name": "create_failure_text",
|
||||
"expected": {"error": create_err},
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/build_mode.py (live BuildManager on a real tmp git repo, clock frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/build-mode" / "truth_worktree.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
def make_git_repo():
|
||||
root = tempfile.mkdtemp(prefix="bm-wt-truth-")
|
||||
env_realloc = subprocess.run(
|
||||
["git", "init", "-q"], cwd=root, capture_output=True, text=True)
|
||||
assert env_realloc.returncode == 0
|
||||
subprocess.run(["git", "config", "user.email", "t@t"], cwd=root, capture_output=True)
|
||||
subprocess.run(["git", "config", "user.name", "t"], cwd=root, capture_output=True)
|
||||
(Path(root) / "seed.txt").write_text("seed\n")
|
||||
subprocess.run(["git", "add", "."], cwd=root, capture_output=True)
|
||||
subprocess.run(["git", "commit", "-qm", "seed"], cwd=root, capture_output=True)
|
||||
return root
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_curator_lexicon_truth.py — regenerate the lexicon truth batteries
|
||||
(contract BUILDER-2026-10-07T16:31:17Z-997c9d).
|
||||
|
||||
Why: batch 1's truth.json encoded the LEXICON DEGRADE — the phantom shadow
|
||||
("lexicon module unavailable — strength gate only") — because
|
||||
lexicon_evolution.py was absent when it was generated. The phantom has now
|
||||
been recovered from provenance (commit 5749029), so the LIVE oracle returns
|
||||
real lexicon verdicts and the Go port must match THEM.
|
||||
|
||||
What this does:
|
||||
1. Rewrites ONLY the fn=='le_promote_eligible' cases inside truth.json,
|
||||
regenerated against the live curator.le_promote_eligible (which now
|
||||
delegates to the recovered lexicon_evolution). Other cases untouched.
|
||||
2. Emits truth_lexicon.json — direct live-oracle cases for the recovered
|
||||
module's full surface: coherence (conflict/consistent/novel), grounded,
|
||||
distinctive_terms, promote_eligible (shared / novel-grounded /
|
||||
novel-parked / no-signal / strength-gate).
|
||||
|
||||
Note on `existing`: the truth carries the block values as a single string;
|
||||
the oracle is called with a one-element list ([existing]) — the batch-1
|
||||
convention, documented here so the Go test wraps identically.
|
||||
|
||||
Usage: python3 scripts/gen_curator_lexicon_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
from rune import curator # noqa: E402 — live oracle (imports lexicon_evolution)
|
||||
import lexicon_evolution as le # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
core = REPO / "src/go/curator-core"
|
||||
|
||||
# ── 1. regenerate the le_promote_eligible cases in truth.json ──────────
|
||||
truth_path = core / "truth.json"
|
||||
truth = json.loads(truth_path.read_text())
|
||||
replaced = 0
|
||||
for case in truth:
|
||||
if case.get("fn") != "le_promote_eligible":
|
||||
continue
|
||||
inp = case["input"]
|
||||
r = curator.le_promote_eligible(
|
||||
inp["proposal"],
|
||||
[inp["existing"]],
|
||||
inp["strength"],
|
||||
min_strength=3,
|
||||
wash_count=inp["wash_count"],
|
||||
)
|
||||
case["expected"] = {"eligible": r}
|
||||
replaced += 1
|
||||
assert replaced >= 4, f"expected >=4 le cases, found {replaced}"
|
||||
# Self-check: no case may still encode the degrade shadow.
|
||||
for case in truth:
|
||||
if case.get("fn") == "le_promote_eligible":
|
||||
assert "lexicon module unavailable" not in json.dumps(case["expected"]), case["name"]
|
||||
truth_path.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"truth.json: {replaced} le_promote_eligible cases regenerated (live lexicon)")
|
||||
|
||||
# ── 2. the direct lexicon-surface battery ──────────────────────────────
|
||||
corpus = [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust",
|
||||
]
|
||||
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# coherence: conflict (distinctive shared term + negation ON it)
|
||||
add("coherence", "conflict",
|
||||
{"fact": "nick never writes Rust code", "existing": ["nick writes Go and Rust code daily"],
|
||||
"corpus": corpus},
|
||||
{"verdict": le.coherence("nick never writes Rust code",
|
||||
["nick writes Go and Rust code daily"], corpus)})
|
||||
# coherence: consistent (distinctive shared term, no negation)
|
||||
add("coherence", "consistent",
|
||||
{"fact": "nick writes Rust code on weekends", "existing": ["nick writes Go and Rust code daily"],
|
||||
"corpus": corpus},
|
||||
{"verdict": le.coherence("nick writes Rust code on weekends",
|
||||
["nick writes Go and Rust code daily"], corpus)})
|
||||
# coherence: novel (nothing distinctive shared)
|
||||
add("coherence", "novel_axis",
|
||||
{"fact": "nick plays bass guitar in a band", "existing": ["nick writes Go and Rust code daily"],
|
||||
"corpus": corpus},
|
||||
{"verdict": le.coherence("nick plays bass guitar in a band",
|
||||
["nick writes Go and Rust code daily"], corpus)})
|
||||
# coherence: common word only is NEVER a relation
|
||||
add("coherence", "common_word_not_relation",
|
||||
{"fact": "nick likes black coffee", "existing": ["black holes bend light"],
|
||||
"corpus": ["black coffee in the morning", "black holes bend light"]},
|
||||
{"verdict": le.coherence("nick likes black coffee", ["black holes bend light"],
|
||||
["black coffee in the morning", "black holes bend light"])})
|
||||
# coherence: empty existing -> novel
|
||||
add("coherence", "empty_existing",
|
||||
{"fact": "nick writes Rust code daily", "existing": [], "corpus": []},
|
||||
{"verdict": le.coherence("nick writes Rust code daily", [], [])})
|
||||
|
||||
# grounded: stem-aware (migrated matches migration)
|
||||
add("grounded", "stem_match",
|
||||
{"fact": "nick migrated the modules", "sources": corpus},
|
||||
{"grounded": le.grounded("nick migrated the modules", corpus)})
|
||||
add("grounded", "invented_token",
|
||||
{"fact": "nick collects vintage railway timetables", "sources": corpus},
|
||||
{"grounded": le.grounded("nick collects vintage railway timetables", corpus)})
|
||||
add("grounded", "empty_sources",
|
||||
{"fact": "nick migrated the modules", "sources": []},
|
||||
{"grounded": le.grounded("nick migrated the modules", [])})
|
||||
|
||||
# distinctive_terms: rare survives, common does not
|
||||
add("distinctive_terms", "rare_vs_common",
|
||||
{"fact": "nick writes Rust code daily", "corpus": corpus},
|
||||
{"terms": sorted(le.distinctive_terms("nick writes Rust code daily", corpus))})
|
||||
|
||||
# promote_eligible: the four verdict shapes
|
||||
add("promote_eligible", "strength_gate",
|
||||
{"proposal": "add support for retries", "existing": ["support for retries in the fetch loop"],
|
||||
"strength": 2, "min_strength": 3, "corpus": []},
|
||||
{"eligible": le.promote_eligible("add support for retries",
|
||||
["support for retries in the fetch loop"], 2, 3, ())})
|
||||
add("promote_eligible", "shared_distinctive",
|
||||
{"proposal": "add retries to the fetch loop", "existing": ["support for retries in the fetch loop"],
|
||||
"strength": 3, "min_strength": 3, "corpus": []},
|
||||
{"eligible": le.promote_eligible("add retries to the fetch loop",
|
||||
["support for retries in the fetch loop"], 3, 3, ())})
|
||||
add("promote_eligible", "novel_grounded",
|
||||
{"proposal": "the curator gates every memory write", "existing": ["nick writes Go and Rust code daily"],
|
||||
"strength": 3, "min_strength": 3, "corpus": corpus},
|
||||
{"eligible": le.promote_eligible("the curator gates every memory write",
|
||||
["nick writes Go and Rust code daily"], 3, 3, corpus)})
|
||||
add("promote_eligible", "novel_parked",
|
||||
{"proposal": "nick collects vintage railway timetables", "existing": ["nick writes Go and Rust code daily"],
|
||||
"strength": 3, "min_strength": 3, "corpus": corpus},
|
||||
{"eligible": le.promote_eligible("nick collects vintage railway timetables",
|
||||
["nick writes Go and Rust code daily"], 3, 3, corpus)})
|
||||
add("promote_eligible", "first_principle",
|
||||
{"proposal": "parking is never wrong for growth", "existing": [],
|
||||
"strength": 3, "min_strength": 3, "corpus": []},
|
||||
{"eligible": le.promote_eligible("parking is never wrong for growth", [], 3, 3, ())})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/lexicon_evolution.py (recovered provenance 0308ccc6) via curator.py",
|
||||
"generated": "2026-10-07 (post-recovery regeneration)",
|
||||
"cases": cases,
|
||||
}
|
||||
(core / "truth_lexicon.json").write_text(
|
||||
json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"truth_lexicon.json: {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_memory_store_truth2.py — memory-store batch 2 truth battery
|
||||
(contract BUILDER-2026-10-07T18:05:47Z-152c16).
|
||||
|
||||
The LIVE _py_* oracles of memory_store.py. Cases: FTS conjunctive builder
|
||||
(stopword filtering, fallback, 8-cap), chunk_text (recombine + hard-split),
|
||||
rank fusion (hybrid/dense-only/lexical-only), project_chunk_rows,
|
||||
map_reduce, fuse_results (dedup + scale-alignment).
|
||||
|
||||
Usage: python3 scripts/gen_memory_store_truth2.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import memory_store as ms # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# ── FTS conjunctive ───────────────────────────────────────────────────
|
||||
add("fts", "significant_terms",
|
||||
{"query": "the curator gates memory writes"},
|
||||
{"q": ms._py_fts_query_conjunctive("the curator gates memory writes")})
|
||||
add("fts", "stopwords_dropped",
|
||||
{"query": "the and was with nick"},
|
||||
{"q": ms._py_fts_query_conjunctive("the and was with nick")})
|
||||
add("fts", "short_fallback",
|
||||
{"query": "go is fun ai"},
|
||||
{"q": ms._py_fts_query_conjunctive("go is fun ai")})
|
||||
long_q = "alpha beta gamma delta epsilon zeta eta theta iota kappa"
|
||||
add("fts", "cap_eight",
|
||||
{"query": long_q},
|
||||
{"q": ms._py_fts_query_conjunctive(long_q)})
|
||||
add("fts", "empty_returns_raw",
|
||||
{"query": "a an of"},
|
||||
{"q": ms._py_fts_query_conjunctive("a an of")})
|
||||
|
||||
# ── chunk_text ────────────────────────────────────────────────────────
|
||||
small = "one paragraph only"
|
||||
add("chunk", "under_size", {"text": small, "size": 1500, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text(small, 1500, 150)})
|
||||
para = "\n\n".join(f"paragraph {i} " + "x" * 40 for i in range(30))
|
||||
add("chunk", "recombine", {"text": para, "size": 400, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text(para, 400, 150)})
|
||||
noline = "y" * 3000
|
||||
add("chunk", "hard_split", {"text": noline, "size": 1000, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text(noline, 1000, 150)})
|
||||
add("chunk", "empty", {"text": "", "size": 1500, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text("", 1500, 150)})
|
||||
|
||||
# ── rank fusion ───────────────────────────────────────────────────────
|
||||
dense = [[1, 0.9], [2, 0.8], [3, 0.7]]
|
||||
bm25 = [[2, 12.0], [4, 8.0], [1, 3.0]]
|
||||
add("fuse_ranks", "hybrid", {"dense": dense, "bm25": bm25},
|
||||
ms._py_rank_fusion(dense, bm25))
|
||||
add("fuse_ranks", "dense_only", {"dense": dense, "bm25": []},
|
||||
ms._py_rank_fusion(dense, []))
|
||||
add("fuse_ranks", "lexical_only", {"dense": [], "bm25": bm25},
|
||||
ms._py_rank_fusion([], bm25))
|
||||
add("fuse_ranks", "neither", {"dense": [], "bm25": []},
|
||||
ms._py_rank_fusion([], []))
|
||||
|
||||
# ── project_chunk_rows ────────────────────────────────────────────────
|
||||
rows = [
|
||||
{"rowid": 1, "chunk_id": "c1", "text": "t" * 500, "wing": "w1", "room": "r1",
|
||||
"source_path": "a.md", "page": 3},
|
||||
{"rowid": 2, "chunk_id": "c2", "text": "short", "wing": "w1", "room": "r1",
|
||||
"source_path": "b.md"},
|
||||
]
|
||||
ranks = {"2": 0.0625, "1": 0.05}
|
||||
add("project", "budget_and_shape", {"ranks": ranks},
|
||||
{"rows": ms._py_project_chunk_rows(ranks, rows, 6)})
|
||||
|
||||
# ── map_reduce ────────────────────────────────────────────────────────
|
||||
entries = [{"id": i, "relevance": i * 0.1} for i in range(25)]
|
||||
add("map_reduce", "top3_per_chunk", {"entries": entries, "chunk_size": 10},
|
||||
{"mapped": ms._py_map_reduce(entries, 10)})
|
||||
add("map_reduce", "under_threshold", {"entries": entries[:5], "chunk_size": 10},
|
||||
{"mapped": ms._py_map_reduce(entries[:5], 10)})
|
||||
|
||||
# ── fuse_results ──────────────────────────────────────────────────────
|
||||
ents = [
|
||||
{"id": 1, "text": "nick prefers coffee", "topic": "human",
|
||||
"relevance": 2.5, "confidence": 0.9},
|
||||
{"id": 2, "text": "NICK PREFERS COFFEE", "topic": "human",
|
||||
"relevance": 1.1, "confidence": 0.8}, # dedup hit
|
||||
{"id": 3, "text": "rune gates memory writes", "topic": "project",
|
||||
"relevance": 5.6, "confidence": 0.95},
|
||||
]
|
||||
res = [
|
||||
{"chunk_id": "c1", "text": "the spine rebuild parity", "wing": "research",
|
||||
"rrf": 0.0625, "confidence": 0.7},
|
||||
{"chunk_id": "c2", "text": "curator provenance recovery", "wing": "research",
|
||||
"rrf": 0.05},
|
||||
]
|
||||
add("fuse_results", "dedup_scale_align", {"entries": ents, "research": res},
|
||||
{"merged": ms._py_fuse_results(ents, res)})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/memory_store.py _py_* oracles",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/memory-store-deep" / "truth_recall.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_scaffold_store_truth.py — scaffold batch 2 truth battery
|
||||
(contract BUILDER-2026-10-07T17:47:00Z-960f12).
|
||||
|
||||
The LIVE oracle (scaffold.Scaffold + structural_store) on a tmp store with
|
||||
now() frozen. Cases: canonical digest stability + secret redaction, commit
|
||||
semantics (success + exact failure texts), verify_on_disk forgotten-save
|
||||
detection, impact frontier (contract_change / implementation_change /
|
||||
contextual), snapshot_predicted_frontier, spine_bridge_entity.
|
||||
|
||||
Usage: python3 scripts/gen_scaffold_store_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T17:50:00"
|
||||
|
||||
|
||||
def main():
|
||||
sc.now = lambda: FROZEN
|
||||
ss.now = lambda: FROZEN
|
||||
|
||||
tmp = Path(tempfile.mkdtemp(prefix="scaffold-store-truth-"))
|
||||
s = sc.Scaffold(tmp, "truth-project")
|
||||
s.init(purpose="store battery")
|
||||
|
||||
cases = []
|
||||
|
||||
# ── canonical digest: stability + redaction ───────────────────────────
|
||||
doc = s.load()
|
||||
d1 = ss.canonical_digest(doc)
|
||||
# key-order permutation must NOT change the digest (semantic content)
|
||||
permuted = {k: doc[k] for k in reversed(list(doc.keys()))}
|
||||
d2 = ss.canonical_digest(permuted)
|
||||
cases.append({
|
||||
"fn": "digest", "name": "key_order_stable",
|
||||
"input": {},
|
||||
"expected": {"digest": d1, "permuted_same": d2 == d1},
|
||||
})
|
||||
secret_doc = {"entities": [{"id": "repo:x", "identities": {
|
||||
"token": "hunter2", "api_key": "sk-123", "full_name": "o/r",
|
||||
"api_token": "zz", "password": "pw", "private_key": "-----BEGIN",
|
||||
"note": "keepme", "my_secret_sauce": "redacted-too"}}]}
|
||||
cases.append({
|
||||
"fn": "digest", "name": "secret_redaction",
|
||||
"input": {},
|
||||
"expected": {"redacted": ss._redact_secrets(secret_doc)},
|
||||
})
|
||||
|
||||
# ── commit: success + digest readback ─────────────────────────────────
|
||||
s.commit(doc, what="battery")
|
||||
cases.append({
|
||||
"fn": "commit", "name": "success",
|
||||
"input": {},
|
||||
"expected": {"ok": True, "on_disk_schema": s.load()["schema_version"]},
|
||||
})
|
||||
|
||||
# forgotten-save detection: mutate in memory, DON'T save
|
||||
doc2 = s.load()
|
||||
doc2["entities"].append({"id": "ghost:1", "kind": "ghost", "identities": {}})
|
||||
try:
|
||||
s.verify_on_disk(lambda d: d["entities"], None, what="battery") if False else None
|
||||
# the real call: selector/expect form
|
||||
s.verify_on_disk(lambda d: [e for e in d["entities"] if e["id"] == "ghost:1"],
|
||||
lambda v: bool(v), what="battery")
|
||||
forgotten = {"ok": True}
|
||||
except AssertionError as ex:
|
||||
forgotten = {"ok": False, "error": str(ex)}
|
||||
cases.append({
|
||||
"fn": "verify_on_disk", "name": "forgotten_save",
|
||||
"input": {},
|
||||
"expected": forgotten,
|
||||
})
|
||||
|
||||
# corrupt store -> commit-style load returns None -> reload failure path
|
||||
# (simulate: point at a nonexistent path)
|
||||
class FakeScaffold:
|
||||
current = tmp / "nonexistent" / "current.json"
|
||||
try:
|
||||
ss_load = ss.load(FakeScaffold.current)
|
||||
reload_none = ss_load is None
|
||||
except Exception: # noqa: BLE001 — intentional: the case IS the degrade verdict (load failure → loads_none=False); error text is not the truth column here
|
||||
reload_none = False
|
||||
cases.append({
|
||||
"fn": "load", "name": "missing_store",
|
||||
"input": {},
|
||||
"expected": {"loads_none": reload_none},
|
||||
})
|
||||
|
||||
# ── impact frontier ───────────────────────────────────────────────────
|
||||
doc3 = new_frontier_doc()
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "contract_change",
|
||||
"input": {"seeds": ["contract:c1"], "doc": doc3},
|
||||
"expected": ss.impact_frontier(doc3, ["contract:c1"]),
|
||||
})
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "implementation_change",
|
||||
"input": {"seeds": ["module:impl_a"], "doc": doc3},
|
||||
"expected": ss.impact_frontier(doc3, ["module:impl_a"]),
|
||||
})
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "violation_escalation",
|
||||
"input": {"seeds": ["module:impl_a"], "change_kind": "contract_violation", "doc": doc3},
|
||||
"expected": ss.impact_frontier(doc3, ["module:impl_a"], change_kind="contract_violation"),
|
||||
})
|
||||
|
||||
# ── snapshot_predicted_frontier + spine bridge ────────────────────────
|
||||
doc4 = new_frontier_doc()
|
||||
s4 = sc.Scaffold(tmp / "p4", "frontier-project")
|
||||
s4.init(purpose="frontier battery")
|
||||
doc4 = s4.load()
|
||||
s4.new_contract(doc4, contract_id="f1", need="n", scope=["src/"])
|
||||
# wire the frontier doc entities into the project doc
|
||||
doc4["entities"].extend(e for e in doc3["entities"] if e["id"] not in {x["id"] for x in doc4["entities"]})
|
||||
doc4["relationships"].extend(doc3["relationships"])
|
||||
frontier = s4.snapshot_predicted_frontier(doc4, "f1", ["contract:c1"])
|
||||
e = s4.contract(doc4, "f1")
|
||||
cases.append({
|
||||
"fn": "snapshot_frontier", "name": "persisted",
|
||||
"input": {"doc": doc4},
|
||||
"expected": {
|
||||
"frontier": frontier,
|
||||
"checkpoint_note": e["checkpoints"][-1]["note"],
|
||||
"ext_has": sorted((e.get("ext", {}).get("predicted_frontier") or {}).keys()),
|
||||
},
|
||||
})
|
||||
cases.append({
|
||||
"fn": "bridge", "name": "identity_only",
|
||||
"input": {},
|
||||
"expected": {"bridge": s4.spine_bridge_entity()},
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/scaffold.py + structural_store.py (live, now frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/scaffold" / "truth_store.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def new_frontier_doc():
|
||||
"""A small structural doc exercising the frontier walk."""
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="contract:c1", kind="contract", identities={})
|
||||
ss.add_entity(doc, id="module:impl_a", kind="module", identities={})
|
||||
ss.add_entity(doc, id="module:impl_b", kind="module", identities={})
|
||||
ss.add_entity(doc, id="module:lib", kind="module", identities={})
|
||||
ss.add_entity(doc, id="canary:x", kind="canary", identities={})
|
||||
ss.add_entity(doc, id="deploy:live", kind="deployment", identities={})
|
||||
for rel in (
|
||||
{"type": "implements", "from": "module:impl_a", "to": "contract:c1"},
|
||||
{"type": "implements", "from": "module:impl_b", "to": "contract:c1"},
|
||||
{"type": "depends_on", "from": "module:impl_a", "to": "module:lib"},
|
||||
{"type": "verified_by", "from": "contract:c1", "to": "canary:x"},
|
||||
{"type": "deployed_as", "from": "module:impl_a", "to": "deploy:live"},
|
||||
{"type": "supersedes", "from": "module:impl_b", "to": "module:impl_a"}, # non-obligation
|
||||
):
|
||||
doc["relationships"].append(rel)
|
||||
return doc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_scaffold_truth.py — scaffold batch 1 truth battery
|
||||
(contract BUILDER-2026-10-07T17:27:25Z-6bc737).
|
||||
|
||||
The LIVE Scaffold runs against a tmp store with now() frozen (module-patch
|
||||
on rune.scaffold's imported binding). Cases: the transition machine (legal
|
||||
+ illegal + unknown phase, exact error texts), new_contract/transition_
|
||||
contract state evolution, check_scope (green/red/unknown), layout_
|
||||
obligations (FLEXIBLE vs USER-LOCKED), json_frontier projection.
|
||||
|
||||
Usage: python3 scripts/gen_scaffold_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune.structural_store import new_doc, add_entity # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T17:30:00"
|
||||
|
||||
|
||||
def main():
|
||||
# Freeze the clock at scaffold's namespace binding.
|
||||
sc.now = lambda: FROZEN
|
||||
|
||||
tmp = Path(tempfile.mkdtemp(prefix="scaffold-truth-"))
|
||||
s = sc.Scaffold(tmp, "truth-project")
|
||||
s.init(purpose="truth battery")
|
||||
|
||||
cases = []
|
||||
|
||||
# ── transition machine ────────────────────────────────────────────────
|
||||
def tv(name, current, to):
|
||||
try:
|
||||
sc.Scaffold.transition_contract # noqa: B018 — presence probe
|
||||
# validate via a throwaway entity path
|
||||
doc = new_doc()
|
||||
e = s.new_contract(doc, contract_id="probe", need="probe")
|
||||
if current != "draft":
|
||||
e["phase"] = current
|
||||
s.transition_contract(doc, "probe", to)
|
||||
cases.append({"fn": "transition", "name": name,
|
||||
"input": {"current": current, "to": to},
|
||||
"expected": {"ok": True}})
|
||||
except Exception as ex: # noqa: BLE001 — intentional: the case IS the error verdict; the oracle's error text becomes the truth column
|
||||
cases.append({"fn": "transition", "name": name,
|
||||
"input": {"current": current, "to": to},
|
||||
"expected": {"ok": False, "error": str(ex)}})
|
||||
|
||||
tv("legal_approved", "draft", "approved")
|
||||
tv("legal_building", "approved", "building")
|
||||
tv("legal_verifying", "building", "verifying")
|
||||
tv("legal_migrating", "verifying", "migrating")
|
||||
tv("legal_superseding", "migrating", "superseding")
|
||||
tv("legal_closed", "superseding", "closed")
|
||||
tv("legal_failed_retry", "failed", "building")
|
||||
tv("legal_abandon", "building", "abandoned")
|
||||
tv("illegal_closed_from_draft", "draft", "closed")
|
||||
tv("illegal_rewind", "verifying", "draft")
|
||||
tv("illegal_skip", "draft", "verifying")
|
||||
tv("unknown_phase", "draft", "nonexistent_phase")
|
||||
|
||||
# ── contract lifecycle evolution ──────────────────────────────────────
|
||||
doc = new_doc()
|
||||
s.new_contract(doc, contract_id="life", need="port the lifecycle",
|
||||
origin="builder", scope=["src/go/scaffold/"],
|
||||
acceptance=["truth battery passes"])
|
||||
e = s.transition_contract(doc, "life", "approved", evidence_ref="e1")
|
||||
e = s.transition_contract(doc, "life", "building", evidence_ref="e2", note="x" * 300)
|
||||
cases.append({
|
||||
"fn": "lifecycle", "name": "evolution",
|
||||
"input": {},
|
||||
"expected": {"entity": e},
|
||||
})
|
||||
|
||||
# duplicate contract
|
||||
try:
|
||||
s.new_contract(doc, contract_id="life", need="again")
|
||||
dup = {"ok": True}
|
||||
except ValueError as ex:
|
||||
dup = {"ok": False, "error": str(ex)}
|
||||
cases.append({"fn": "dup", "name": "duplicate", "input": {},
|
||||
"expected": dup})
|
||||
|
||||
# ── check_scope ───────────────────────────────────────────────────────
|
||||
doc2 = new_doc()
|
||||
s.new_contract(doc2, contract_id="scoped", need="n", scope=["src/go/a/", "src/go/b/main.go"])
|
||||
e2 = s.contract(doc2, "scoped")
|
||||
cases.append({
|
||||
"fn": "check_scope", "name": "mixed",
|
||||
"input": {"changed": ["src/go/a/x.go", "src/go/b/main.go", "src/other/f.py", "src/go/a/deep/y.go"], "scope": ["src/go/a/", "src/go/b/main.go"]},
|
||||
"expected": s.check_scope(doc2, "scoped",
|
||||
["src/go/a/x.go", "src/go/b/main.go", "src/other/f.py", "src/go/a/deep/y.go"]),
|
||||
})
|
||||
doc3 = new_doc()
|
||||
s.new_contract(doc3, contract_id="unscoped", need="n")
|
||||
cases.append({
|
||||
"fn": "check_scope", "name": "unscoped",
|
||||
"input": {"changed": ["anything.py"], "scope": []},
|
||||
"expected": s.check_scope(doc3, "unscoped", ["anything.py"]),
|
||||
})
|
||||
|
||||
# ── layout obligations ────────────────────────────────────────────────
|
||||
doc4 = new_doc()
|
||||
add_entity(doc4, id="layout:panel", kind="layout_contract",
|
||||
identities={"component": "Panel", "region": "sidebar",
|
||||
"implementation": "src/go/panel.go", "lock": False,
|
||||
"provenance": "scaffold"}, maturity="DECLARED")
|
||||
add_entity(doc4, id="layout:header", kind="layout_contract",
|
||||
identities={"component": "Header", "region": "top",
|
||||
"implementation": "src/go/header.go", "lock": True,
|
||||
"provenance": "user"}, maturity="DECLARED")
|
||||
cases.append({
|
||||
"fn": "layout", "name": "mixed_locks",
|
||||
"input": {"affected": ["Panel", "region:top"], "doc": doc4},
|
||||
"expected": {"obligations": s.layout_obligations(doc4, ["Panel", "region:top"])},
|
||||
})
|
||||
cases.append({
|
||||
"fn": "layout", "name": "no_hit",
|
||||
"input": {"affected": ["Unrelated"], "doc": doc4},
|
||||
"expected": {"obligations": []},
|
||||
})
|
||||
|
||||
# ── json_frontier ─────────────────────────────────────────────────────
|
||||
f = {"contracts": ["c1"], "level2_verification": ["v1"]}
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "projection",
|
||||
"input": {"frontier": f},
|
||||
"expected": {"json": sc.json_frontier(f)},
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/scaffold.py (live Scaffold, now frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/scaffold" / "truth_scaffold.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_tool_executor_truth.py — regenerate the tool-executor truth batteries
|
||||
HERMETICALLY (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
|
||||
Why: the original batch-2/3/4 generators baked the generation host's temp
|
||||
paths (/mnt/workdrive/tmp*) into truth_fileops.json / truth_ops.json /
|
||||
truth_paths.json — inputs AND expectations. CI's runner user cannot read
|
||||
those paths, so the parity gate failed with permission-denied (go-surface,
|
||||
run 19792 job 56712) despite a locally green tree.
|
||||
|
||||
Design (the environment leak, closed):
|
||||
- Fixtures are created at generation time under roots the ORACLE ITSELF
|
||||
allows: read root = first creatable dir in ALLOWED_READ_DIRS (prefers
|
||||
/tmp), write root = first creatable dir in ALLOWED_WRITE_DIRS.
|
||||
- Every occurrence of those roots — in inputs AND expectations — is
|
||||
replaced with {{TMP}} / {{WROOT}} placeholders.
|
||||
- Each truth file carries a `fixtures` manifest (files + dirs) so the Go
|
||||
test recreates the SAME fixture content under ITS OWN roots at runtime.
|
||||
Both sides then run the same case semantics on their own environment;
|
||||
substituted bytes must agree exactly.
|
||||
- The grep-hit comparison stays order-insensitive (sorted line sets) on the
|
||||
Go side: the oracle's os.walk order is OS-scheduling dependent, not a
|
||||
parity-relevant property. Everything else is exact after substitution.
|
||||
|
||||
Oracle authority: src/rune/tool_executor.py is called LIVE for every case
|
||||
except the two cmd_gate verdicts whose oracle path would execute a shell
|
||||
command (plain_ok / shell_ok use harmless commands instead: "ls -la" and
|
||||
"true" — the gate branches they exercise do not inspect command content
|
||||
beyond non-emptiness).
|
||||
|
||||
Usage: python3 scripts/gen_tool_executor_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import tool_executor as te # noqa: E402 — the live oracle
|
||||
|
||||
|
||||
def mk_root(dirs, tag):
|
||||
"""First creatable temp dir under the given allowed dirs."""
|
||||
for d in dirs:
|
||||
try:
|
||||
return tempfile.mkdtemp(prefix="rune-truth-", dir=d)
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError(f"no creatable root among {dirs}")
|
||||
|
||||
|
||||
def build_fixtures(root):
|
||||
"""The shared fixture set: exactly what the cases observe."""
|
||||
(Path(root) / "w.txt").write_text(
|
||||
"".join(f"line {i}\n" for i in range(1, 21)), encoding="utf-8"
|
||||
)
|
||||
(Path(root) / "a.txt").write_text("the spine rebuild parity\n", encoding="utf-8")
|
||||
(Path(root) / "b.log").write_text("spine appears here too\n", encoding="utf-8")
|
||||
(Path(root) / "file.txt").write_text("content\n", encoding="utf-8")
|
||||
(Path(root) / "adir").mkdir()
|
||||
return {
|
||||
"files": [
|
||||
{"path": "{{TMP}}/w.txt",
|
||||
"content": "".join(f"line {i}\n" for i in range(1, 21))},
|
||||
{"path": "{{TMP}}/a.txt", "content": "the spine rebuild parity\n"},
|
||||
{"path": "{{TMP}}/b.log", "content": "spine appears here too\n"},
|
||||
{"path": "{{TMP}}/file.txt", "content": "content\n"},
|
||||
],
|
||||
"dirs": ["{{TMP}}/adir"],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
read_root = mk_root(te.ALLOWED_READ_DIRS, "read")
|
||||
write_root = mk_root(te.ALLOWED_WRITE_DIRS, "write")
|
||||
fixtures = build_fixtures(read_root)
|
||||
|
||||
wpath = f"{read_root}/w.txt"
|
||||
edited = "line 1\nCHANGED\n" + "".join(f"line {i}\n" for i in range(3, 21))
|
||||
|
||||
cases_fileops = [
|
||||
{
|
||||
"fn": "read_window", "name": "mid",
|
||||
"input": {"path": f"{wpath}", "offset": 3, "limit": 5},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "read_window", "name": "default",
|
||||
"input": {"path": f"{wpath}"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "diff", "name": "simple",
|
||||
"input": {"path": wpath, "new_content": edited},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
r = te._read_file({"path": wpath, "offset": 3, "limit": 5})
|
||||
assert r["success"] is True, r
|
||||
cases_fileops[0]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._read_file({"path": wpath})
|
||||
assert r["success"] is True, r
|
||||
cases_fileops[1]["expected"] = {
|
||||
"ok": True, "lines": len(r["result"].split("\n")),
|
||||
}
|
||||
|
||||
diff = te.generate_diff(Path(wpath), edited)
|
||||
dlines = diff.split("\n")
|
||||
cases_fileops[2]["expected"] = {
|
||||
"diff_head": dlines[:3],
|
||||
"has_minus": any(l.startswith("-line 2") for l in dlines),
|
||||
"has_plus": any(l.startswith("+CHANGED") for l in dlines),
|
||||
}
|
||||
|
||||
cases_ops = [
|
||||
{
|
||||
"fn": "list_dir", "name": "basic",
|
||||
"input": {"path": read_root},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "list_dir", "name": "pattern",
|
||||
"input": {"path": read_root, "pattern": "*.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "grep", "name": "hit",
|
||||
"input": {"path": read_root, "pattern": "spine"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "grep", "name": "miss",
|
||||
"input": {"path": read_root, "pattern": "absent-token"},
|
||||
"expected": None,
|
||||
},
|
||||
# The gate branches: pipe_refused and shell_empty return BEFORE any
|
||||
# execution (live oracle); plain_ok / shell_ok would execute, so they
|
||||
# run harmless commands ("ls -la", "true") — the gate branches they
|
||||
# exercise do not inspect command content beyond non-emptiness.
|
||||
{
|
||||
"fn": "cmd_gate", "name": "pipe_refused",
|
||||
"input": {"command": "ls -la | head"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "plain_ok",
|
||||
"input": {"command": "ls -la"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "shell_ok",
|
||||
"input": {"command": "true", "shell": True},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "shell_empty",
|
||||
"input": {"shell": True},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
r = te._list_directory({"path": read_root})
|
||||
assert r["success"] is True, r
|
||||
cases_ops[0]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._list_directory({"path": read_root, "pattern": "*.txt"})
|
||||
assert r["success"] is True, r
|
||||
cases_ops[1]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._search_files({"path": read_root, "pattern": "spine"})
|
||||
assert r["success"] is True and r["result"] != "(no matches)", r
|
||||
cases_ops[2]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._search_files({"path": read_root, "pattern": "absent-token"})
|
||||
assert r["success"] is True and r["result"] == "(no matches)", r
|
||||
cases_ops[3]["expected"] = {"ok": True, "result": "(no matches)"}
|
||||
|
||||
r = te._run_command({"command": "ls -la | head"})
|
||||
cases_ops[4]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is False, r
|
||||
|
||||
r = te._run_command({"command": "ls -la"})
|
||||
cases_ops[5]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is True, r
|
||||
|
||||
r = te._run_command({"command": "true", "shell": True})
|
||||
cases_ops[6]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is True, r
|
||||
|
||||
r = te._run_command({"shell": True})
|
||||
cases_ops[7]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is False, r
|
||||
|
||||
cases_paths = [
|
||||
{
|
||||
"fn": "resolve", "name": "abs",
|
||||
"input": {"path": f"{read_root}/file.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "hit",
|
||||
"input": {"path": f"{read_root}/file.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "miss",
|
||||
"input": {"path": f"{read_root}/missing.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "blocked",
|
||||
"input": {"path": "/etc/hostname"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "protected",
|
||||
"input": {"path": "/etc/x.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "outside",
|
||||
"input": {"path": "/usr/share/x.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "ok",
|
||||
"input": {"path": f"{write_root}/new.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
resolved = str(te.resolve_path(f"{read_root}/file.txt"))
|
||||
cases_paths[0]["expected"] = {"resolved": resolved}
|
||||
|
||||
ok, msg, _ = te.validate_read_path(f"{read_root}/file.txt")
|
||||
cases_paths[1]["expected"] = {"ok": ok}
|
||||
assert ok is True, msg
|
||||
|
||||
ok, msg, _ = te.validate_read_path(f"{read_root}/missing.txt")
|
||||
cases_paths[2]["expected"] = {"ok": ok, "msg": msg}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_read_path("/etc/hostname")
|
||||
cases_paths[3]["expected"] = {"ok": ok}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_write_path("/etc/x.txt")
|
||||
cases_paths[4]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_write_path("/usr/share/x.txt")
|
||||
cases_paths[5]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, resolved = te.validate_write_path(f"{write_root}/new.txt")
|
||||
cases_paths[6]["expected"] = {"ok": ok, "resolved": str(resolved)}
|
||||
assert ok is True, msg
|
||||
|
||||
def templated(obj):
|
||||
s = json.dumps(obj, ensure_ascii=False)
|
||||
s = s.replace(read_root, "{{TMP}}").replace(write_root, "{{WROOT}}")
|
||||
return json.loads(s)
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)",
|
||||
},
|
||||
"fixtures": fixtures,
|
||||
"cases": None,
|
||||
}
|
||||
|
||||
for name, cases in (
|
||||
("truth_fileops.json", cases_fileops),
|
||||
("truth_ops.json", cases_ops),
|
||||
("truth_paths.json", cases_paths),
|
||||
):
|
||||
doc = dict(out)
|
||||
doc["cases"] = templated(cases)
|
||||
text = json.dumps(doc, indent=1, ensure_ascii=False)
|
||||
# Self-check: no generation-time root may leak into the artifact.
|
||||
assert read_root not in text and write_root not in text, name
|
||||
target = REPO / "src/go/tool-executor" / name
|
||||
target.write_text(text + "\n", encoding="utf-8")
|
||||
print(f"wrote {target} ({len(doc['cases'])} cases)")
|
||||
|
||||
# Cleanup the generation-time fixtures; the truth carries the manifest.
|
||||
import shutil
|
||||
shutil.rmtree(read_root, ignore_errors=True)
|
||||
shutil.rmtree(write_root, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,259 @@
|
||||
package main
|
||||
|
||||
// build_mode.go — batch 1: the DETERMINISTIC core of src/rune/build_mode.py
|
||||
// (contract BUILDER-2026-10-07T17:08:00Z-6e177d-unknown). The phase machine,
|
||||
// ledger mechanics, resume scan, and status shape — clock INJECTED per the
|
||||
// date-time-bomb law. Worktree git operations are reactive and stay out
|
||||
// (batch 2).
|
||||
//
|
||||
// PARITY: truth_buildmode.json was generated by the LIVE Python BuildManager
|
||||
// (clock frozen via module patch, create_worktree no-op'd) — the Go replay
|
||||
// must match the state dicts byte-for-byte (key-for-key after JSON decode).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Phase — the 11 build phases in DECLARATION order (resume scans reversed).
|
||||
var Phases = []string{
|
||||
"assess", "research", "feasible", "consult", "consent", "plan",
|
||||
"build", "verify", "correct", "integrate", "record",
|
||||
}
|
||||
|
||||
// PhaseStatus values.
|
||||
const (
|
||||
StatusPending = "pending"
|
||||
StatusInProgress = "in_progress"
|
||||
StatusComplete = "complete"
|
||||
StatusFailed = "failed"
|
||||
StatusSkipped = "skipped"
|
||||
)
|
||||
|
||||
// BuildPhase — three-phase execution for the build step.
|
||||
var BuildPhases = []string{"foundation", "implementation", "integration"}
|
||||
|
||||
// VerificationTiers — tiered verification strategy.
|
||||
var VerificationTiers = []string{"syntactic", "semantic", "integration", "system"}
|
||||
|
||||
// LedgerEntry — one ledger record (Python _log_phase shape: note the
|
||||
// timestamp key, NOT started_at — the dataclass LedgerEntry is a DIFFERENT
|
||||
// shape that the live manager never writes; parity follows the live code).
|
||||
type LedgerEntry struct {
|
||||
Phase string `json:"phase"`
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// BuildState — the manager's persisted state (Python BuildState/asdict).
|
||||
type BuildState struct {
|
||||
BuildID string `json:"build_id"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
WorktreePath *string `json:"worktree_path"`
|
||||
CurrentPhase string `json:"current_phase"`
|
||||
Phases map[string]string `json:"phases"`
|
||||
Ledger []LedgerEntry `json:"ledger"`
|
||||
Batches []map[string]interface{} `json:"batches"`
|
||||
Corrections int `json:"corrections"`
|
||||
MaxCorrections int `json:"max_corrections"`
|
||||
CurrentBuildPhase string `json:"current_build_phase"`
|
||||
Trajectory []map[string]interface{} `json:"trajectory"`
|
||||
FailurePatterns []map[string]interface{} `json:"failure_patterns"`
|
||||
ComplexityScore int `json:"complexity_score"`
|
||||
EstimatedSteps int `json:"estimated_steps"`
|
||||
ActualSteps int `json:"actual_steps"`
|
||||
FilesChanged []string `json:"files_changed"`
|
||||
Contracts []map[string]interface{} `json:"contracts"`
|
||||
}
|
||||
|
||||
// NewBuildState — the Python constructor's defaults. Slices initialize
|
||||
// EMPTY (Python asdict serializes [] — Go nil marshals null, which would
|
||||
// break byte parity).
|
||||
func NewBuildState(buildID, description, createdAt string) *BuildState {
|
||||
return &BuildState{
|
||||
BuildID: buildID,
|
||||
Description: description,
|
||||
CreatedAt: createdAt,
|
||||
CurrentPhase: "assess",
|
||||
Phases: map[string]string{},
|
||||
Ledger: []LedgerEntry{},
|
||||
Batches: []map[string]interface{}{},
|
||||
MaxCorrections: 3,
|
||||
CurrentBuildPhase: "foundation",
|
||||
Trajectory: []map[string]interface{}{},
|
||||
FailurePatterns: []map[string]interface{}{},
|
||||
FilesChanged: []string{},
|
||||
Contracts: []map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
// LogPhase — _log_phase: append the ledger entry, update the phases map.
|
||||
// (The Python manager also saves to disk — the store write stays reactive.)
|
||||
func LogPhase(state *BuildState, phase, status string, details map[string]interface{}, errors []string, nowISO string) {
|
||||
if details == nil {
|
||||
details = map[string]interface{}{}
|
||||
}
|
||||
if errors == nil {
|
||||
errors = []string{}
|
||||
}
|
||||
state.Ledger = append(state.Ledger, LedgerEntry{
|
||||
Phase: phase,
|
||||
Status: status,
|
||||
Timestamp: nowISO,
|
||||
Details: details,
|
||||
Errors: errors,
|
||||
})
|
||||
state.Phases[phase] = status
|
||||
}
|
||||
|
||||
// CompleteAssess — complete_assess.
|
||||
func CompleteAssess(state *BuildState, report map[string]interface{}, nowISO string) {
|
||||
LogPhase(state, "assess", StatusComplete, report, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteResearch — complete_research.
|
||||
func CompleteResearch(state *BuildState, findings map[string]interface{}, nowISO string) {
|
||||
LogPhase(state, "research", StatusComplete, findings, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteFeasible — complete_feasible. PARITY: the payload carries a
|
||||
// nested "details" key (details or {}) — cut-off reading missed it once;
|
||||
// the oracle's ledger pinned it.
|
||||
func CompleteFeasible(state *BuildState, feasible bool, risk string, extra map[string]interface{}, nowISO string) {
|
||||
if extra == nil {
|
||||
extra = map[string]interface{}{}
|
||||
}
|
||||
LogPhase(state, "feasible", StatusComplete, map[string]interface{}{
|
||||
"feasible": feasible, "risk": risk, "details": extra,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteConsult — complete_consult.
|
||||
func CompleteConsult(state *BuildState, patterns []string, nowISO string) {
|
||||
if patterns == nil {
|
||||
patterns = []string{}
|
||||
}
|
||||
LogPhase(state, "consult", StatusComplete, map[string]interface{}{
|
||||
"patterns": patterns,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteConsent — complete_consent.
|
||||
func CompleteConsent(state *BuildState, approved bool, nowISO string) {
|
||||
LogPhase(state, "consent", StatusComplete, map[string]interface{}{
|
||||
"approved": approved,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompletePlan — complete_plan: the batches ASSIGNMENT is a state side
|
||||
// effect beyond the ledger entry (parity pinned).
|
||||
func CompletePlan(state *BuildState, batches []map[string]interface{}, nowISO string) {
|
||||
if batches == nil {
|
||||
batches = []map[string]interface{}{}
|
||||
}
|
||||
state.Batches = batches
|
||||
LogPhase(state, "plan", StatusComplete, map[string]interface{}{
|
||||
"batches": len(batches),
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteBuildPhase — complete_build_phase.
|
||||
func CompleteBuildPhase(state *BuildState, results []map[string]interface{}, nowISO string) {
|
||||
if results == nil {
|
||||
results = []map[string]interface{}{}
|
||||
}
|
||||
LogPhase(state, "build", StatusComplete, map[string]interface{}{
|
||||
"results": results,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteVerify — complete_verify: failure lives in DETAILS (status stays
|
||||
// "complete" — the pinned oracle quirk; the resume scan never fires on it).
|
||||
func CompleteVerify(state *BuildState, passed bool, failures []string, nowISO string) {
|
||||
if failures == nil {
|
||||
failures = []string{}
|
||||
}
|
||||
LogPhase(state, "verify", StatusComplete, map[string]interface{}{
|
||||
"passed": passed, "failures": failures,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartCorrect — start_correct: corrections INCREMENT (state side effect).
|
||||
func StartCorrect(state *BuildState, nowISO string) {
|
||||
state.Corrections++
|
||||
LogPhase(state, "correct", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteCorrect — complete_correct.
|
||||
func CompleteCorrect(state *BuildState, fixed bool, nowISO string) {
|
||||
LogPhase(state, "correct", StatusComplete, map[string]interface{}{
|
||||
"fixed": fixed,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteIntegrate — complete_integrate.
|
||||
func CompleteIntegrate(state *BuildState, passed bool, failures []string, nowISO string) {
|
||||
if failures == nil {
|
||||
failures = []string{}
|
||||
}
|
||||
LogPhase(state, "integrate", StatusComplete, map[string]interface{}{
|
||||
"passed": passed, "failures": failures,
|
||||
}, nil, nowISO)
|
||||
}
|
||||
|
||||
// CompleteRecord — complete_record.
|
||||
func CompleteRecord(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "record", StatusComplete, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartBuildID — the build_id format: %Y%m%d-%H%M%S (UTC) from the
|
||||
// INJECTED clock.
|
||||
func StartBuildID(nowUTC string) string {
|
||||
// nowUTC is "2026-10-07T16:08:00" (isoformat seconds, naive-UTC form
|
||||
// the generator freezes) → "20261007-160800".
|
||||
if len(nowUTC) < 19 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s-%s%s%s",
|
||||
nowUTC[0:4], nowUTC[5:7], nowUTC[8:10],
|
||||
nowUTC[11:13], nowUTC[14:16], nowUTC[17:19])
|
||||
}
|
||||
|
||||
// ResumePoint — reversed declaration-order scan: the first phase whose
|
||||
// status is in_progress or failed; "" when all complete (Python None).
|
||||
func ResumePoint(state *BuildState) string {
|
||||
for i := len(Phases) - 1; i >= 0; i-- {
|
||||
status, has := state.Phases[Phases[i]]
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
if status == StatusInProgress || status == StatusFailed {
|
||||
return Phases[i]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CanResume — resume point exists.
|
||||
func CanResume(state *BuildState) bool {
|
||||
return ResumePoint(state) != ""
|
||||
}
|
||||
|
||||
// BuildStatus — the get_status shape.
|
||||
func BuildStatus(state *BuildState) map[string]interface{} {
|
||||
if state == nil {
|
||||
return map[string]interface{}{"status": "no_build"}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"build_id": state.BuildID,
|
||||
"description": state.Description,
|
||||
"current_phase": state.CurrentPhase,
|
||||
"phases": state.Phases,
|
||||
"batches": len(state.Batches),
|
||||
"corrections": state.Corrections,
|
||||
"worktree": state.WorktreePath,
|
||||
"ledger_entries": len(state.Ledger),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
// cli.go — the module-family CLI: one JSON op per invocation (curator-core
|
||||
// convention). Exposes the ported build-mode mechanics for shell/Go callers;
|
||||
// a main package needs a main().
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var req map[string]interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad JSON input: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
op, _ := req["op"].(string)
|
||||
switch op {
|
||||
case "start_build_id":
|
||||
// Live-clock variant: %Y%m%d-%H%M%S in UTC (the truth battery used
|
||||
// the frozen clock; the CLI serves the reactive layer).
|
||||
nowISO := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
emit(map[string]interface{}{"build_id": StartBuildID(nowISO)})
|
||||
case "log_phase":
|
||||
stateAny, _ := req["state"].(map[string]interface{})
|
||||
var state BuildState
|
||||
sb, _ := json.Marshal(stateAny)
|
||||
if err := json.Unmarshal(sb, &state); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad state: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
details, _ := req["details"].(map[string]interface{})
|
||||
var errs []string
|
||||
if arr, ok := req["errors"].([]interface{}); ok {
|
||||
for _, e := range arr {
|
||||
s, _ := e.(string)
|
||||
errs = append(errs, s)
|
||||
}
|
||||
}
|
||||
nowISO, _ := req["now"].(string)
|
||||
if nowISO == "" {
|
||||
nowISO = time.Now().UTC().Format("2006-01-02T15:04:05+00:00")
|
||||
}
|
||||
LogPhase(&state, str(req, "phase"), str(req, "status"), details, errs, nowISO)
|
||||
emit(map[string]interface{}{"state": state})
|
||||
case "resume_point":
|
||||
var state BuildState
|
||||
sb, _ := json.Marshal(req["state"])
|
||||
if err := json.Unmarshal(sb, &state); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad state: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
emit(map[string]interface{}{"resume_point": ResumePoint(&state)})
|
||||
case "status":
|
||||
var state *BuildState
|
||||
if raw, has := req["state"]; has && raw != nil {
|
||||
state = &BuildState{}
|
||||
sb, _ := json.Marshal(raw)
|
||||
if err := json.Unmarshal(sb, state); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad state: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
emit(BuildStatus(state))
|
||||
default:
|
||||
emit(map[string]interface{}{"error": "unknown op: " + op})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func str(m map[string]interface{}, k string) string {
|
||||
v, _ := m[k].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func emit(v map[string]interface{}) {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/build-mode
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,676 @@
|
||||
{
|
||||
"oracle": "src/rune/build_mode.py (live BuildManager, clock frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": "2026-10-07T16:08:00",
|
||||
"cases": [
|
||||
{
|
||||
"name": "fresh_start_assess_in_progress",
|
||||
"build_id": "20261007-160800",
|
||||
"calls": [],
|
||||
"state": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"created_at": "2026-10-07T16:08:00+00:00",
|
||||
"worktree_path": null,
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "in_progress"
|
||||
},
|
||||
"ledger": [
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"batches": [],
|
||||
"corrections": 0,
|
||||
"max_corrections": 3,
|
||||
"current_build_phase": "foundation",
|
||||
"trajectory": [],
|
||||
"failure_patterns": [],
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"actual_steps": 0,
|
||||
"files_changed": [],
|
||||
"contracts": []
|
||||
},
|
||||
"resume_point": "assess",
|
||||
"can_resume": true,
|
||||
"status": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "in_progress"
|
||||
},
|
||||
"batches": 0,
|
||||
"corrections": 0,
|
||||
"worktree": null,
|
||||
"ledger_entries": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assess_complete",
|
||||
"build_id": "20261007-160800",
|
||||
"calls": [
|
||||
{
|
||||
"fn": "complete_assess",
|
||||
"args": {
|
||||
"complexity_report": {
|
||||
"complexity": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"created_at": "2026-10-07T16:08:00+00:00",
|
||||
"worktree_path": null,
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete"
|
||||
},
|
||||
"ledger": [
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"complexity": 3
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"batches": [],
|
||||
"corrections": 0,
|
||||
"max_corrections": 3,
|
||||
"current_build_phase": "foundation",
|
||||
"trajectory": [],
|
||||
"failure_patterns": [],
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"actual_steps": 0,
|
||||
"files_changed": [],
|
||||
"contracts": []
|
||||
},
|
||||
"resume_point": null,
|
||||
"can_resume": false,
|
||||
"status": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete"
|
||||
},
|
||||
"batches": 0,
|
||||
"corrections": 0,
|
||||
"worktree": null,
|
||||
"ledger_entries": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "through_plan",
|
||||
"build_id": "20261007-160800",
|
||||
"calls": [
|
||||
{
|
||||
"fn": "complete_assess",
|
||||
"args": {
|
||||
"complexity_report": {
|
||||
"complexity": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_research",
|
||||
"args": {
|
||||
"findings": {
|
||||
"findings": "dichotomy applies"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_feasible",
|
||||
"args": {
|
||||
"feasible": true,
|
||||
"risk": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_consult",
|
||||
"args": {
|
||||
"patterns": [
|
||||
"oracle-battery pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_consent",
|
||||
"args": {
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_plan",
|
||||
"args": {
|
||||
"batches": [
|
||||
{
|
||||
"batch": 1,
|
||||
"fns": 11
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"created_at": "2026-10-07T16:08:00+00:00",
|
||||
"worktree_path": null,
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete",
|
||||
"research": "complete",
|
||||
"feasible": "complete",
|
||||
"consult": "complete",
|
||||
"consent": "complete",
|
||||
"plan": "complete"
|
||||
},
|
||||
"ledger": [
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"complexity": 3
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "research",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"findings": "dichotomy applies"
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "feasible",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"feasible": true,
|
||||
"risk": "low",
|
||||
"details": {}
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "consult",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"patterns": [
|
||||
"oracle-battery pattern"
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "consent",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"approved": true
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "plan",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"batches": 1
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"batches": [
|
||||
{
|
||||
"batch": 1,
|
||||
"fns": 11
|
||||
}
|
||||
],
|
||||
"corrections": 0,
|
||||
"max_corrections": 3,
|
||||
"current_build_phase": "foundation",
|
||||
"trajectory": [],
|
||||
"failure_patterns": [],
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"actual_steps": 0,
|
||||
"files_changed": [],
|
||||
"contracts": []
|
||||
},
|
||||
"resume_point": null,
|
||||
"can_resume": false,
|
||||
"status": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete",
|
||||
"research": "complete",
|
||||
"feasible": "complete",
|
||||
"consult": "complete",
|
||||
"consent": "complete",
|
||||
"plan": "complete"
|
||||
},
|
||||
"batches": 1,
|
||||
"corrections": 0,
|
||||
"worktree": null,
|
||||
"ledger_entries": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "verify_with_failures",
|
||||
"build_id": "20261007-160800",
|
||||
"calls": [
|
||||
{
|
||||
"fn": "complete_assess",
|
||||
"args": {
|
||||
"complexity_report": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_research",
|
||||
"args": {
|
||||
"findings": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_feasible",
|
||||
"args": {
|
||||
"feasible": true,
|
||||
"risk": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_consult",
|
||||
"args": {
|
||||
"patterns": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_consent",
|
||||
"args": {
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_plan",
|
||||
"args": {
|
||||
"batches": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_build_phase",
|
||||
"args": {
|
||||
"results": [
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_verify",
|
||||
"args": {
|
||||
"passed": false,
|
||||
"failures": [
|
||||
"test_x failed"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"created_at": "2026-10-07T16:08:00+00:00",
|
||||
"worktree_path": null,
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete",
|
||||
"research": "complete",
|
||||
"feasible": "complete",
|
||||
"consult": "complete",
|
||||
"consent": "complete",
|
||||
"plan": "complete",
|
||||
"build": "complete",
|
||||
"verify": "complete"
|
||||
},
|
||||
"ledger": [
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "research",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "feasible",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"feasible": true,
|
||||
"risk": "low",
|
||||
"details": {}
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "consult",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"patterns": []
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "consent",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"approved": true
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "plan",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"batches": 0
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "build",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"results": [
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "verify",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"passed": false,
|
||||
"failures": [
|
||||
"test_x failed"
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"batches": [],
|
||||
"corrections": 0,
|
||||
"max_corrections": 3,
|
||||
"current_build_phase": "foundation",
|
||||
"trajectory": [],
|
||||
"failure_patterns": [],
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"actual_steps": 0,
|
||||
"files_changed": [],
|
||||
"contracts": []
|
||||
},
|
||||
"resume_point": null,
|
||||
"can_resume": false,
|
||||
"status": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete",
|
||||
"research": "complete",
|
||||
"feasible": "complete",
|
||||
"consult": "complete",
|
||||
"consent": "complete",
|
||||
"plan": "complete",
|
||||
"build": "complete",
|
||||
"verify": "complete"
|
||||
},
|
||||
"batches": 0,
|
||||
"corrections": 0,
|
||||
"worktree": null,
|
||||
"ledger_entries": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "correction_cycle",
|
||||
"build_id": "20261007-160800",
|
||||
"calls": [
|
||||
{
|
||||
"fn": "complete_assess",
|
||||
"args": {
|
||||
"complexity_report": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_research",
|
||||
"args": {
|
||||
"findings": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_feasible",
|
||||
"args": {
|
||||
"feasible": true,
|
||||
"risk": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_consult",
|
||||
"args": {
|
||||
"patterns": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_consent",
|
||||
"args": {
|
||||
"approved": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "complete_plan",
|
||||
"args": {
|
||||
"batches": [
|
||||
{
|
||||
"batch": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "start_correct",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"fn": "complete_correct",
|
||||
"args": {
|
||||
"fixed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"created_at": "2026-10-07T16:08:00+00:00",
|
||||
"worktree_path": null,
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete",
|
||||
"research": "complete",
|
||||
"feasible": "complete",
|
||||
"consult": "complete",
|
||||
"consent": "complete",
|
||||
"plan": "complete",
|
||||
"correct": "complete"
|
||||
},
|
||||
"ledger": [
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "assess",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "research",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "feasible",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"feasible": true,
|
||||
"risk": "low",
|
||||
"details": {}
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "consult",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"patterns": []
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "consent",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"approved": true
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "plan",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"batches": 1
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "correct",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "correct",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-07T16:08:00+00:00",
|
||||
"details": {
|
||||
"fixed": true
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"batches": [
|
||||
{
|
||||
"batch": 1
|
||||
}
|
||||
],
|
||||
"corrections": 1,
|
||||
"max_corrections": 3,
|
||||
"current_build_phase": "foundation",
|
||||
"trajectory": [],
|
||||
"failure_patterns": [],
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"actual_steps": 0,
|
||||
"files_changed": [],
|
||||
"contracts": []
|
||||
},
|
||||
"resume_point": null,
|
||||
"can_resume": false,
|
||||
"status": {
|
||||
"build_id": "20261007-160800",
|
||||
"description": "truth battery build",
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"assess": "complete",
|
||||
"research": "complete",
|
||||
"feasible": "complete",
|
||||
"consult": "complete",
|
||||
"consent": "complete",
|
||||
"plan": "complete",
|
||||
"correct": "complete"
|
||||
},
|
||||
"batches": 1,
|
||||
"corrections": 1,
|
||||
"worktree": null,
|
||||
"ledger_entries": 9
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
// truth_buildmode_test.go — build-mode batch 1 parity gate: the phase
|
||||
// machine + ledger mechanics replay truth_buildmode.json's RECORDED CALLS
|
||||
// through the ported functions (live Python BuildManager, clock frozen —
|
||||
// contract BUILDER-2026-10-07T17:08:00Z-6e177d).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildModeParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_buildmode.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_buildmode.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Name string `json:"name"`
|
||||
BuildID string `json:"build_id"`
|
||||
Calls []struct {
|
||||
Fn string `json:"fn"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
} `json:"calls"`
|
||||
State json.RawMessage `json:"state"`
|
||||
ResumePoint *string `json:"resume_point"`
|
||||
CanResume bool `json:"can_resume"`
|
||||
Status json.RawMessage `json:"status"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
stamp := doc.FrozenNow + "+00:00"
|
||||
for _, c := range doc.Cases {
|
||||
if got := StartBuildID(doc.FrozenNow); got != c.BuildID {
|
||||
t.Errorf("%s: build_id got %q want %q", c.Name, got, c.BuildID)
|
||||
}
|
||||
|
||||
// Replay the recorded calls through the ported functions.
|
||||
got := NewBuildState(c.BuildID, "truth battery build", stamp)
|
||||
noop := "/nonexistent-worktree"
|
||||
got.WorktreePath = &noop
|
||||
// start_build itself logs the first entry (assess/in_progress).
|
||||
LogPhase(got, "assess", StatusInProgress, nil, nil, stamp)
|
||||
for _, call := range c.Calls {
|
||||
args := call.Args
|
||||
switch call.Fn {
|
||||
case "complete_assess":
|
||||
// Python logs the report DICT itself (the parameter's value),
|
||||
// not a kwargs map.
|
||||
report, _ := args["complexity_report"].(map[string]interface{})
|
||||
CompleteAssess(got, report, stamp)
|
||||
case "complete_research":
|
||||
findings, _ := args["findings"].(map[string]interface{})
|
||||
CompleteResearch(got, findings, stamp)
|
||||
case "complete_feasible":
|
||||
CompleteFeasible(got, args["feasible"] == true, str(args, "risk"), nil, stamp)
|
||||
case "complete_consult":
|
||||
CompleteConsult(got, strList(args["patterns"]), stamp)
|
||||
case "complete_consent":
|
||||
CompleteConsent(got, args["approved"] == true, stamp)
|
||||
case "complete_plan":
|
||||
CompletePlan(got, objList(args["batches"]), stamp)
|
||||
case "complete_build_phase":
|
||||
CompleteBuildPhase(got, objList(args["results"]), stamp)
|
||||
case "complete_verify":
|
||||
CompleteVerify(got, args["passed"] == true, strList(args["failures"]), stamp)
|
||||
case "start_correct":
|
||||
StartCorrect(got, stamp)
|
||||
case "complete_correct":
|
||||
CompleteCorrect(got, args["fixed"] == true, stamp)
|
||||
case "complete_integrate":
|
||||
CompleteIntegrate(got, args["passed"] == true, strList(args["failures"]), stamp)
|
||||
case "complete_record":
|
||||
CompleteRecord(got, stamp)
|
||||
default:
|
||||
t.Fatalf("%s: unknown call %q", c.Name, call.Fn)
|
||||
}
|
||||
}
|
||||
|
||||
var wantState BuildState
|
||||
if err := json.Unmarshal(c.State, &wantState); err != nil {
|
||||
t.Fatalf("%s: state unparseable: %v", c.Name, err)
|
||||
}
|
||||
// jsonEq — compare canonical JSON bytes (Go-built []string vs
|
||||
// JSON-decoded []interface{} differ in type, not in bytes).
|
||||
jsonEq := func(name string, got, want interface{}) {
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(want)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: %s divergence:\n got %s\nwant %s", c.Name, name, gb, wb)
|
||||
}
|
||||
}
|
||||
jsonEq("phases", got.Phases, wantState.Phases)
|
||||
jsonEq("ledger", got.Ledger, wantState.Ledger)
|
||||
if got.Corrections != wantState.Corrections {
|
||||
t.Errorf("%s: corrections got %d want %d", c.Name, got.Corrections, wantState.Corrections)
|
||||
}
|
||||
jsonEq("batches", got.Batches, wantState.Batches)
|
||||
|
||||
gotResume := ResumePoint(got)
|
||||
wantResume := ""
|
||||
if c.ResumePoint != nil {
|
||||
wantResume = *c.ResumePoint
|
||||
}
|
||||
if gotResume != wantResume {
|
||||
t.Errorf("%s: resume got %q want %q", c.Name, gotResume, wantResume)
|
||||
}
|
||||
if CanResume(got) != c.CanResume {
|
||||
t.Errorf("%s: can_resume got %v want %v", c.Name, CanResume(got), c.CanResume)
|
||||
}
|
||||
|
||||
gotStatus := BuildStatus(got)
|
||||
gb, _ := json.Marshal(gotStatus)
|
||||
var gotMap, wantMap map[string]interface{}
|
||||
json.Unmarshal(gb, &gotMap)
|
||||
json.Unmarshal(c.Status, &wantMap)
|
||||
// worktree is the no-op path; compare everything else.
|
||||
delete(gotMap, "worktree")
|
||||
delete(wantMap, "worktree")
|
||||
if string(mustJSON(gotMap)) != string(mustJSON(wantMap)) {
|
||||
t.Errorf("%s: status divergence:\n got %v\nwant %v", c.Name, gotMap, wantMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResumeScanQuirk — the pinned oracle quirk: complete_verify(false)
|
||||
// records status "complete" (failure lives in DETAILS), so the resume scan
|
||||
// never fires on it. Failure status only appears if explicitly logged.
|
||||
func TestResumeScanQuirk(t *testing.T) {
|
||||
s := NewBuildState("20261007-160800", "quirk probe", "x")
|
||||
CompletePlan(s, nil, "t")
|
||||
CompleteVerify(s, false, []string{"test_x"}, "t")
|
||||
if rp := ResumePoint(s); rp != "" {
|
||||
t.Errorf("verify-with-failures must NOT trip the resume scan, got %q", rp)
|
||||
}
|
||||
LogPhase(s, "record", StatusFailed, nil, nil, "t")
|
||||
if rp := ResumePoint(s); rp != "record" {
|
||||
t.Errorf("explicitly failed phase must resume, got %q", rp)
|
||||
}
|
||||
}
|
||||
|
||||
// strList / objList — typed JSON accessors (str lives in cli.go).
|
||||
func strList(v interface{}) []string {
|
||||
arr, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
s, _ := e.(string)
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func objList(v interface{}) []map[string]interface{} {
|
||||
arr, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]interface{}, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
m, _ := e.(map[string]interface{})
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mustJSON(v interface{}) []byte {
|
||||
b, _ := json.Marshal(v)
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"oracle": "src/rune/build_mode.py (live BuildManager on a real tmp git repo, clock frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": "2026-10-07T18:28:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "plan",
|
||||
"name": "create",
|
||||
"expected": {
|
||||
"argv": [
|
||||
"git",
|
||||
"worktree",
|
||||
"add",
|
||||
"/home/nick/.rune/worktrees/20261007-182800",
|
||||
"-b",
|
||||
"build-20261007-182800"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "plan",
|
||||
"name": "merge",
|
||||
"expected": {
|
||||
"argv": [
|
||||
"git",
|
||||
"merge",
|
||||
"build-20261007-182800",
|
||||
"--no-ff",
|
||||
"-m",
|
||||
"Build 20261007-182800: worktree battery"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "plan",
|
||||
"name": "delete_pair",
|
||||
"expected": {
|
||||
"argv": [
|
||||
[
|
||||
"git",
|
||||
"worktree",
|
||||
"remove",
|
||||
"/home/nick/.rune/worktrees/20261007-182800",
|
||||
"--force"
|
||||
],
|
||||
[
|
||||
"git",
|
||||
"branch",
|
||||
"-D",
|
||||
"build-20261007-182800"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "state",
|
||||
"name": "round_trip",
|
||||
"expected": {
|
||||
"build_id": "20261007-182800",
|
||||
"after_create_is_set": true,
|
||||
"after_merge_is_none": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "error",
|
||||
"name": "merge_failure_text",
|
||||
"expected": {
|
||||
"error": "Failed to merge worktree: fatal: not something we can merge"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "error",
|
||||
"name": "create_failure_text",
|
||||
"expected": {
|
||||
"error": "Failed to create worktree: 'x' already exists"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
// truth_worktree_test.go — build-mode batch 2 parity gate: the worktree
|
||||
// plans replay truth_worktree.json (live BuildManager driving a real tmp
|
||||
// git repo, clock frozen — contract BUILDER-2026-10-07T18:26:40Z-8be89a),
|
||||
// plus a REAL git round-trip proving the plans drive actual git.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const truthWorktreeDir = "/home/nick/.rune/worktrees"
|
||||
|
||||
func TestWorktreePlanParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_worktree.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_worktree.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
buildID := "20261007-182800"
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Name {
|
||||
case "create":
|
||||
got := WorktreePlan(buildID, truthWorktreeDir)
|
||||
wantArr, _ := c.Expected["argv"].([]interface{})
|
||||
argvEq(t, c.Name, got.Args, wantArr)
|
||||
case "merge":
|
||||
got := MergePlan(buildID, "worktree battery")
|
||||
wantArr, _ := c.Expected["argv"].([]interface{})
|
||||
argvEq(t, c.Name, got.Args, wantArr)
|
||||
case "delete_pair":
|
||||
got := DeletePlan(buildID, filepath.Join(truthWorktreeDir, buildID))
|
||||
wantPair, _ := c.Expected["argv"].([]interface{})
|
||||
if len(got) != len(wantPair) {
|
||||
t.Fatalf("%s: %d plans, want %d", c.Name, len(got), len(wantPair))
|
||||
}
|
||||
for i, w := range wantPair {
|
||||
wantArr, _ := w.([]interface{})
|
||||
argvEq(t, c.Name, got[i].Args, wantArr)
|
||||
}
|
||||
case "round_trip":
|
||||
// state transitions verified in the live round-trip below.
|
||||
case "merge_failure_text":
|
||||
// exercised by the real-repo failure test below.
|
||||
case "create_failure_text":
|
||||
// exercised by the real-repo failure test below.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWorktreeLiveRoundTrip — the plans drive REAL git: create → path
|
||||
// exists; merge of a committed branch lands; delete removes worktree+branch.
|
||||
func TestWorktreeLiveRoundTrip(t *testing.T) {
|
||||
repo := makeGitRepo(t)
|
||||
// redirect the plan cwd to the fixture repo
|
||||
orig := RepoDir
|
||||
RepoDir = repo
|
||||
t.Cleanup(func() { RepoDir = orig })
|
||||
|
||||
buildID := "20261007-182800"
|
||||
wtDir := t.TempDir()
|
||||
|
||||
// CREATE
|
||||
path, err := CreateWorktree(&BuildState{BuildID: buildID}, wtDir)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("worktree path missing after create: %v", err)
|
||||
}
|
||||
|
||||
// commit something ON the build branch (gives merge something to do)
|
||||
if err := gitIn(path, "config", "user.email", "t@t"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gitIn(path, "config", "user.name", "t"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(path, "feature.txt"), []byte("work\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gitIn(path, "add", "."); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gitIn(path, "commit", "-qm", "feature"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// MERGE (from the repo root — git merge requires a work tree, and the
|
||||
// fixture repo's default branch is checked out at the root)
|
||||
state := &BuildState{BuildID: buildID, WorktreePath: &path}
|
||||
if err := MergeWorktree(state, "worktree battery"); err != nil {
|
||||
t.Fatalf("merge: %v", err)
|
||||
}
|
||||
if state.WorktreePath != nil {
|
||||
t.Error("merge must clear the worktree path (delete follows)")
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(repo, "feature.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("merged file missing in repo root: %v", err)
|
||||
}
|
||||
if string(data) != "work\n" {
|
||||
t.Errorf("merged content %q", data)
|
||||
}
|
||||
|
||||
// error texts
|
||||
if err := gitIn(repo, "checkout", "-q", "-b", "conflict"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, "feature.txt"), []byte("other\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gitIn(repo, "commit", "-qam", "conflict"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan := MergePlan(buildID, "worktree battery")
|
||||
cmd := exec.Command(plan.Args[0], plan.Args[1:]...)
|
||||
cmd.Dir = plan.Cwd
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatal("conflicting merge unexpectedly succeeded")
|
||||
}
|
||||
got := "Failed to merge worktree: " + strings.TrimSpace(string(out))
|
||||
// The wrapper text is the stable contract; git's own message varies by
|
||||
// version ("fatal: not something we can merge" / "merge: <b> - not ...").
|
||||
if !strings.HasPrefix(got, "Failed to merge worktree: ") ||
|
||||
!strings.Contains(got, "not something we can merge") {
|
||||
t.Errorf("merge failure text shape: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateFailureText — duplicate worktree → exact Python RuntimeError.
|
||||
func TestCreateFailureText(t *testing.T) {
|
||||
repo := makeGitRepo(t)
|
||||
orig := RepoDir
|
||||
RepoDir = repo
|
||||
t.Cleanup(func() { RepoDir = orig })
|
||||
|
||||
buildID := "20261007-182800"
|
||||
wtDir := t.TempDir()
|
||||
if _, err := CreateWorktree(&BuildState{BuildID: buildID}, wtDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := CreateWorktree(&BuildState{BuildID: buildID}, wtDir)
|
||||
if err == nil {
|
||||
t.Fatal("duplicate create must fail")
|
||||
}
|
||||
want := "Failed to create worktree: "
|
||||
if !strings.HasPrefix(err.Error(), want) {
|
||||
t.Errorf("create failure text %q, want prefix %q", err.Error(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// makeGitRepo — a real git repo fixture (seed commit; deterministic).
|
||||
func makeGitRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
git := func(args ...string) {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = root
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %v: %s", args, err, out)
|
||||
}
|
||||
}
|
||||
git("init", "-q", "-b", "main")
|
||||
git("config", "user.email", "t@t")
|
||||
git("config", "user.name", "t")
|
||||
if err := os.WriteFile(filepath.Join(root, "seed.txt"), []byte("seed\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git("add", ".")
|
||||
git("commit", "-qm", "seed")
|
||||
return root
|
||||
}
|
||||
|
||||
func gitIn(dir string, args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
if _, err := cmd.CombinedOutput(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func argvEq(t *testing.T, name string, got []string, wantArr []interface{}) {
|
||||
t.Helper()
|
||||
if len(got) != len(wantArr) {
|
||||
t.Errorf("%s: argv len %d, want %d: got %v", name, len(got), len(wantArr), got)
|
||||
return
|
||||
}
|
||||
for i, w := range wantArr {
|
||||
want, _ := w.(string)
|
||||
if got[i] != want {
|
||||
t.Errorf("%s: argv[%d] got %q want %q", name, i, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
// worktree.go — batch 2: the worktree git operations of build_mode.py
|
||||
// (contract BUILDER-2026-10-07T18:26:40Z-8be89a). The PLAN layer is pure
|
||||
// (command argv + error mapping + state transitions — oracle-batteried);
|
||||
// the EXEC is reactive (real git, fixture-tested).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RepoDir — build_mode.REPO_DIR (var: tests redirect to a fixture repo).
|
||||
var RepoDir = "/mnt/workdrive/rune"
|
||||
|
||||
// GitPlan — one planned git invocation.
|
||||
type GitPlan struct {
|
||||
Args []string `json:"args"`
|
||||
Cwd string `json:"cwd"`
|
||||
}
|
||||
|
||||
// WorktreePlan — create_worktree: `git worktree add {path} -b build-{id}`.
|
||||
func WorktreePlan(buildID, worktreeDir string) GitPlan {
|
||||
return GitPlan{
|
||||
Args: []string{"git", "worktree", "add",
|
||||
filepath.Join(worktreeDir, buildID), "-b", "build-" + buildID},
|
||||
Cwd: RepoDir,
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePlan — delete_worktree: `git worktree remove {path} --force` then
|
||||
// `git branch -D build-{id}`. BOTH run regardless of the first's outcome
|
||||
// (Python runs two bare subprocess.run calls — no failure short-circuit).
|
||||
func DeletePlan(buildID, worktreePath string) []GitPlan {
|
||||
return []GitPlan{
|
||||
{Args: []string{"git", "worktree", "remove", worktreePath, "--force"}, Cwd: RepoDir},
|
||||
{Args: []string{"git", "branch", "-D", "build-" + buildID}, Cwd: RepoDir},
|
||||
}
|
||||
}
|
||||
|
||||
// MergePlan — merge_worktree: `git merge build-{id} --no-ff -m {msg}`.
|
||||
func MergePlan(buildID, description string) GitPlan {
|
||||
return GitPlan{
|
||||
Args: []string{"git", "merge", "build-" + buildID, "--no-ff",
|
||||
"-m", fmt.Sprintf("Build %s: %s", buildID, description)},
|
||||
Cwd: RepoDir,
|
||||
}
|
||||
}
|
||||
|
||||
// RunPlan — execute one plan; map failure to the Python RuntimeError text.
|
||||
func RunPlan(p GitPlan) error {
|
||||
cmd := exec.Command(p.Args[0], p.Args[1:]...)
|
||||
cmd.Dir = p.Cwd
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("Failed to create worktree: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunPlans — execute a sequence; failures don't short-circuit the
|
||||
// remaining steps (parity with the Python's two bare subprocess calls).
|
||||
// The FIRST error is returned (the merge path raises on its single call;
|
||||
// the delete path swallows both).
|
||||
func RunPlans(plans []GitPlan) error {
|
||||
var firstErr error
|
||||
for _, p := range plans {
|
||||
if err := RunPlan(p); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// CreateWorktree — the reactive wrapper: plan + exec + state transition.
|
||||
func CreateWorktree(state *BuildState, worktreeDir string) (string, error) {
|
||||
plan := WorktreePlan(state.BuildID, worktreeDir)
|
||||
if err := RunPlan(mapCreateError(plan)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := plan.Args[3]
|
||||
state.WorktreePath = &path
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// mapCreateError — create failures carry the "create worktree" text;
|
||||
// RunPlan's default is already that text (single call site).
|
||||
func mapCreateError(p GitPlan) GitPlan { return p }
|
||||
|
||||
// DeleteWorktree — plan + exec + clear state. Errors swallowed (parity:
|
||||
// Python ignores both results).
|
||||
func DeleteWorktree(state *BuildState) {
|
||||
if state.WorktreePath == nil {
|
||||
return
|
||||
}
|
||||
_ = RunPlans(DeletePlan(state.BuildID, *state.WorktreePath))
|
||||
state.WorktreePath = nil
|
||||
}
|
||||
|
||||
// MergeWorktree — plan + exec + cleanup (delete after merge). Error text:
|
||||
// "Failed to merge worktree: {stderr}".
|
||||
func MergeWorktree(state *BuildState, description string) error {
|
||||
if state.WorktreePath == nil {
|
||||
return nil
|
||||
}
|
||||
plan := MergePlan(state.BuildID, description)
|
||||
cmd := exec.Command(plan.Args[0], plan.Args[1:]...)
|
||||
cmd.Dir = plan.Cwd
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("Failed to merge worktree: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
DeleteWorktree(state)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDir — mkdir -p for the builds/worktree dirs (the Python __init__).
|
||||
func EnsureDirs(buildsDir, worktreeDir string) error {
|
||||
for _, d := range []string{buildsDir, worktreeDir} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Binary file not shown.
@@ -48,7 +48,7 @@ func main() {
|
||||
strength := intOf(req["strength"], 1)
|
||||
minS := intOf(req["min_strength"], 3)
|
||||
wash := intOf(req["wash_count"], 0)
|
||||
emit(LePromoteEligible(proposal, existing, strength, minS, wash))
|
||||
emit(LePromoteEligible(proposal, []string{existing}, strength, minS, wash))
|
||||
default:
|
||||
emit(map[string]interface{}{"error": "unknown op: " + op})
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
// coherence.go — batch 3: the semantic coherence gate (the anti-hallucination
|
||||
// check for candidate promotion). PARITY port of curator.py's
|
||||
// _grounded_in_sessions distinctive-word algorithm:
|
||||
//
|
||||
// - Significant words (4+ letters, minus the stop set) are extracted
|
||||
// - The RAREST significant word (by corpus frequency) is the probe
|
||||
// - Grounding requires the rarest word present in >= 2 DISTINCT sessions
|
||||
// (independence beats multiplicity — one rich session can't dominate)
|
||||
// - Any absent rarest word = not grounded (parking is never wrong for
|
||||
// growth; applying wrongly is)
|
||||
//
|
||||
// The corpus-grounding half (_grounded_in_corpus) needs the memory kernel's
|
||||
// chunk_recall — wired later through the Survivor membrane. This file covers
|
||||
// the sessions half, which is the load-bearing gate for behaviour
|
||||
// refinements.
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var wordRE = regexp.MustCompile(`[A-Za-z]{4,}`)
|
||||
|
||||
// stopWords — the Python oracle's stop set (order-insensitive; membership only).
|
||||
var stopWords = map[string]bool{
|
||||
"should": true, "would": true, "could": true, "need": true,
|
||||
"with": true, "that": true, "this": true, "about": true,
|
||||
"before": true, "after": true, "from": true, "into": true,
|
||||
"there": true, "their": true, "these": true, "those": true,
|
||||
"while": true, "being": true, "have": true, "has": true,
|
||||
"been": true, "will": true, "were": true, "when": true,
|
||||
"then": true, "them": true, "they": true, "just": true,
|
||||
"more": true, "also": true, "each": true, "than": true,
|
||||
"very": true, "your": true, "the": true, "and": true,
|
||||
"user": true, "value": true, "i": true, "we": true,
|
||||
"you": true, "our": true, "its": true, "for": true,
|
||||
}
|
||||
|
||||
// GroundedInSessions — the distinctive-word algorithm over in-memory session
|
||||
// texts. The Python oracle filters by file mtime (window_days); the Go port
|
||||
// takes pre-filtered texts (callers supply recent sessions) and the caller
|
||||
// owns the recency policy. Decision logic is byte-faithful.
|
||||
func GroundedInSessions(fact string, texts []string) bool {
|
||||
if len(texts) == 0 {
|
||||
return false
|
||||
}
|
||||
corpus := strings.Join(texts, " ")
|
||||
lowCorpus := strings.ToLower(corpus)
|
||||
|
||||
words := wordRE.FindAllString(strings.ToLower(fact), -1)
|
||||
var sig []string
|
||||
for _, w := range words {
|
||||
if !stopWords[w] {
|
||||
sig = append(sig, w)
|
||||
}
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
freq := map[string]int{}
|
||||
for _, w := range sig {
|
||||
freq[w] = strings.Count(lowCorpus, w)
|
||||
}
|
||||
|
||||
// Rarest significant word (Python: min(sig, key=freq.get, default 0)).
|
||||
rarest := sig[0]
|
||||
for _, w := range sig[1:] {
|
||||
if freq[w] < freq[rarest] {
|
||||
rarest = w
|
||||
}
|
||||
}
|
||||
if !strings.Contains(lowCorpus, rarest) {
|
||||
return false
|
||||
}
|
||||
// Independence: >= 2 DISTINCT sessions contain the rarest word.
|
||||
hits := 0
|
||||
for _, t := range texts {
|
||||
if strings.Contains(strings.ToLower(t), rarest) {
|
||||
hits++
|
||||
}
|
||||
}
|
||||
return hits >= 2
|
||||
}
|
||||
|
||||
// CoherenceCheck — the gate entry (parity with coherence_check's structure).
|
||||
// Sources are checked in order; sessions grounding is in-process, corpus
|
||||
// grounding delegates to corpusFn when wired (nil = skip, as the Python
|
||||
// no-kernel path does).
|
||||
func CoherenceCheck(fact string, sources []string, sessionTexts []string, corpusFn func(string) bool) bool {
|
||||
if len(strings.TrimSpace(fact)) < 15 {
|
||||
return true // non-claim — don't block cold start
|
||||
}
|
||||
for _, src := range sources {
|
||||
if src == "sessions" && GroundedInSessions(fact, sessionTexts) {
|
||||
return true
|
||||
}
|
||||
if src == "corpus" && corpusFn != nil && corpusFn(fact) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var _ = time.Now // reserved for the mtime-window port
|
||||
|
||||
// timeNowUTC — test seam for the decay pass (mirrors Python's now()).
|
||||
func timeNowUTC() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
// decay.go — batch 4: the decay pass and the token-overlap compress
|
||||
// mechanics. PARITY ports of curator.py's decay_pass, _tokens, _overlap
|
||||
// with the same constants (STALE_DAYS=90, DECAY_RATE=0.2, FORGET_FLOOR=0.2).
|
||||
// The full compress_pass needs the store blocks read/write seam — the pure
|
||||
// clustering pieces port here; the store seam lands with the memory kernel
|
||||
// membrane.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const StaleDays = 90
|
||||
const DecayRate = 0.2
|
||||
const ForgetFloor = 0.2
|
||||
|
||||
var compressStopwords = map[string]bool{
|
||||
"the": true, "and": true, "for": true, "with": true, "from": true,
|
||||
"that": true, "this": true, "has": true, "have": true, "was": true,
|
||||
"were": true, "are": true, "you": true, "your": true, "our": true,
|
||||
"not": true, "but": true, "also": true, "his": true, "her": true,
|
||||
"how": true, "all": true, "any": true, "each": true, "own": true,
|
||||
"same": true, "too": true, "very": true,
|
||||
}
|
||||
|
||||
// Tokens — normalized significant words (norm'd, stopword-filtered).
|
||||
func Tokens(text string) []string {
|
||||
var out []string
|
||||
for _, w := range strings.Fields(Norm(text)) {
|
||||
if !compressStopwords[w] {
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Overlap — Jaccard over significant tokens (0.0 when either side is empty).
|
||||
func Overlap(a, b string) float64 {
|
||||
ta := tokenSet(a)
|
||||
tb := tokenSet(b)
|
||||
if len(ta) == 0 || len(tb) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
inter := 0
|
||||
for w := range ta {
|
||||
if tb[w] {
|
||||
inter++
|
||||
}
|
||||
}
|
||||
union := len(ta) + len(tb) - inter
|
||||
return float64(inter) / float64(union)
|
||||
}
|
||||
|
||||
func tokenSet(text string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
for _, w := range Tokens(text) {
|
||||
m[w] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// DecayPass — reduce stale entries' importance; FORGET below the floor.
|
||||
// Skips _candidates and constitution (inviolable). Mutates idx in place.
|
||||
// meta shape: {"importance": float, "last_referenced": iso-string}.
|
||||
func DecayPass(idx Index, journalLines *[]JournalEntry) bool {
|
||||
now := time.Now().UTC()
|
||||
changed := false
|
||||
for label, raw := range idx {
|
||||
if label == CandidatesKey || label == "constitution" {
|
||||
continue
|
||||
}
|
||||
entries, ok := raw.(map[string]map[string]interface{})
|
||||
if !ok {
|
||||
if m, isMap := raw.(map[string]interface{}); isMap {
|
||||
entries = map[string]map[string]interface{}{}
|
||||
for k, v := range m {
|
||||
if em, ok := v.(map[string]interface{}); ok {
|
||||
entries[k] = em
|
||||
}
|
||||
}
|
||||
idx[label] = entries
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for key, meta := range entries {
|
||||
lr, _ := meta["last_referenced"].(string)
|
||||
ageDays := float64(StaleDays) + 1
|
||||
if ref, err := time.Parse(time.RFC3339, lr); err == nil {
|
||||
ageDays = now.Sub(ref).Hours() / 24
|
||||
}
|
||||
if ageDays < StaleDays {
|
||||
continue
|
||||
}
|
||||
imp, _ := meta["importance"].(float64)
|
||||
meta["importance"] = math.Max(0.0, imp-DecayRate)
|
||||
changed = true
|
||||
if meta["importance"].(float64) < ForgetFloor {
|
||||
*journalLines = append(*journalLines, JournalEntry{
|
||||
Op: "FORGET", Target: label,
|
||||
Before: strField(meta, "text"),
|
||||
Evidence: fmt.Sprintf("stale for %.0fd, importance %.2f", ageDays, meta["importance"].(float64)),
|
||||
Source: "decay",
|
||||
})
|
||||
delete(entries, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// ClusterLines — greedy clustering by token overlap (>= 0.35 joins the best
|
||||
// cluster; else a new cluster). PARITY with compress_pass's clustering loop.
|
||||
func ClusterLines(entries []string) [][]string {
|
||||
var clusters [][]string
|
||||
for _, e := range entries {
|
||||
bestI, bestS := -1, 0.35
|
||||
for i, c := range clusters {
|
||||
best := 0.0
|
||||
for _, member := range c {
|
||||
if s := Overlap(e, member); s > best {
|
||||
best = s
|
||||
}
|
||||
}
|
||||
if best > bestS {
|
||||
bestI, bestS = i, best
|
||||
}
|
||||
}
|
||||
if bestI >= 0 {
|
||||
clusters[bestI] = append(clusters[bestI], e)
|
||||
} else {
|
||||
clusters = append(clusters, []string{e})
|
||||
}
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
// ClusterSummary — the representative is the LONGEST line; others noted.
|
||||
// PARITY: rep + " (also: " + first-4 others (60-char each, '- ' stripped) + ")"
|
||||
func ClusterSummary(cluster []string) string {
|
||||
rep := cluster[0]
|
||||
for _, c := range cluster {
|
||||
if len(c) > len(rep) {
|
||||
rep = c
|
||||
}
|
||||
}
|
||||
var others []string
|
||||
for _, m := range cluster {
|
||||
if m != rep {
|
||||
others = append(others, m)
|
||||
}
|
||||
}
|
||||
sort.Strings(others)
|
||||
var parts []string
|
||||
for i, o := range others {
|
||||
if i >= 4 {
|
||||
break
|
||||
}
|
||||
parts = append(parts, truncate(strings.TrimPrefix(strings.TrimSpace(o), "- "), 60))
|
||||
}
|
||||
out := strings.TrimRight(rep, " \t\n")
|
||||
if len(parts) > 0 {
|
||||
out += " (also: " + strings.Join(parts, "; ") + ")"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func strField(meta map[string]interface{}, key string) string {
|
||||
s, _ := meta[key].(string)
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
// lexicon.go — batch 2: the REAL lexicon meta-judgement (verbatim port of
|
||||
// src/rune/lexicon_evolution.py, recovered from ~/odysseus 0308ccc6). This
|
||||
// REPLACES the batch-1 degrade: LePromoteEligible now carries the actual
|
||||
// distinctiveness semantics instead of the strength-only shadow. The degrade
|
||||
// path is retained as LePromoteEligibleDegrade for byte-parity with the
|
||||
// historical batch-1 truth.
|
||||
//
|
||||
// PARITY NOTE: the Python oracle is the LIVE recovered module; truth2.json
|
||||
// cases were generated against it 2026-10-07.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// lexStop — SAME set as memory_store._STOP (Python comment kept verbatim:
|
||||
// single source of truth would be ideal; keep in sync).
|
||||
var lexStop = map[string]bool{
|
||||
"the": true, "and": true, "for": true, "with": true, "that": true,
|
||||
"this": true, "from": true, "into": true, "when": true, "then": true,
|
||||
"were": true, "have": true, "been": true, "will": true, "was": true,
|
||||
"are": true, "but": true, "not": true, "you": true, "your": true,
|
||||
"also": true, "its": true, "his": true, "her": true, "him": true,
|
||||
"over": true, "under": true, "them": true, "they": true, "there": true,
|
||||
"about": true, "after": true, "what": true, "which": true, "who": true,
|
||||
"how": true, "why": true, "where": true, "while": true, "just": true,
|
||||
"more": true, "each": true, "than": true, "very": true, "such": true,
|
||||
"some": true, "only": true, "prefers": true, "needs": true,
|
||||
"wants": true, "likes": true,
|
||||
}
|
||||
|
||||
// lexFraming — generic framing words never carry a relation.
|
||||
var lexFraming = map[string]bool{
|
||||
"work": true, "always": true, "way": true, "thing": true, "value": true,
|
||||
"identity": true, "persona": true, "someone": true, "something": true,
|
||||
"never": true, "before": true, "after": true, "then": true, "when": true,
|
||||
"while": true, "must": true, "should": true, "will": true, "would": true,
|
||||
"could": true, "can": true, "have": true, "has": true, "had": true,
|
||||
"been": true, "being": true, "used": true, "use": true, "uses": true,
|
||||
"make": true, "makes": true, "made": true, "keep": true, "keeps": true,
|
||||
"take": true, "takes": true, "took": true, "get": true, "gets": true,
|
||||
"got": true, "going": true,
|
||||
}
|
||||
|
||||
// lexNegation — negation markers (substring check against the lowercased
|
||||
// fact, Python `n in low` semantics).
|
||||
var lexNegation = []string{
|
||||
"not", "never", "don't", "doesn't", "cannot", "can't", "no",
|
||||
"nor", "neither", "without", "avoid", "reject", "against",
|
||||
"refuses", "refuse", "opposed",
|
||||
}
|
||||
|
||||
const lexDistinctiveDF = 0.05 // same rare-word threshold as recall/associations
|
||||
|
||||
var wordRe = regexp.MustCompile(`[a-z]{4,}`)
|
||||
|
||||
// ContentWords — significant content words (length > 3, not stop/framing).
|
||||
func ContentWords(text string) []string {
|
||||
var out []string
|
||||
for _, w := range wordRe.FindAllString(strings.ToLower(text), -1) {
|
||||
if !lexStop[w] && !lexFraming[w] {
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Stem — light morphological fold (ordered longest-first suffix rules with
|
||||
// optional post-fold, then the y->i rule).
|
||||
func Stem(w string) string {
|
||||
rules := []struct{ suf, fold string }{
|
||||
{"ationally", ""}, {"ableness", ""}, {"iveness", ""},
|
||||
{"ation", ""}, {"ating", ""}, {"ative", "e"},
|
||||
{"atively", "e"}, {"ability", ""}, {"ities", "y"},
|
||||
{"ingly", ""}, {"fully", ""}, {"ness", ""}, {"ment", ""},
|
||||
{"itive", "e"}, {"atable", "e"}, {"ated", ""}, {"ates", "e"},
|
||||
{"ions", ""}, {"ings", ""}, {"able", "e"}, {"ible", "e"},
|
||||
{"ing", "e"}, {"ed", ""}, {"es", ""}, {"ly", ""}, {"s", ""}, {"e", ""},
|
||||
}
|
||||
for _, r := range rules {
|
||||
if len(w)-len(r.suf) >= 4 && strings.HasSuffix(w, r.suf) {
|
||||
w = w[:len(w)-len(r.suf)]
|
||||
if r.fold != "" {
|
||||
w = w + r.fold
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// y -> i fold (honesty -> honest)
|
||||
if len(w) > 4 && strings.HasSuffix(w, "y") && !strings.ContainsRune("aeiouy", rune(w[len(w)-2])) {
|
||||
w = w[:len(w)-1]
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// DistinctiveTerms — terms in fact RARE across the corpus (< 5% df,
|
||||
// stem-aware document frequency, threshold max(total*0.05, 1.5)).
|
||||
func DistinctiveTerms(fact string, corpusTexts []string) map[string]bool {
|
||||
total := len(corpusTexts)
|
||||
if total < 1 {
|
||||
total = 1
|
||||
}
|
||||
factTerms := map[string]bool{}
|
||||
for _, w := range ContentWords(fact) {
|
||||
factTerms[w] = true
|
||||
}
|
||||
if len(factTerms) == 0 {
|
||||
return map[string]bool{}
|
||||
}
|
||||
docTerms := map[string]int{}
|
||||
for _, t := range corpusTexts {
|
||||
seen := map[string]bool{}
|
||||
for _, w := range wordRe.FindAllString(strings.ToLower(t), -1) {
|
||||
seen[Stem(w)] = true
|
||||
}
|
||||
for s := range seen {
|
||||
docTerms[s]++
|
||||
}
|
||||
}
|
||||
thresh := float64(total)*lexDistinctiveDF
|
||||
if thresh < 1.5 {
|
||||
thresh = 1.5
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for t := range factTerms {
|
||||
if float64(docTerms[Stem(t)]) < thresh {
|
||||
out[t] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SharedDistinctive — distinctive terms shared between fact and existing
|
||||
// values (stem-matched).
|
||||
func SharedDistinctive(fact string, existingValues, corpusTexts []string) map[string]bool {
|
||||
distinct := DistinctiveTerms(fact, corpusTexts)
|
||||
if len(distinct) == 0 {
|
||||
return map[string]bool{}
|
||||
}
|
||||
shared := map[string]bool{}
|
||||
type stemVal struct {
|
||||
s string
|
||||
}
|
||||
var existingStems []stemVal
|
||||
for _, val := range existingValues {
|
||||
for _, w := range wordRe.FindAllString(strings.ToLower(val), -1) {
|
||||
existingStems = append(existingStems, stemVal{Stem(w)})
|
||||
}
|
||||
}
|
||||
for term := range distinct {
|
||||
ts := Stem(term)
|
||||
for _, sv := range existingStems {
|
||||
if sv.s == ts {
|
||||
shared[term] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return shared
|
||||
}
|
||||
|
||||
// Coherence — "conflict" | "consistent" | "novel" (lexicon-aware).
|
||||
func Coherence(fact string, existingValues []string, corpusTexts []string) string {
|
||||
var values []string
|
||||
for _, v := range existingValues {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return "novel"
|
||||
}
|
||||
corpus := corpusTexts
|
||||
if len(corpus) == 0 {
|
||||
corpus = values
|
||||
}
|
||||
shared := SharedDistinctive(fact, values, corpus)
|
||||
if len(shared) == 0 {
|
||||
return "novel"
|
||||
}
|
||||
low := strings.ToLower(fact)
|
||||
hasNeg := false
|
||||
for _, n := range lexNegation {
|
||||
if strings.Contains(low, n) {
|
||||
hasNeg = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasNeg {
|
||||
for t := range shared {
|
||||
if strings.Contains(" "+low+" ", " "+t+" ") {
|
||||
return "conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "consistent"
|
||||
}
|
||||
|
||||
// GroundedLex — distinctive-term grounding in source texts (min_distinct=1
|
||||
// default; stem-aware: migrated matches migration).
|
||||
func GroundedLex(fact string, sourceTexts []string, minDistinct int) bool {
|
||||
var srcs []string
|
||||
for _, s := range sourceTexts {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
srcs = append(srcs, s)
|
||||
}
|
||||
}
|
||||
if len(srcs) == 0 {
|
||||
return false
|
||||
}
|
||||
factTerms := map[string]bool{}
|
||||
for _, w := range ContentWords(fact) {
|
||||
factTerms[w] = true
|
||||
}
|
||||
if len(factTerms) == 0 {
|
||||
return false
|
||||
}
|
||||
var joined []string
|
||||
for _, s := range srcs {
|
||||
joined = append(joined, strings.ToLower(s))
|
||||
}
|
||||
stems := map[string]bool{}
|
||||
for _, w := range wordRe.FindAllString(strings.Join(joined, " "), -1) {
|
||||
stems[Stem(w)] = true
|
||||
}
|
||||
hits := 0
|
||||
for t := range factTerms {
|
||||
if stems[Stem(t)] {
|
||||
hits++
|
||||
if hits >= minDistinct {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PromoteEligible — the REAL lexicon-aware eligibility (evidence threshold
|
||||
// AND (shared distinctive term OR novel axis grounded in corpus)).
|
||||
// Verdict strings byte-match the Python f-strings; the shared-terms reason
|
||||
// sorts the terms (Python sorted()) and takes the first 3.
|
||||
func PromoteEligible(proposal string, existing []string, evidenceStrength, minStrength int, corpusTexts []string) map[string]interface{} {
|
||||
if evidenceStrength < minStrength {
|
||||
return map[string]interface{}{
|
||||
"eligible": false,
|
||||
"reason": fmt.Sprintf("evidence strength %d < %d", evidenceStrength, minStrength),
|
||||
}
|
||||
}
|
||||
var block []string
|
||||
for _, e := range existing {
|
||||
if strings.TrimSpace(e) != "" {
|
||||
block = append(block, e)
|
||||
}
|
||||
}
|
||||
if len(block) == 0 {
|
||||
return map[string]interface{}{"eligible": true, "reason": "empty block — first principle"}
|
||||
}
|
||||
corpus := corpusTexts
|
||||
if len(corpus) == 0 {
|
||||
corpus = block
|
||||
}
|
||||
shared := SharedDistinctive(proposal, block, corpus)
|
||||
if len(shared) > 0 {
|
||||
terms := make([]string, 0, len(shared))
|
||||
for t := range shared {
|
||||
terms = append(terms, t)
|
||||
}
|
||||
sort.Strings(terms)
|
||||
n := len(terms)
|
||||
if n > 3 {
|
||||
n = 3
|
||||
}
|
||||
// PARITY: Python f-string renders the sorted list with list repr —
|
||||
// ['a', 'b', 'c'] — not Go's [a b c].
|
||||
var parts []string
|
||||
for _, t := range terms[:n] {
|
||||
parts = append(parts, fmt.Sprintf("'%s'", t))
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"eligible": true,
|
||||
"reason": fmt.Sprintf("shares distinctive term(s): [%s]", strings.Join(parts, ", ")),
|
||||
}
|
||||
}
|
||||
terms := ContentWords(proposal)
|
||||
if len(terms) == 0 {
|
||||
return map[string]interface{}{"eligible": false, "reason": "no lexical signal"}
|
||||
}
|
||||
if GroundedLex(proposal, corpus, 1) {
|
||||
return map[string]interface{}{
|
||||
"eligible": true,
|
||||
"reason": fmt.Sprintf("novel axis grounded in corpus (%d terms)", len(terms)),
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"eligible": false,
|
||||
"reason": "novel axis not grounded in absorbed material — parked",
|
||||
}
|
||||
}
|
||||
@@ -77,10 +77,27 @@ func FindEntry(body, needle string) string {
|
||||
}
|
||||
|
||||
// LePromoteEligible — drift-with-integrity: wash_count lowers the strength
|
||||
// threshold (>=20 → 1, >=10 → 2, >=5 → 2, else min_strength). The lexicon
|
||||
// module is a phantom — Python degrades to the strength-only gate with an
|
||||
// explicit reason string; this port reproduces THAT verdict byte-identically.
|
||||
func LePromoteEligible(proposal, existing string, strength, minStrength, washCount int) map[string]interface{} {
|
||||
// threshold (>=20 → 1, >=10 → 2, >=5 → 2, else min_strength), then the REAL
|
||||
// lexicon verdict (batch 2: the recovered lexicon_evolution semantics via
|
||||
// lexicon.go — shared distinctive term OR novel axis grounded in corpus).
|
||||
// The old strength-only degrade path is preserved as
|
||||
// LePromoteEligibleDegrade for the historical record.
|
||||
func LePromoteEligible(proposal string, existing []string, strength, minStrength, washCount int) map[string]interface{} {
|
||||
adjustedMin := minStrength
|
||||
if washCount >= 20 {
|
||||
adjustedMin = 1
|
||||
} else if washCount >= 10 {
|
||||
adjustedMin = 2
|
||||
} else if washCount >= 5 {
|
||||
adjustedMin = 2
|
||||
}
|
||||
return PromoteEligible(proposal, existing, strength, adjustedMin, nil)
|
||||
}
|
||||
|
||||
// LePromoteEligibleDegrade — the batch-1 verdict shape (kept for the
|
||||
// historical truth record; the phantom is recovered, this path is no
|
||||
// longer the live semantics).
|
||||
func LePromoteEligibleDegrade(proposal, existing string, strength, minStrength, washCount int) map[string]interface{} {
|
||||
adjustedMin := minStrength
|
||||
if washCount >= 20 {
|
||||
adjustedMin = 1
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=3, wash_count=0)"
|
||||
"reason": "shares distinctive term(s): ['retries', 'support']"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -79,7 +79,7 @@
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=2, wash_count=10)"
|
||||
"reason": "shares distinctive term(s): ['retries', 'support']"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=1, wash_count=20)"
|
||||
"reason": "shares distinctive term(s): ['retries', 'support']"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -110,8 +110,8 @@
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=3, wash_count=0)"
|
||||
"eligible": false,
|
||||
"reason": "novel axis not grounded in absorbed material — parked"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -163,4 +163,4 @@
|
||||
"key": "eb081bc793c68acd"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
[
|
||||
{
|
||||
"fn": "grounded_sessions",
|
||||
"name": "g_hit",
|
||||
"input": {
|
||||
"fact": "spine rebuild parity",
|
||||
"sessions": [
|
||||
"spine rebuild verified structural parity",
|
||||
"spine parity held across the rebuild",
|
||||
"unrelated session about railway timetables and tea"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded_sessions",
|
||||
"name": "g_miss_rarest_absent",
|
||||
"input": {
|
||||
"fact": "the spine rebuild preserved structural identity",
|
||||
"sessions": [
|
||||
"spine rebuild verified structural parity",
|
||||
"spine parity held across the rebuild",
|
||||
"unrelated session about railway timetables and tea"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded_sessions",
|
||||
"name": "g_miss_invention",
|
||||
"input": {
|
||||
"fact": "railway timetable scheduling optimization",
|
||||
"sessions": [
|
||||
"spine rebuild verified structural parity",
|
||||
"spine parity held across the rebuild",
|
||||
"unrelated session about railway timetables and tea"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "coherence_short_circuit",
|
||||
"name": "short",
|
||||
"input": {
|
||||
"fact": "too short"
|
||||
},
|
||||
"expected": {
|
||||
"result": true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
// truth_batch3_test.go — batch-3 parity gate: the distinctive-word grounding
|
||||
// algorithm against the live Python oracle (truth_batch3.json, verified
|
||||
// 2026-10-07 — including the subtle miss cases the oracle taught us).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch3Parity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_batch3.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_batch3.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input struct {
|
||||
Fact string `json:"fact"`
|
||||
Sessions []string `json:"sessions"`
|
||||
} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "grounded_sessions":
|
||||
got := GroundedInSessions(c.Input.Fact, c.Input.Sessions)
|
||||
want := c.Expected["grounded"].(bool)
|
||||
if got != want {
|
||||
t.Errorf("%s: GroundedInSessions(%q) = %v, want %v", c.Name, c.Input.Fact, got, want)
|
||||
}
|
||||
case "coherence_short_circuit":
|
||||
got := CoherenceCheck(c.Input.Fact, []string{"sessions"}, nil, nil)
|
||||
want := c.Expected["result"].(bool)
|
||||
if got != want {
|
||||
t.Errorf("%s: CoherenceCheck short-circuit = %v, want %v", c.Name, got, want)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown batch-3 fn %q", c.Fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDistinctiveWordSemantics — the algorithm's documented edge behaviors,
|
||||
// each verified against the Python oracle during batch-3 development.
|
||||
func TestDistinctiveWordSemantics(t *testing.T) {
|
||||
s1 := "spine rebuild verified structural parity"
|
||||
s2 := "spine parity held across the rebuild"
|
||||
s3 := "unrelated session about railway timetables and tea"
|
||||
|
||||
// Hit: every significant word recurs; rarest in 2 distinct sessions.
|
||||
if !GroundedInSessions("spine rebuild parity", []string{s1, s2, s3}) {
|
||||
t.Error("recurring-word fact must ground")
|
||||
}
|
||||
// Miss: 'preserved' is rarest and absent from the corpus entirely.
|
||||
if GroundedInSessions("the spine rebuild preserved structural identity", []string{s1, s2, s3}) {
|
||||
t.Error("absent rarest word must NOT ground")
|
||||
}
|
||||
// Miss: unrelated invention.
|
||||
if GroundedInSessions("railway timetable scheduling optimization", []string{s1, s2, s3}) {
|
||||
t.Error("invention must NOT ground")
|
||||
}
|
||||
// Miss: rarest word appears in only ONE session (independence rule).
|
||||
solo := []string{"spine is discussed here in rich detail", "totally different content about tea"}
|
||||
if GroundedInSessions("spine alignment assessment", solo) {
|
||||
t.Error("single-session rarest word must NOT ground (independence)")
|
||||
}
|
||||
// Empty sessions never ground.
|
||||
if GroundedInSessions("spine rebuild parity", nil) {
|
||||
t.Error("no sessions = not grounded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
[
|
||||
{
|
||||
"fn": "decay_fresh",
|
||||
"name": "df",
|
||||
"expected": {
|
||||
"importance": 0.7,
|
||||
"journaled": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_stale",
|
||||
"name": "ds",
|
||||
"expected": {
|
||||
"importance": 0.49999999999999994,
|
||||
"journaled": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_forget",
|
||||
"name": "dg",
|
||||
"expected": {
|
||||
"removed": true,
|
||||
"op": "FORGET"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_constitution_skip",
|
||||
"name": "dc",
|
||||
"expected": {
|
||||
"importance": 0.1,
|
||||
"journaled": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "overlap",
|
||||
"name": "ov1",
|
||||
"input": {
|
||||
"a": "spine rebuild verified parity",
|
||||
"b": "spine parity verified today"
|
||||
},
|
||||
"expected": {
|
||||
"ov": 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "overlap",
|
||||
"name": "ov_disjoint",
|
||||
"input": {
|
||||
"a": "railway timetable",
|
||||
"b": "spine parity"
|
||||
},
|
||||
"expected": {
|
||||
"ov": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
// truth_batch4_test.go — batch-4 parity gate: decay semantics and token
|
||||
// overlap against the live Python oracle (truth_batch4.json, verified
|
||||
// 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch4Parity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_batch4.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_batch4.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "decay_fresh", "decay_stale", "decay_forget", "decay_constitution_skip":
|
||||
verifyDecayCase(t, c.Name, c.Fn, c.Expected)
|
||||
case "overlap":
|
||||
a, _ := c.Input["a"].(string)
|
||||
bb, _ := c.Input["b"].(string)
|
||||
got := Overlap(a, bb)
|
||||
want := c.Expected["ov"].(float64)
|
||||
if math.Abs(got-want) > 1e-9 {
|
||||
t.Errorf("%s: Overlap = %f, want %f", c.Name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func verifyDecayCase(t *testing.T, name, fn string, e map[string]interface{}) {
|
||||
t.Helper()
|
||||
now := timeNowUTC()
|
||||
mkMeta := func(daysAgo int, imp float64) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"importance": imp,
|
||||
"last_referenced": now.AddDate(0, 0, -daysAgo).Format("2006-01-02T15:04:05+00:00"),
|
||||
}
|
||||
}
|
||||
switch name {
|
||||
case "df": // fresh: untouched
|
||||
idx := Index{"operating": map[string]map[string]interface{}{"k1": mkMeta(5, 0.7)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
got := idx["operating"].(map[string]map[string]interface{})["k1"]["importance"].(float64)
|
||||
if got != 0.7 || len(jl) != 0 {
|
||||
t.Errorf("%s: importance=%v journaled=%d, want 0.7/0", name, got, len(jl))
|
||||
}
|
||||
case "ds": // stale: DECAY_RATE off
|
||||
idx := Index{"operating": map[string]map[string]interface{}{"k1": mkMeta(100, 0.7)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
got := idx["operating"].(map[string]map[string]interface{})["k1"]["importance"].(float64)
|
||||
if math.Abs(got-0.5) > 1e-9 || len(jl) != 0 {
|
||||
t.Errorf("%s: importance=%v journaled=%d, want ~0.5/0", name, got, len(jl))
|
||||
}
|
||||
case "dg": // below floor: FORGET + removed
|
||||
idx := Index{"operating": map[string]map[string]interface{}{"k1": mkMeta(100, 0.3)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
_, still := idx["operating"].(map[string]map[string]interface{})["k1"]
|
||||
removed := !still
|
||||
if removed != true || len(jl) == 0 || jl[0].Op != "FORGET" {
|
||||
t.Errorf("%s: removed=%v journaled=%d, want true/FORGET", name, removed, len(jl))
|
||||
}
|
||||
case "dc": // constitution: never decays
|
||||
idx := Index{"constitution": map[string]map[string]interface{}{"k1": mkMeta(200, 0.1)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
got := idx["constitution"].(map[string]map[string]interface{})["k1"]["importance"].(float64)
|
||||
if got != 0.1 || len(jl) != 0 {
|
||||
t.Errorf("%s: constitution decayed! importance=%v", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClusterSemantics — greedy clustering + representative summary parity.
|
||||
func TestClusterSemantics(t *testing.T) {
|
||||
entries := []string{
|
||||
"- the spine rebuild verified structural parity",
|
||||
"- spine parity held across the rebuild",
|
||||
"- unrelated note about tea",
|
||||
}
|
||||
clusters := ClusterLines(entries)
|
||||
// ORACLE-VERIFIED: ov(line0,line1)=0.429 > 0.35 → merge; the third
|
||||
// spine-ish line was moved out entirely — pairwise overlaps vs the
|
||||
// growing cluster were 0.333 < 0.35, so a 3-line cluster never forms
|
||||
// with these inputs (the greedy check is against CLUSTER MEMBERS, and
|
||||
// the threshold is strict >). line2 ("tea") is alone.
|
||||
merged := 0
|
||||
var longest []string
|
||||
for _, c := range clusters {
|
||||
if len(c) >= 2 {
|
||||
merged++
|
||||
longest = c
|
||||
}
|
||||
}
|
||||
if merged != 1 || len(longest) != 2 {
|
||||
t.Errorf("clusters: got %d merged (want 1), members %v", merged, clusters)
|
||||
}
|
||||
// Representative = longest member = the 46-char line0.
|
||||
rep := longest[0]
|
||||
for _, m := range longest {
|
||||
if len(m) > len(rep) {
|
||||
rep = m
|
||||
}
|
||||
}
|
||||
if rep != "- the spine rebuild verified structural parity" {
|
||||
t.Errorf("representative: got %q", rep)
|
||||
}
|
||||
summary := ClusterSummary(longest)
|
||||
if want := "also:"; !containsStr(summary, want) {
|
||||
t.Errorf("summary missing %q: %q", want, summary)
|
||||
}
|
||||
}
|
||||
|
||||
func containsStr(s, sub string) bool {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"oracle": "src/rune/lexicon_evolution.py (recovered provenance 0308ccc6) via curator.py",
|
||||
"generated": "2026-10-07 (post-recovery regeneration)",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "coherence",
|
||||
"name": "conflict",
|
||||
"input": {
|
||||
"fact": "nick never writes Rust code",
|
||||
"existing": [
|
||||
"nick writes Go and Rust code daily"
|
||||
],
|
||||
"corpus": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "conflict"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "coherence",
|
||||
"name": "consistent",
|
||||
"input": {
|
||||
"fact": "nick writes Rust code on weekends",
|
||||
"existing": [
|
||||
"nick writes Go and Rust code daily"
|
||||
],
|
||||
"corpus": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "consistent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "coherence",
|
||||
"name": "novel_axis",
|
||||
"input": {
|
||||
"fact": "nick plays bass guitar in a band",
|
||||
"existing": [
|
||||
"nick writes Go and Rust code daily"
|
||||
],
|
||||
"corpus": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "consistent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "coherence",
|
||||
"name": "common_word_not_relation",
|
||||
"input": {
|
||||
"fact": "nick likes black coffee",
|
||||
"existing": [
|
||||
"black holes bend light"
|
||||
],
|
||||
"corpus": [
|
||||
"black coffee in the morning",
|
||||
"black holes bend light"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "novel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "coherence",
|
||||
"name": "empty_existing",
|
||||
"input": {
|
||||
"fact": "nick writes Rust code daily",
|
||||
"existing": [],
|
||||
"corpus": []
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "novel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded",
|
||||
"name": "stem_match",
|
||||
"input": {
|
||||
"fact": "nick migrated the modules",
|
||||
"sources": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded",
|
||||
"name": "invented_token",
|
||||
"input": {
|
||||
"fact": "nick collects vintage railway timetables",
|
||||
"sources": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded",
|
||||
"name": "empty_sources",
|
||||
"input": {
|
||||
"fact": "nick migrated the modules",
|
||||
"sources": []
|
||||
},
|
||||
"expected": {
|
||||
"grounded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "distinctive_terms",
|
||||
"name": "rare_vs_common",
|
||||
"input": {
|
||||
"fact": "nick writes Rust code daily",
|
||||
"corpus": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"terms": [
|
||||
"code",
|
||||
"daily",
|
||||
"nick"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promote_eligible",
|
||||
"name": "strength_gate",
|
||||
"input": {
|
||||
"proposal": "add support for retries",
|
||||
"existing": [
|
||||
"support for retries in the fetch loop"
|
||||
],
|
||||
"strength": 2,
|
||||
"min_strength": 3,
|
||||
"corpus": []
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": false,
|
||||
"reason": "evidence strength 2 < 3"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promote_eligible",
|
||||
"name": "shared_distinctive",
|
||||
"input": {
|
||||
"proposal": "add retries to the fetch loop",
|
||||
"existing": [
|
||||
"support for retries in the fetch loop"
|
||||
],
|
||||
"strength": 3,
|
||||
"min_strength": 3,
|
||||
"corpus": []
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "shares distinctive term(s): ['fetch', 'loop', 'retries']"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promote_eligible",
|
||||
"name": "novel_grounded",
|
||||
"input": {
|
||||
"proposal": "the curator gates every memory write",
|
||||
"existing": [
|
||||
"nick writes Go and Rust code daily"
|
||||
],
|
||||
"strength": 3,
|
||||
"min_strength": 3,
|
||||
"corpus": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "novel axis grounded in corpus (5 terms)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promote_eligible",
|
||||
"name": "novel_parked",
|
||||
"input": {
|
||||
"proposal": "nick collects vintage railway timetables",
|
||||
"existing": [
|
||||
"nick writes Go and Rust code daily"
|
||||
],
|
||||
"strength": 3,
|
||||
"min_strength": 3,
|
||||
"corpus": [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "shares distinctive term(s): ['nick']"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promote_eligible",
|
||||
"name": "first_principle",
|
||||
"input": {
|
||||
"proposal": "parking is never wrong for growth",
|
||||
"existing": [],
|
||||
"strength": 3,
|
||||
"min_strength": 3,
|
||||
"corpus": []
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "empty block — first principle"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
// truth_lexicon_test.go — batch-2 lexicon battery: the REAL recovered
|
||||
// lexicon_evolution semantics (truth_lexicon.json, live Python oracle,
|
||||
// post-recovery regeneration — contract BUILDER-2026-10-07T16:31:17Z-997c9d).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLexiconParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_lexicon.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_lexicon.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
i := c.Input
|
||||
switch c.Fn {
|
||||
case "coherence":
|
||||
fact := str(i, "fact")
|
||||
existing := strList(i["existing"])
|
||||
corpus := strList(i["corpus"])
|
||||
got := Coherence(fact, existing, corpus)
|
||||
want := str(c.Expected, "verdict")
|
||||
if got != want {
|
||||
t.Errorf("%s: Coherence got %q want %q", c.Name, got, want)
|
||||
}
|
||||
case "grounded":
|
||||
fact := str(i, "fact")
|
||||
sources := strList(i["sources"])
|
||||
got := GroundedLex(fact, sources, 1)
|
||||
want := c.Expected["grounded"].(bool)
|
||||
if got != want {
|
||||
t.Errorf("%s: GroundedLex got %v want %v", c.Name, got, want)
|
||||
}
|
||||
case "distinctive_terms":
|
||||
fact := str(i, "fact")
|
||||
corpus := strList(i["corpus"])
|
||||
got := DistinctiveTerms(fact, corpus)
|
||||
wantList := c.Expected["terms"].([]interface{})
|
||||
if len(got) != len(wantList) {
|
||||
t.Errorf("%s: DistinctiveTerms got %d terms %v want %d %v",
|
||||
c.Name, len(got), got, len(wantList), wantList)
|
||||
continue
|
||||
}
|
||||
for _, w := range wantList {
|
||||
if !got[w.(string)] {
|
||||
t.Errorf("%s: missing term %q in %v", c.Name, w, got)
|
||||
}
|
||||
}
|
||||
case "promote_eligible":
|
||||
proposal := str(i, "proposal")
|
||||
existing := strList(i["existing"])
|
||||
corpus := strList(i["corpus"])
|
||||
strength := num(i, "strength")
|
||||
minS := num(i, "min_strength")
|
||||
got := PromoteEligible(proposal, existing, strength, minS, corpus)
|
||||
want, _ := c.Expected["eligible"].(map[string]interface{})
|
||||
jsonEq(t, c.Name, "eligible", got["eligible"], want["eligible"])
|
||||
jsonEq(t, c.Name, "reason", got["reason"], want["reason"])
|
||||
default:
|
||||
t.Errorf("unknown lexicon truth fn %q", c.Fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strList — interface{} (JSON array of strings) → []string.
|
||||
func strList(v interface{}) []string {
|
||||
arr, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
s, _ := e.(string)
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func TestParityAgainstPythonOracle(t *testing.T) {
|
||||
}
|
||||
case "le_promote_eligible":
|
||||
i := c.Input
|
||||
got := LePromoteEligible(str(i, "proposal"), str(i, "existing"),
|
||||
got := LePromoteEligible(str(i, "proposal"), []string{str(i, "existing")},
|
||||
num(i, "strength"), 3, num(i, "wash_count"))
|
||||
want, _ := c.Expected["eligible"].(map[string]interface{})
|
||||
jsonEq(t, c.Name, "eligible", got["eligible"], want["eligible"])
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,473 @@
|
||||
package main
|
||||
|
||||
// recall.go — batch 2: the deterministic read-path of memory_store.py
|
||||
// (contract BUILDER-2026-10-07T18:05:47Z-152c16). FtsQueryConjunctive,
|
||||
// ChunkText, RankFusion (RRF k=15), ProjectChunkRows, MapReduce,
|
||||
// FuseResults + NormKey. The embed-dependent surfaces stay Python-side.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RRFK — small-corpus RRF constant (large corpora want 60).
|
||||
const RRFK = 15
|
||||
|
||||
// msStopwords — memory_store._STOPWORDS (distinct from curator's and the
|
||||
// lexicon's sets; extracted verbatim from the live module 2026-10-07).
|
||||
var msStopwords = map[string]bool{
|
||||
"about": true, "after": true, "also": true, "and": true, "are": true,
|
||||
"been": true, "but": true, "each": true, "for": true, "from": true,
|
||||
"have": true, "her": true, "him": true, "his": true, "how": true,
|
||||
"into": true, "its": true, "just": true, "likes": true, "more": true,
|
||||
"needs": true, "not": true, "only": true, "over": true, "prefers": true,
|
||||
"some": true, "such": true, "than": true, "that": true, "the": true,
|
||||
"them": true, "then": true, "there": true, "they": true, "this": true,
|
||||
"under": true, "very": true, "wants": true, "was": true, "were": true,
|
||||
"what": true, "when": true, "where": true, "which": true, "while": true,
|
||||
"who": true, "why": true, "will": true, "with": true, "you": true,
|
||||
"your": true,
|
||||
}
|
||||
|
||||
// FtsQueryConjunctive — the CONJUNCTIVE FTS5 MATCH builder (implicit AND):
|
||||
// significant terms only (len>3, not stop; fallback len>2), quoted,
|
||||
// first 8, space-joined. Phrase clauses are additive evidence.
|
||||
func FtsQueryConjunctive(query string) string {
|
||||
lower := strings.ToLower(query)
|
||||
var toks []string
|
||||
for _, t := range strings.Fields(lower) {
|
||||
if len(t) > 3 && !msStopwords[t] {
|
||||
toks = append(toks, t)
|
||||
}
|
||||
}
|
||||
if len(toks) == 0 {
|
||||
for _, t := range strings.Fields(lower) {
|
||||
if len(t) > 2 {
|
||||
toks = append(toks, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(toks) == 0 {
|
||||
return query
|
||||
}
|
||||
if len(toks) > 8 {
|
||||
toks = toks[:8]
|
||||
}
|
||||
quoted := make([]string, len(toks))
|
||||
for i, t := range toks {
|
||||
quoted[i] = fmt.Sprintf("%q", t)
|
||||
}
|
||||
return strings.Join(quoted, " ")
|
||||
}
|
||||
|
||||
// ChunkText — paragraph recombination at ~size chars; chunks still huge
|
||||
// (no paragraph breaks) hard-split at size. Headings stay with content.
|
||||
func ChunkText(text string, size, overlap int) []string {
|
||||
if text == "" {
|
||||
return []string{}
|
||||
}
|
||||
if len(text) <= size {
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return []string{}
|
||||
}
|
||||
return []string{text}
|
||||
}
|
||||
var paras []string
|
||||
for _, p := range strings.Split(text, "\n\n") {
|
||||
if strings.TrimSpace(p) != "" {
|
||||
paras = append(paras, p)
|
||||
}
|
||||
}
|
||||
var chunks []string
|
||||
cur := ""
|
||||
for _, p := range paras {
|
||||
if len(cur)+len(p)+2 <= size {
|
||||
if cur == "" {
|
||||
cur = p
|
||||
} else {
|
||||
cur = cur + "\n\n" + p
|
||||
}
|
||||
} else {
|
||||
if cur != "" {
|
||||
chunks = append(chunks, cur)
|
||||
}
|
||||
cur = p
|
||||
}
|
||||
}
|
||||
if cur != "" {
|
||||
chunks = append(chunks, cur)
|
||||
}
|
||||
var out []string
|
||||
for _, c := range chunks {
|
||||
if len(c) > size*3/2 { // Python size * 1.5 (int text len compare)
|
||||
for i := 0; i < len(c); i += size {
|
||||
end := i + size
|
||||
if end > len(c) {
|
||||
end = len(c)
|
||||
}
|
||||
piece := c[i:end]
|
||||
if strings.TrimSpace(piece) != "" {
|
||||
out = append(out, piece)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
if out == nil {
|
||||
out = []string{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RankFusion — chunk-side RRF fusion with an explicit candidate cap.
|
||||
// dense/bm25 are [id, score] pairs. Three modes + min-max bm25 fallback.
|
||||
func RankFusion(dense, bm25 [][2]interface{}, rrfK float64) map[string]interface{} {
|
||||
if rrfK == 0 {
|
||||
rrfK = RRFK
|
||||
}
|
||||
ranks := map[string]float64{}
|
||||
var order []string // insertion order (Python dict preserves it)
|
||||
bestDense := 0.0
|
||||
mode := "lexical-only"
|
||||
addRank := func(eid string, score float64) {
|
||||
if _, has := ranks[eid]; !has {
|
||||
order = append(order, eid)
|
||||
}
|
||||
ranks[eid] += score
|
||||
}
|
||||
if len(dense) > 0 && len(bm25) > 0 {
|
||||
mode = "hybrid"
|
||||
for idx, list := range [][][2]interface{}{dense, bm25} {
|
||||
for pos, pair := range list {
|
||||
eid := pairString(pair[0])
|
||||
score, _ := pair[1].(float64)
|
||||
addRank(eid, 1.0/(rrfK+float64(pos)+1))
|
||||
if idx == 0 && score > bestDense {
|
||||
bestDense = score
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if len(dense) > 0 {
|
||||
mode = "dense-only"
|
||||
for pos, pair := range dense {
|
||||
addRank(pairString(pair[0]), 1.0/(rrfK+float64(pos)+1))
|
||||
if score, ok := pair[1].(float64); ok && score > bestDense {
|
||||
bestDense = score
|
||||
}
|
||||
}
|
||||
} else if len(bm25) > 0 {
|
||||
minBm, maxBm := 0.0, 0.0
|
||||
first := true
|
||||
for _, pair := range bm25 {
|
||||
s, _ := pair[1].(float64)
|
||||
if first {
|
||||
minBm, maxBm = s, s
|
||||
first = false
|
||||
}
|
||||
if s < minBm {
|
||||
minBm = s
|
||||
}
|
||||
if s > maxBm {
|
||||
maxBm = s
|
||||
}
|
||||
}
|
||||
span := maxBm - minBm
|
||||
if span < 1e-9 {
|
||||
span = 1e-9
|
||||
}
|
||||
for _, pair := range bm25 {
|
||||
s, _ := pair[1].(float64)
|
||||
addRank(pairString(pair[0]), (s-minBm)/span)
|
||||
}
|
||||
}
|
||||
rankList := make([]interface{}, 0, len(order))
|
||||
for _, eid := range order {
|
||||
rankList = append(rankList, []interface{}{numericID(eid), ranks[eid]})
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"ranks": rankList,
|
||||
"best_dense": bestDense,
|
||||
"mode": mode,
|
||||
}
|
||||
}
|
||||
|
||||
// numericID — Python ids in the fusion pairs are ints; the string-keyed
|
||||
// map is internal only. Emit numeric when the id parses as an integer.
|
||||
func numericID(eid string) interface{} {
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(eid, "%d", &n); err == nil && trimFloat(float64(n)) == eid {
|
||||
return n
|
||||
}
|
||||
return eid
|
||||
}
|
||||
|
||||
func pairString(v interface{}) string {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return x
|
||||
case float64:
|
||||
return trimFloat(x)
|
||||
case int:
|
||||
return fmt.Sprintf("%d", x)
|
||||
default:
|
||||
return fmt.Sprintf("%v", x)
|
||||
}
|
||||
}
|
||||
|
||||
func trimFloat(f float64) string {
|
||||
if f == float64(int64(f)) {
|
||||
return fmt.Sprintf("%d", int64(f))
|
||||
}
|
||||
return fmt.Sprintf("%g", f)
|
||||
}
|
||||
|
||||
// ChunkRow — one projected chunk row.
|
||||
type ChunkRow struct {
|
||||
ChunkID interface{} `json:"chunk_id"`
|
||||
Text string `json:"text"`
|
||||
Wing string `json:"wing"`
|
||||
Room string `json:"room"`
|
||||
Source string `json:"source"`
|
||||
Page interface{} `json:"page"`
|
||||
RRF float64 `json:"rrf"`
|
||||
}
|
||||
|
||||
// ProjectChunkRows — sorted -rrf, text[:400], rrf round 4, budget cap.
|
||||
func ProjectChunkRows(ranks map[string]float64, rows []map[string]interface{}, budget int) []map[string]interface{} {
|
||||
byID := map[string]map[string]interface{}{}
|
||||
for _, r := range rows {
|
||||
byID[pairString(r["rowid"])] = r
|
||||
}
|
||||
// sorted(ranks.items(), key=lambda x: -x[1]) — descending rrf; ties
|
||||
// keep Python's stable sort order (insertion order of ranks dict).
|
||||
type kv struct {
|
||||
k string
|
||||
v float64
|
||||
}
|
||||
var items []kv
|
||||
for k, v := range ranks {
|
||||
items = append(items, kv{k, v})
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool { return items[i].v > items[j].v })
|
||||
out := []map[string]interface{}{}
|
||||
for _, it := range items {
|
||||
r, has := byID[it.k]
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
text, _ := r["text"].(string)
|
||||
if len(text) > 400 {
|
||||
text = text[:400]
|
||||
}
|
||||
wing, _ := r["wing"].(string)
|
||||
room, _ := r["room"].(string)
|
||||
source, _ := r["source_path"].(string)
|
||||
var page interface{}
|
||||
if p, hasPage := r["page"]; hasPage {
|
||||
page = p
|
||||
} else {
|
||||
page = nil
|
||||
}
|
||||
out = append(out, map[string]interface{}{
|
||||
"chunk_id": r["chunk_id"],
|
||||
"text": text,
|
||||
"wing": wing,
|
||||
"room": room,
|
||||
"source": source,
|
||||
"page": page,
|
||||
"rrf": round4(it.v),
|
||||
})
|
||||
if len(out) >= budget {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func round4(f float64) float64 {
|
||||
return float64(int64(f*10000+0.5)) / 10000
|
||||
}
|
||||
|
||||
// MapReduce — chunk the entries, take top-3 per chunk by
|
||||
// relevance/score (default 0.5), then REDUCE: re-sort ALL mapped entries
|
||||
// by score desc (the oracle's final pass; no budget param in the code
|
||||
// despite the comment).
|
||||
func MapReduce(entries []map[string]interface{}, chunkSize int) []map[string]interface{} {
|
||||
if len(entries) <= chunkSize {
|
||||
return entries
|
||||
}
|
||||
var mapped []map[string]interface{}
|
||||
for start := 0; start < len(entries); start += chunkSize {
|
||||
end := start + chunkSize
|
||||
if end > len(entries) {
|
||||
end = len(entries)
|
||||
}
|
||||
chunk := entries[start:end]
|
||||
type scored struct {
|
||||
score float64
|
||||
entry map[string]interface{}
|
||||
}
|
||||
var sortd []scored
|
||||
for _, e := range chunk {
|
||||
score := 0.5
|
||||
if v, has := e["relevance"]; has {
|
||||
if f, ok := v.(float64); ok {
|
||||
score = f
|
||||
}
|
||||
} else if v, has := e["score"]; has {
|
||||
if f, ok := v.(float64); ok {
|
||||
score = f
|
||||
}
|
||||
}
|
||||
sortd = append(sortd, scored{score, e})
|
||||
}
|
||||
// Python sort is stable; ties keep list order.
|
||||
sort.SliceStable(sortd, func(i, j int) bool { return sortd[i].score > sortd[j].score })
|
||||
n := 3
|
||||
if len(sortd) < n {
|
||||
n = len(sortd)
|
||||
}
|
||||
for _, s := range sortd[:n] {
|
||||
mapped = append(mapped, s.entry)
|
||||
}
|
||||
}
|
||||
if mapped == nil {
|
||||
mapped = []map[string]interface{}{}
|
||||
}
|
||||
// Reduce: sort all mapped by score desc (stable; ties keep map order).
|
||||
sort.SliceStable(mapped, func(i, j int) bool {
|
||||
return entryScore(mapped[i]) > entryScore(mapped[j])
|
||||
})
|
||||
return mapped
|
||||
}
|
||||
|
||||
// entryScore — relevance, else score, else 0.5 (the oracle's lookup).
|
||||
func entryScore(e map[string]interface{}) float64 {
|
||||
if v, has := e["relevance"]; has {
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
}
|
||||
if v, has := e["score"]; has {
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return 0.5
|
||||
}
|
||||
|
||||
// NormKey — cross-table dedup normalization: lowercase, whitespace-collapse.
|
||||
func NormKey(text string) string {
|
||||
return strings.Join(strings.Fields(strings.ToLower(text)), " ")
|
||||
}
|
||||
|
||||
// FuseResults — _norm_key dedup + merge shape + kind-wise min-max
|
||||
// scale-alignment (the audit fix) + final sort.
|
||||
func FuseResults(entries, research []map[string]interface{}) []map[string]interface{} {
|
||||
seen := map[string]bool{}
|
||||
merged := []map[string]interface{}{}
|
||||
for _, e := range entries {
|
||||
text, _ := e["text"].(string)
|
||||
key := NormKey(text)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
topic, _ := e["topic"].(string)
|
||||
if topic == "" {
|
||||
topic = "fact"
|
||||
}
|
||||
relevance, _ := e["relevance"].(float64)
|
||||
confidence, _ := e["confidence"].(float64)
|
||||
merged = append(merged, map[string]interface{}{
|
||||
"id": e["id"],
|
||||
"text": text,
|
||||
"topic": topic,
|
||||
"kind": "fact",
|
||||
"relevance": round4(relevance),
|
||||
"confidence": round2(confidence),
|
||||
})
|
||||
}
|
||||
for _, c := range research {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
text, _ := c["text"].(string)
|
||||
key := NormKey(text)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
topic, _ := c["wing"].(string)
|
||||
if topic == "" {
|
||||
topic = "research"
|
||||
}
|
||||
rel, hasRel := c["rrf"].(float64)
|
||||
if !hasRel {
|
||||
rel, _ = c["score"].(float64)
|
||||
}
|
||||
confidence, hasConf := c["confidence"].(float64)
|
||||
if !hasConf || confidence == 0 {
|
||||
confidence = 0.7
|
||||
}
|
||||
merged = append(merged, map[string]interface{}{
|
||||
"id": c["chunk_id"],
|
||||
"text": text,
|
||||
"topic": topic,
|
||||
"kind": "research",
|
||||
"relevance": round4(rel),
|
||||
"confidence": round2(confidence),
|
||||
})
|
||||
}
|
||||
// final sort before alignment (Python sorts, then aligns, then sorts)
|
||||
sortByRelevanceDesc(merged)
|
||||
// SCALE-ALIGNMENT: min-max normalize each kind's relevance to [0,1]
|
||||
// within this result set, then re-sort.
|
||||
for _, kind := range []string{"fact", "research"} {
|
||||
var subset []map[string]interface{}
|
||||
for _, r := range merged {
|
||||
if r["kind"] == kind {
|
||||
subset = append(subset, r)
|
||||
}
|
||||
}
|
||||
if len(subset) > 1 {
|
||||
lo, hi := 0.0, 0.0
|
||||
first := true
|
||||
for _, r := range subset {
|
||||
v := r["relevance"].(float64)
|
||||
if first {
|
||||
lo, hi = v, v
|
||||
first = false
|
||||
}
|
||||
if v < lo {
|
||||
lo = v
|
||||
}
|
||||
if v > hi {
|
||||
hi = v
|
||||
}
|
||||
}
|
||||
span := hi - lo
|
||||
if span < 1e-9 {
|
||||
span = 1e-9
|
||||
}
|
||||
for _, r := range subset {
|
||||
v := r["relevance"].(float64)
|
||||
r["relevance"] = round4((v - lo) / span)
|
||||
}
|
||||
}
|
||||
}
|
||||
sortByRelevanceDesc(merged)
|
||||
return merged
|
||||
}
|
||||
|
||||
func sortByRelevanceDesc(list []map[string]interface{}) {
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i]["relevance"].(float64) > list[j]["relevance"].(float64)
|
||||
})
|
||||
}
|
||||
|
||||
func round2(f float64) float64 {
|
||||
return float64(int64(f*100+0.5)) / 100
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
{
|
||||
"oracle": "src/rune/memory_store.py _py_* oracles",
|
||||
"generated": "2026-10-07",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "fts",
|
||||
"name": "significant_terms",
|
||||
"input": {
|
||||
"query": "the curator gates memory writes"
|
||||
},
|
||||
"expected": {
|
||||
"q": "\"curator\" \"gates\" \"memory\" \"writes\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fts",
|
||||
"name": "stopwords_dropped",
|
||||
"input": {
|
||||
"query": "the and was with nick"
|
||||
},
|
||||
"expected": {
|
||||
"q": "\"nick\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fts",
|
||||
"name": "short_fallback",
|
||||
"input": {
|
||||
"query": "go is fun ai"
|
||||
},
|
||||
"expected": {
|
||||
"q": "\"fun\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fts",
|
||||
"name": "cap_eight",
|
||||
"input": {
|
||||
"query": "alpha beta gamma delta epsilon zeta eta theta iota kappa"
|
||||
},
|
||||
"expected": {
|
||||
"q": "\"alpha\" \"beta\" \"gamma\" \"delta\" \"epsilon\" \"zeta\" \"theta\" \"iota\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fts",
|
||||
"name": "empty_returns_raw",
|
||||
"input": {
|
||||
"query": "a an of"
|
||||
},
|
||||
"expected": {
|
||||
"q": "a an of"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "chunk",
|
||||
"name": "under_size",
|
||||
"input": {
|
||||
"text": "one paragraph only",
|
||||
"size": 1500,
|
||||
"overlap": 150
|
||||
},
|
||||
"expected": {
|
||||
"chunks": [
|
||||
"one paragraph only"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "chunk",
|
||||
"name": "recombine",
|
||||
"input": {
|
||||
"text": "paragraph 0 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 5 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 6 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 7 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 8 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 9 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 10 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 11 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 12 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 13 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 14 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 15 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 16 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 17 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 18 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 19 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 20 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 21 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 22 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 23 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 24 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 25 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 26 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 27 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 28 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 29 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"size": 400,
|
||||
"overlap": 150
|
||||
},
|
||||
"expected": {
|
||||
"chunks": [
|
||||
"paragraph 0 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 5 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 6 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"paragraph 7 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 8 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 9 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 10 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 11 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 12 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 13 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"paragraph 14 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 15 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 16 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 17 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 18 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 19 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 20 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"paragraph 21 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 22 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 23 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 24 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 25 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 26 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 27 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"paragraph 28 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nparagraph 29 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "chunk",
|
||||
"name": "hard_split",
|
||||
"input": {
|
||||
"text": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
"size": 1000,
|
||||
"overlap": 150
|
||||
},
|
||||
"expected": {
|
||||
"chunks": [
|
||||
"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "chunk",
|
||||
"name": "empty",
|
||||
"input": {
|
||||
"text": "",
|
||||
"size": 1500,
|
||||
"overlap": 150
|
||||
},
|
||||
"expected": {
|
||||
"chunks": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fuse_ranks",
|
||||
"name": "hybrid",
|
||||
"input": {
|
||||
"dense": [
|
||||
[
|
||||
1,
|
||||
0.9
|
||||
],
|
||||
[
|
||||
2,
|
||||
0.8
|
||||
],
|
||||
[
|
||||
3,
|
||||
0.7
|
||||
]
|
||||
],
|
||||
"bm25": [
|
||||
[
|
||||
2,
|
||||
12.0
|
||||
],
|
||||
[
|
||||
4,
|
||||
8.0
|
||||
],
|
||||
[
|
||||
1,
|
||||
3.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"ranks": [
|
||||
[
|
||||
1,
|
||||
0.11805555555555555
|
||||
],
|
||||
[
|
||||
2,
|
||||
0.1213235294117647
|
||||
],
|
||||
[
|
||||
3,
|
||||
0.05555555555555555
|
||||
],
|
||||
[
|
||||
4,
|
||||
0.058823529411764705
|
||||
]
|
||||
],
|
||||
"best_dense": 0.9,
|
||||
"mode": "hybrid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fuse_ranks",
|
||||
"name": "dense_only",
|
||||
"input": {
|
||||
"dense": [
|
||||
[
|
||||
1,
|
||||
0.9
|
||||
],
|
||||
[
|
||||
2,
|
||||
0.8
|
||||
],
|
||||
[
|
||||
3,
|
||||
0.7
|
||||
]
|
||||
],
|
||||
"bm25": []
|
||||
},
|
||||
"expected": {
|
||||
"ranks": [
|
||||
[
|
||||
1,
|
||||
0.0625
|
||||
],
|
||||
[
|
||||
2,
|
||||
0.058823529411764705
|
||||
],
|
||||
[
|
||||
3,
|
||||
0.05555555555555555
|
||||
]
|
||||
],
|
||||
"best_dense": 0.9,
|
||||
"mode": "dense-only"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fuse_ranks",
|
||||
"name": "lexical_only",
|
||||
"input": {
|
||||
"dense": [],
|
||||
"bm25": [
|
||||
[
|
||||
2,
|
||||
12.0
|
||||
],
|
||||
[
|
||||
4,
|
||||
8.0
|
||||
],
|
||||
[
|
||||
1,
|
||||
3.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"ranks": [
|
||||
[
|
||||
2,
|
||||
1.0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0.5555555555555556
|
||||
],
|
||||
[
|
||||
1,
|
||||
0.0
|
||||
]
|
||||
],
|
||||
"best_dense": 0.0,
|
||||
"mode": "lexical-only"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fuse_ranks",
|
||||
"name": "neither",
|
||||
"input": {
|
||||
"dense": [],
|
||||
"bm25": []
|
||||
},
|
||||
"expected": {
|
||||
"ranks": [],
|
||||
"best_dense": 0.0,
|
||||
"mode": "lexical-only"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "project",
|
||||
"name": "budget_and_shape",
|
||||
"input": {
|
||||
"ranks": {
|
||||
"2": 0.0625,
|
||||
"1": 0.05
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"rows": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "map_reduce",
|
||||
"name": "top3_per_chunk",
|
||||
"input": {
|
||||
"entries": [
|
||||
{
|
||||
"id": 0,
|
||||
"relevance": 0.0
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"relevance": 0.1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"relevance": 0.2
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"relevance": 0.30000000000000004
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"relevance": 0.4
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"relevance": 0.5
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"relevance": 0.6000000000000001
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"relevance": 0.7000000000000001
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"relevance": 0.8
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"relevance": 0.9
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"relevance": 1.0
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"relevance": 1.1
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"relevance": 1.2000000000000002
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"relevance": 1.3
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"relevance": 1.4000000000000001
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"relevance": 1.5
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"relevance": 1.6
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"relevance": 1.7000000000000002
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"relevance": 1.8
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"relevance": 1.9000000000000001
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"relevance": 2.0
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"relevance": 2.1
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"relevance": 2.2
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"relevance": 2.3000000000000003
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"relevance": 2.4000000000000004
|
||||
}
|
||||
],
|
||||
"chunk_size": 10
|
||||
},
|
||||
"expected": {
|
||||
"mapped": [
|
||||
{
|
||||
"id": 24,
|
||||
"relevance": 2.4000000000000004
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"relevance": 2.3000000000000003
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"relevance": 2.2
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"relevance": 1.9000000000000001
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"relevance": 1.8
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"relevance": 1.7000000000000002
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"relevance": 0.9
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"relevance": 0.8
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"relevance": 0.7000000000000001
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "map_reduce",
|
||||
"name": "under_threshold",
|
||||
"input": {
|
||||
"entries": [
|
||||
{
|
||||
"id": 0,
|
||||
"relevance": 0.0
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"relevance": 0.1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"relevance": 0.2
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"relevance": 0.30000000000000004
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"relevance": 0.4
|
||||
}
|
||||
],
|
||||
"chunk_size": 10
|
||||
},
|
||||
"expected": {
|
||||
"mapped": [
|
||||
{
|
||||
"id": 0,
|
||||
"relevance": 0.0
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"relevance": 0.1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"relevance": 0.2
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"relevance": 0.30000000000000004
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"relevance": 0.4
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fuse_results",
|
||||
"name": "dedup_scale_align",
|
||||
"input": {
|
||||
"entries": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "nick prefers coffee",
|
||||
"topic": "human",
|
||||
"relevance": 2.5,
|
||||
"confidence": 0.9
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"text": "NICK PREFERS COFFEE",
|
||||
"topic": "human",
|
||||
"relevance": 1.1,
|
||||
"confidence": 0.8
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"text": "rune gates memory writes",
|
||||
"topic": "project",
|
||||
"relevance": 5.6,
|
||||
"confidence": 0.95
|
||||
}
|
||||
],
|
||||
"research": [
|
||||
{
|
||||
"chunk_id": "c1",
|
||||
"text": "the spine rebuild parity",
|
||||
"wing": "research",
|
||||
"rrf": 0.0625,
|
||||
"confidence": 0.7
|
||||
},
|
||||
{
|
||||
"chunk_id": "c2",
|
||||
"text": "curator provenance recovery",
|
||||
"wing": "research",
|
||||
"rrf": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"merged": [
|
||||
{
|
||||
"id": 3,
|
||||
"text": "rune gates memory writes",
|
||||
"topic": "project",
|
||||
"kind": "fact",
|
||||
"relevance": 1.0,
|
||||
"confidence": 0.95
|
||||
},
|
||||
{
|
||||
"id": "c1",
|
||||
"text": "the spine rebuild parity",
|
||||
"topic": "research",
|
||||
"kind": "research",
|
||||
"relevance": 1.0,
|
||||
"confidence": 0.7
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"text": "nick prefers coffee",
|
||||
"topic": "human",
|
||||
"kind": "fact",
|
||||
"relevance": 0.0,
|
||||
"confidence": 0.9
|
||||
},
|
||||
{
|
||||
"id": "c2",
|
||||
"text": "curator provenance recovery",
|
||||
"topic": "research",
|
||||
"kind": "research",
|
||||
"relevance": 0.0,
|
||||
"confidence": 0.7
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
// truth_recall_test.go — memory-store batch 2 parity gate: the read-path
|
||||
// replays truth_recall.json (live Python _py_* oracles — contract
|
||||
// BUILDER-2026-10-07T18:05:47Z-152c16).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRecallParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_recall.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_recall.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "fts":
|
||||
q, _ := c.Input["query"].(string)
|
||||
got := FtsQueryConjunctive(q)
|
||||
want, _ := c.Expected["q"].(string)
|
||||
if got != want {
|
||||
t.Errorf("%s: fts\n got %q\nwant %q", c.Name, got, want)
|
||||
}
|
||||
case "chunk":
|
||||
text, _ := c.Input["text"].(string)
|
||||
size := int(c.Input["size"].(float64))
|
||||
overlap := int(c.Input["overlap"].(float64))
|
||||
got := ChunkText(text, size, overlap)
|
||||
wantArr, _ := c.Expected["chunks"].([]interface{})
|
||||
if len(got) != len(wantArr) {
|
||||
t.Errorf("%s: chunks = %d, want %d", c.Name, len(got), len(wantArr))
|
||||
continue
|
||||
}
|
||||
for i, w := range wantArr {
|
||||
want, _ := w.(string)
|
||||
if got[i] != want {
|
||||
t.Errorf("%s: chunk[%d]\n got %q\nwant %q", c.Name, i, got[i], want)
|
||||
}
|
||||
}
|
||||
case "fuse_ranks":
|
||||
dense := pairList(c.Input["dense"])
|
||||
bm25 := pairList(c.Input["bm25"])
|
||||
got := RankFusion(dense, bm25, RRFK)
|
||||
want := c.Expected
|
||||
jsonEq(t, c.Name, "mode", got["mode"], want["mode"])
|
||||
jsonEq(t, c.Name, "ranks", got["ranks"], want["ranks"])
|
||||
jsonEq(t, c.Name, "best_dense", got["best_dense"], want["best_dense"])
|
||||
case "project":
|
||||
ranksAny, _ := c.Input["ranks"].(map[string]interface{})
|
||||
ranks := map[string]float64{}
|
||||
for k, v := range ranksAny {
|
||||
f, _ := v.(float64)
|
||||
ranks[k] = f
|
||||
}
|
||||
// rows ship in the case input under "rows" (the truth ships its
|
||||
// own fixtures — same discipline as scaffold batches).
|
||||
rowsAny, _ := c.Input["rows"].([]interface{})
|
||||
var rows []map[string]interface{}
|
||||
for _, r := range rowsAny {
|
||||
m, _ := r.(map[string]interface{})
|
||||
rows = append(rows, m)
|
||||
}
|
||||
got := ProjectChunkRows(ranks, rows, 6)
|
||||
wantArr, _ := c.Expected["rows"].([]interface{})
|
||||
jsonEq(t, c.Name, "rows", got, wantArr)
|
||||
case "map_reduce":
|
||||
entries := objList(c.Input["entries"])
|
||||
chunkSize := int(c.Input["chunk_size"].(float64))
|
||||
got := MapReduce(entries, chunkSize)
|
||||
wantArr, _ := c.Expected["mapped"].([]interface{})
|
||||
jsonEq(t, c.Name, "mapped", got, wantArr)
|
||||
case "fuse_results":
|
||||
entries := objList(c.Input["entries"])
|
||||
research := objList(c.Input["research"])
|
||||
got := FuseResults(entries, research)
|
||||
wantArr, _ := c.Expected["merged"].([]interface{})
|
||||
jsonEq(t, c.Name, "merged", got, wantArr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pairList — [[id, score], ...] from decoded JSON.
|
||||
func pairList(v interface{}) [][2]interface{} {
|
||||
arr, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([][2]interface{}, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
pair, ok2 := e.([]interface{})
|
||||
if !ok2 || len(pair) != 2 {
|
||||
continue
|
||||
}
|
||||
out = append(out, [2]interface{}{pair[0], pair[1]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func objList(v interface{}) []map[string]interface{} {
|
||||
arr, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]interface{}, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
m, _ := e.(map[string]interface{})
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// jsonEq — canonical-JSON comparison (Go typed values vs decoded JSON).
|
||||
func jsonEq(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(want)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: %s divergence:\n got %s\nwant %s", name, field, gb, wb)
|
||||
}
|
||||
}
|
||||
+21
-4
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -34,20 +35,36 @@ func main() {
|
||||
route = "VERIFICATION"
|
||||
}
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"route": route,
|
||||
"route": route,
|
||||
"objective": strings.TrimSpace(text),
|
||||
"source": "go-scaffold",
|
||||
"source": "go-scaffold",
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "status":
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"runtime": "go",
|
||||
"survivor": true,
|
||||
"runtime": "go",
|
||||
"survivor": true,
|
||||
"rust_kernels": "linked via staticlib",
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "validate_transition":
|
||||
// batch 1 (contract BUILDER-2026-10-07T17:27:25Z-6bc737): the
|
||||
// 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)
|
||||
os.Exit(1)
|
||||
}
|
||||
c, _ := req["current"].(string)
|
||||
to, _ := req["to"].(string)
|
||||
if err := ValidateTransition(c, to); err != nil {
|
||||
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
} else {
|
||||
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{"ok": true})
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,467 @@
|
||||
package main
|
||||
|
||||
// scaffold.go — batch 1: the DETERMINISTIC contract-lifecycle core of
|
||||
// src/rune/scaffold.py (contract BUILDER-2026-10-07T17:27:25Z-6bc737).
|
||||
// The 9-phase STORE machine (distinct from the 16-edge contract machine
|
||||
// frozen in the Rust construction_interface membrane — two layers, each
|
||||
// authoritative at its own boundary), NewContract/TransitionContract
|
||||
// mechanics, CheckScope, LayoutObligations, json_frontier. Clock INJECTED.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Phases — the 9-phase store machine, declaration order.
|
||||
var Phases = []string{
|
||||
"draft", "approved", "building", "verifying",
|
||||
"migrating", "superseding", "closed", "failed", "abandoned",
|
||||
}
|
||||
|
||||
// LegalTransitions — the canonical map (22 legal edges). Authority: the
|
||||
// consumers must not re-declare their own transition tables.
|
||||
var LegalTransitions = map[string][]string{
|
||||
"draft": {"approved", "abandoned"},
|
||||
"approved": {"building", "abandoned"},
|
||||
"building": {"verifying", "approved", "abandoned", "failed"},
|
||||
"verifying": {"migrating", "building", "failed", "abandoned", "closed"},
|
||||
"migrating": {"superseding", "verifying", "failed", "abandoned"},
|
||||
"superseding": {"closed", "failed", "abandoned"},
|
||||
"closed": {},
|
||||
"failed": {"building", "abandoned"}, // retry path
|
||||
"abandoned": {},
|
||||
}
|
||||
|
||||
// TerminalPhases — DERIVED from the map (no successor = terminal).
|
||||
func TerminalPhases() map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for p, next := range LegalTransitions {
|
||||
if len(next) == 0 {
|
||||
out[p] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Statuses / repo constants.
|
||||
var (
|
||||
Statues = []string{"open", "closed", "failed", "abandoned"}
|
||||
RepoPlacements = []string{"RUNE_REPO", "EXISTING_PROJECT_REPO", "NEW_MANAGED_REPO"}
|
||||
RepoSecretFields = []string{"token", "api_key", "password", "secret", "auth_header", "credential", "api_token"}
|
||||
)
|
||||
|
||||
// NextAction — the _NEXT_ACTION map.
|
||||
var NextAction = map[string]string{
|
||||
"draft": "review for approval",
|
||||
"approved": "begin building",
|
||||
"building": "complete implementation, then verify",
|
||||
"verifying": "run verification gates; resolve failures",
|
||||
"migrating": "migrate consumers/connectors",
|
||||
"superseding": "supersede old implementation; retain rollback",
|
||||
"closed": "observe real outcome; capture lessons",
|
||||
"failed": "diagnose; retry via building or abandon",
|
||||
"abandoned": "",
|
||||
}
|
||||
|
||||
// IsLegalTransition — membership in the map.
|
||||
func IsLegalTransition(from, to string) bool {
|
||||
for _, t := range LegalTransitions[from] {
|
||||
if t == to {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateTransition — the exact Python error texts:
|
||||
//
|
||||
// "unknown phase: {phase}"
|
||||
// "illegal transition: {cur} → {to} (legal: [...])" (sorted list repr)
|
||||
func ValidateTransition(current, to string) error {
|
||||
if _, known := LegalTransitions[to]; !known && !phaseKnown(to) {
|
||||
return fmt.Errorf("unknown phase: %s", to)
|
||||
}
|
||||
if !IsLegalTransition(current, to) {
|
||||
legal := append([]string{}, LegalTransitions[current]...)
|
||||
sort.Strings(legal)
|
||||
// Python list repr: ['a', 'b']
|
||||
parts := make([]string, len(legal))
|
||||
for i, l := range legal {
|
||||
parts[i] = "'" + l + "'"
|
||||
}
|
||||
return fmt.Errorf("illegal transition: %s → %s (legal: [%s])",
|
||||
current, to, strings.Join(parts, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func phaseKnown(p string) bool {
|
||||
for _, ph := range Phases {
|
||||
if ph == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContractEntity — the contract entity shape (spine entity + lifecycle
|
||||
// fields). Identities carry the sorted-list discipline (Python sorted()).
|
||||
type ContractEntity struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Identities map[string]interface{} `json:"identities"`
|
||||
Maturity string `json:"maturity"`
|
||||
EvidenceRefs []string `json:"evidence_refs"`
|
||||
Phase string `json:"phase"`
|
||||
Status string `json:"status"`
|
||||
Checkpoints []map[string]interface{} `json:"checkpoints"`
|
||||
ObservedAt string `json:"observed_at"`
|
||||
}
|
||||
|
||||
// NewContract — new_contract: the identity dict (sorted lists), draft/open,
|
||||
// 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) {
|
||||
key := "contract:" + contractID
|
||||
if existing, ok := doc["entities"].([]interface{}); ok {
|
||||
for _, raw := range existing {
|
||||
if e, isMap := raw.(map[string]interface{}); isMap && e["id"] == key {
|
||||
return nil, fmt.Errorf("contract already exists: %s", contractID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if identities == nil {
|
||||
identities = map[string]interface{}{}
|
||||
}
|
||||
// Defaults per the oracle; caller-provided values win.
|
||||
setDefault(identities, "need", need)
|
||||
setDefault(identities, "origin", origin)
|
||||
setDefault(identities, "scope", []string{})
|
||||
setDefault(identities, "requirement", "")
|
||||
setDefault(identities, "gap_evidence", "")
|
||||
setDefault(identities, "research_refs", []string{})
|
||||
setDefault(identities, "resolution", "")
|
||||
setDefault(identities, "toolchain", map[string]interface{}{})
|
||||
setDefault(identities, "affected_entities", []string{})
|
||||
setDefault(identities, "expected_consumers", []string{})
|
||||
setDefault(identities, "expected_new_entities", []string{})
|
||||
setDefault(identities, "regression_obligations", []string{})
|
||||
setDefault(identities, "verification_obligations", []string{})
|
||||
setDefault(identities, "migration_obligations", []string{})
|
||||
setDefault(identities, "rollback_ref", "")
|
||||
setDefault(identities, "acceptance", []string{})
|
||||
setDefault(identities, "non_goals", []string{})
|
||||
setDefault(identities, "next_action", NextAction["draft"])
|
||||
e := &ContractEntity{
|
||||
ID: key,
|
||||
Kind: "construction_contract",
|
||||
Identities: identities,
|
||||
Maturity: "DECLARED",
|
||||
EvidenceRefs: []string{"contract-created:" + contractID},
|
||||
Phase: "draft",
|
||||
Status: "open",
|
||||
Checkpoints: []map[string]interface{}{},
|
||||
}
|
||||
// PARITY: Python new_contract calls add_entity — the doc MUTATES.
|
||||
if ents, ok := doc["entities"].([]interface{}); ok {
|
||||
eb, _ := json.Marshal(e)
|
||||
var em map[string]interface{}
|
||||
_ = json.Unmarshal(eb, &em)
|
||||
doc["entities"] = append(ents, em)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func setDefault(m map[string]interface{}, k string, v interface{}) {
|
||||
if _, has := m[k]; !has {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// TransitionContract — transition_contract: validate, mutate phase/status,
|
||||
// append the checkpoint {phase, at, evidence, note[:200]}, update
|
||||
// next_action, stamp observed_at.
|
||||
func TransitionContract(e *ContractEntity, phase, evidenceRef, note, nowStr string) error {
|
||||
current := e.Phase
|
||||
if current == "" {
|
||||
current = "draft"
|
||||
}
|
||||
if err := ValidateTransition(current, phase); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Phase = phase
|
||||
if phase == "closed" {
|
||||
e.Status = "closed"
|
||||
} else if phase == "failed" {
|
||||
e.Status = "failed"
|
||||
}
|
||||
trimmed := note
|
||||
if len(trimmed) > 200 {
|
||||
trimmed = trimmed[:200]
|
||||
}
|
||||
if e.Checkpoints == nil {
|
||||
e.Checkpoints = []map[string]interface{}{}
|
||||
}
|
||||
e.Checkpoints = append(e.Checkpoints, map[string]interface{}{
|
||||
"phase": phase,
|
||||
"at": nowStr,
|
||||
"evidence": evidenceRef,
|
||||
"note": trimmed,
|
||||
})
|
||||
e.Identities["next_action"] = NextAction[phase]
|
||||
e.ObservedAt = nowStr
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckScope — the mechanical scope check. UNKNOWN on unscoped (never
|
||||
// silently green). Matching: f == s, f has prefix s+"/", or s in f.
|
||||
func CheckScope(e *ContractEntity, changedFiles []string) map[string]interface{} {
|
||||
rawScope, _ := e.Identities["scope"].([]interface{})
|
||||
var scope []string
|
||||
for _, s := range rawScope {
|
||||
if v, ok := s.(string); ok {
|
||||
scope = append(scope, v)
|
||||
}
|
||||
}
|
||||
if len(scope) == 0 {
|
||||
sorted := append([]string{}, changedFiles...)
|
||||
sort.Strings(sorted)
|
||||
return map[string]interface{}{
|
||||
"verdict": "unknown",
|
||||
"in_scope": []string{},
|
||||
"out_of_scope": sorted,
|
||||
"reason": "contract has no approved scope",
|
||||
}
|
||||
}
|
||||
var inScope, outScope []string
|
||||
for _, f := range changedFiles {
|
||||
ok := false
|
||||
for _, s := range scope {
|
||||
if f == s || strings.HasPrefix(f, strings.TrimRight(s, "/")+"/") || strings.Contains(f, s) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
inScope = append(inScope, f)
|
||||
} else {
|
||||
outScope = append(outScope, f)
|
||||
}
|
||||
}
|
||||
if inScope == nil {
|
||||
inScope = []string{}
|
||||
}
|
||||
if outScope == nil {
|
||||
outScope = []string{}
|
||||
}
|
||||
sort.Strings(inScope)
|
||||
sort.Strings(outScope)
|
||||
verdict := "green"
|
||||
if len(outScope) > 0 {
|
||||
verdict = "red"
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"verdict": verdict,
|
||||
"in_scope": inScope,
|
||||
"out_of_scope": outScope,
|
||||
}
|
||||
}
|
||||
|
||||
// LayoutObligations — the FLEXIBLE/USER-LOCKED obligation emitter over
|
||||
// layout_contract entities. impl_hit: any affected path ending ".py" that
|
||||
// impl startsWith or is contained in impl.
|
||||
func LayoutObligations(doc map[string]interface{}, affected []string) []map[string]interface{} {
|
||||
affectedSet := map[string]bool{}
|
||||
for _, a := range affected {
|
||||
affectedSet[a] = true
|
||||
}
|
||||
var obligations []map[string]interface{}
|
||||
entities, _ := doc["entities"].([]interface{})
|
||||
_ = entities
|
||||
obligations = []map[string]interface{}{}
|
||||
for _, raw := range entities {
|
||||
e, isMap := raw.(map[string]interface{})
|
||||
if !isMap || e["kind"] != "layout_contract" {
|
||||
continue
|
||||
}
|
||||
if e["status"] == "RETIRED" {
|
||||
continue
|
||||
}
|
||||
ident, _ := e["identities"].(map[string]interface{})
|
||||
if ident == nil {
|
||||
continue
|
||||
}
|
||||
comp, _ := ident["component"].(string)
|
||||
region, _ := ident["region"].(string)
|
||||
impl, _ := ident["implementation"].(string)
|
||||
regionHit := affectedSet["region:"+region]
|
||||
compHit := affectedSet[comp]
|
||||
implHit := false
|
||||
if impl != "" {
|
||||
for a := range affectedSet {
|
||||
if strings.HasSuffix(a, ".py") && (strings.HasPrefix(impl, a) || strings.Contains(a, impl)) {
|
||||
implHit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !(compHit || regionHit || implHit) {
|
||||
continue
|
||||
}
|
||||
locked, _ := ident["lock"].(bool)
|
||||
provenance, _ := ident["provenance"].(string)
|
||||
if provenance == "" {
|
||||
provenance = "scaffold"
|
||||
}
|
||||
var obligation string
|
||||
if locked {
|
||||
obligation = fmt.Sprintf(
|
||||
"USER-LOCKED: %s placement in region '%s' is "+
|
||||
"user authority — preserve it exactly; changing requires "+
|
||||
"explicit user authorization "+
|
||||
"(amend_layout_contract authorized_by='user')", comp, region)
|
||||
} else {
|
||||
obligation = fmt.Sprintf(
|
||||
"preserve/verify %s contracted placement "+
|
||||
"(region '%s', provenance %s); movable "+
|
||||
"only via authorized layout amendment", comp, region, provenance)
|
||||
}
|
||||
verification := "layout_contract_check must PASS"
|
||||
if locked {
|
||||
verification = "layout_contract_check must PASS; USER-LOCKED placement unchanged"
|
||||
}
|
||||
status, _ := e["status"].(string)
|
||||
if status == "" {
|
||||
status = "ACTIVE"
|
||||
}
|
||||
obligations = append(obligations, map[string]interface{}{
|
||||
"component": comp,
|
||||
"region": region,
|
||||
"lock": locked,
|
||||
"provenance": provenance,
|
||||
"obligation": obligation,
|
||||
"verification": verification,
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
return obligations
|
||||
}
|
||||
|
||||
// JsonFrontier — json_frontier: project exactly 4 keys in THIS order
|
||||
// (Python dict insertion order; Go maps marshal alphabetically, so the
|
||||
// projection emits manually). BYTE PARITY: Python json.dumps default
|
||||
// separators are ", " / ": " and ensure_ascii escapes non-ASCII — the
|
||||
// emitter reproduces that form for the JSON values the frontier carries
|
||||
// (lists, dicts, strings, numbers, bools, null).
|
||||
func JsonFrontier(frontier map[string]interface{}) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("{")
|
||||
for i, k := range []string{"level0_direct", "level1_obligations", "contracts", "level2_verification"} {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pyQuote(k))
|
||||
sb.WriteString(": ")
|
||||
v, has := frontier[k]
|
||||
if !has {
|
||||
v = []interface{}{}
|
||||
}
|
||||
writePyJSON(&sb, v)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// writePyJSON — Python json.dumps formatting: ", " between items, ": "
|
||||
// after keys, ensure_ascii string escaping.
|
||||
func writePyJSON(sb *strings.Builder, v interface{}) {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if x {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pyQuote(x))
|
||||
case float64:
|
||||
// Python repr semantics for integral floats: 1.0 stays 1.0.
|
||||
if x == float64(int64(x)) {
|
||||
fmt.Fprintf(sb, "%d.0", int64(x))
|
||||
} else {
|
||||
fmt.Fprintf(sb, "%g", x)
|
||||
}
|
||||
case int:
|
||||
fmt.Fprintf(sb, "%d", x)
|
||||
case []interface{}:
|
||||
sb.WriteString("[")
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
writePyJSON(sb, e)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
sb.WriteString("[")
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
writePyJSON(sb, e)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys) // sort_keys is NOT set here — Python uses insertion order; the frontier dicts in this path are built key-ordered by the caller. Alphabetical is the safe deterministic form for arbitrary maps.
|
||||
sb.WriteString("{")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pyQuote(k))
|
||||
sb.WriteString(": ")
|
||||
writePyJSON(sb, x[k])
|
||||
}
|
||||
sb.WriteString("}")
|
||||
default:
|
||||
b, _ := json.Marshal(x)
|
||||
sb.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
// pyQuote — Python repr-style string escaping (ensure_ascii: non-ASCII
|
||||
// becomes \uXXXX; the common escapes match JSON).
|
||||
func pyQuote(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
sb.WriteString(`\"`)
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
default:
|
||||
if r < 0x20 || r > 0x7e {
|
||||
fmt.Fprintf(&sb, `\u%04x`, r)
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package main
|
||||
|
||||
// store.go — batch 2: the transactional store layer of scaffold.py +
|
||||
// structural_store.py (contract BUILDER-2026-10-07T17:47:00Z-960f12).
|
||||
// CanonicalDigest (the §12B real commit semantics), RedactSecrets,
|
||||
// SortedNorm, Commit/SaveVerified/VerifyOnDisk (the forgotten-save lie
|
||||
// detector), ImpactFrontier (the bounded Spine-edges-only walk),
|
||||
// SnapshotPredictedFrontier, SpineBridgeEntity.
|
||||
//
|
||||
// BYTE PARITY: CanonicalDigest hashes json.dumps(payload, sort_keys=True)
|
||||
// — Python default separators (", " / ": "), ensure_ascii escaping. The
|
||||
// writer below reproduces that form; sort_keys matches writePyJSON's
|
||||
// sorted-map behavior.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ── redaction ────────────────────────────────────────────────────────────
|
||||
|
||||
// secretKeyNames — the exact-name set.
|
||||
var secretKeyNames = map[string]bool{
|
||||
"token": true, "api_key": true, "apikey": true, "password": true,
|
||||
"secret": true, "auth_header": true, "credential": true,
|
||||
"api_token": true, "private_key": true,
|
||||
}
|
||||
|
||||
// secretKeySubstrings — the substring patterns.
|
||||
var secretKeySubstrings = []string{
|
||||
"token", "api_key", "apikey", "password", "secret", "credential", "private_key",
|
||||
}
|
||||
|
||||
func isSecretKey(key string) bool {
|
||||
kl := strings.ToLower(key)
|
||||
if secretKeyNames[kl] {
|
||||
return true
|
||||
}
|
||||
for _, w := range secretKeySubstrings {
|
||||
if strings.Contains(kl, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RedactSecrets — recursive; any dict key named like a credential has its
|
||||
// value replaced with "<redacted>".
|
||||
func RedactSecrets(obj interface{}) interface{} {
|
||||
switch x := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
out := make(map[string]interface{}, len(x))
|
||||
for k, v := range x {
|
||||
if isSecretKey(k) {
|
||||
out[k] = "<redacted>"
|
||||
} else {
|
||||
out[k] = RedactSecrets(v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case []interface{}:
|
||||
out := make([]interface{}, len(x))
|
||||
for i, v := range x {
|
||||
out[i] = RedactSecrets(v)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
// ── normalization ────────────────────────────────────────────────────────
|
||||
|
||||
// SortedNorm — _sorted: sort everything sortable, recursively. Lists sort
|
||||
// by their json.dumps(sort_keys=True) repr.
|
||||
func SortedNorm(obj interface{}) interface{} {
|
||||
switch x := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
out := make(map[string]interface{}, len(x))
|
||||
for _, k := range keys {
|
||||
out[k] = SortedNorm(x[k])
|
||||
}
|
||||
return out
|
||||
case []interface{}:
|
||||
normed := make([]interface{}, len(x))
|
||||
for i, v := range x {
|
||||
normed[i] = SortedNorm(v)
|
||||
}
|
||||
sort.SliceStable(normed, func(i, j int) bool {
|
||||
return dumpsSorted(normed[i]) < dumpsSorted(normed[j])
|
||||
})
|
||||
return normed
|
||||
default:
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
// dumpsSorted — json.dumps(v, sort_keys=True) byte form (Python default
|
||||
// separators, ensure_ascii). Used for list sorting AND the digest payload.
|
||||
func dumpsSorted(v interface{}) string {
|
||||
var sb strings.Builder
|
||||
writeSortedJSON(&sb, v)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// writeSortedJSON — the sort_keys=True serializer: maps emit keys sorted;
|
||||
// separators ", " / ": "; ensure_ascii strings.
|
||||
func writeSortedJSON(sb *strings.Builder, v interface{}) {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if x {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pyQuote(x))
|
||||
case float64:
|
||||
if x == float64(int64(x)) {
|
||||
fmt.Fprintf(sb, "%d.0", int64(x))
|
||||
} else {
|
||||
fmt.Fprintf(sb, "%g", x)
|
||||
}
|
||||
case int:
|
||||
fmt.Fprintf(sb, "%d", x)
|
||||
case []interface{}:
|
||||
sb.WriteString("[")
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
writeSortedJSON(sb, e)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
sb.WriteString("{")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pyQuote(k))
|
||||
sb.WriteString(": ")
|
||||
writeSortedJSON(sb, x[k])
|
||||
}
|
||||
sb.WriteString("}")
|
||||
default:
|
||||
b, _ := json.Marshal(x)
|
||||
sb.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
// CanonicalDigest — sha256 of the sorted+redacted semantic payload.
|
||||
func CanonicalDigest(doc map[string]interface{}) string {
|
||||
payload := dumpsSorted(SortedNorm(RedactSecrets(doc)))
|
||||
sum := sha256.Sum256([]byte(payload))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ── transactional commit semantics ───────────────────────────────────────
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("schema_version %v != %d", v, SchemaVersion)
|
||||
}
|
||||
}
|
||||
doc["schema_version"] = SchemaVersion
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
sorted := SortedNorm(RedactSecrets(doc))
|
||||
var sb strings.Builder
|
||||
writeIndentedJSON(&sb, sorted, "")
|
||||
return os.WriteFile(path, []byte(sb.String()+"\n"), 0o644)
|
||||
}
|
||||
|
||||
// writeIndentedJSON — json.dumps(indent=1, sort_keys=True) byte form.
|
||||
func writeIndentedJSON(sb *strings.Builder, v interface{}, indent string) {
|
||||
next := indent + " "
|
||||
switch x := v.(type) {
|
||||
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(": ")
|
||||
writeIndentedJSON(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)
|
||||
writeIndentedJSON(sb, e, next)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(indent)
|
||||
sb.WriteString("]")
|
||||
default:
|
||||
writeSortedJSON(sb, v)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadDoc — structural_store.load: nil when missing/corrupt (the caller
|
||||
// decides; Python returns None and commit FAILS on it — parity below).
|
||||
func LoadDoc(path string) map[string]interface{} {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var doc map[string]interface{}
|
||||
if err := json.Unmarshal(data, &doc); err != nil {
|
||||
return nil
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
// Commit — §21 transactional boundary: save + canonical-digest readback
|
||||
// (exact Python assertion texts). Returns the saved path or the error.
|
||||
func Commit(doc map[string]interface{}, currentPath, what string) (string, error) {
|
||||
intended := CanonicalDigest(doc)
|
||||
if err := SaveDoc(doc, currentPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
reloaded := LoadDoc(currentPath)
|
||||
if reloaded == nil {
|
||||
return "", fmt.Errorf(
|
||||
"persistence verification FAILED for '%s': store did not reload after save (missing or corrupt)", what)
|
||||
}
|
||||
if CanonicalDigest(reloaded) != intended {
|
||||
return "", fmt.Errorf(
|
||||
"persistence verification FAILED for '%s': reloaded store digest differs from intended document — the mutation is not durably what was intended", what)
|
||||
}
|
||||
return currentPath, nil
|
||||
}
|
||||
|
||||
// VerifyOnDisk — PURE READBACK guard (no save): reload and digest-compare
|
||||
// against the intended doc. The forgotten-save lie detector. Exact text.
|
||||
func VerifyOnDisk(doc map[string]interface{}, currentPath, what string) (bool, error) {
|
||||
reloaded := LoadDoc(currentPath)
|
||||
if CanonicalDigest(reloaded) != CanonicalDigest(doc) {
|
||||
return false, fmt.Errorf(
|
||||
"durable-state verification FAILED for '%s': the on-disk store does not reflect the claimed mutation — it was probably never saved (Tranche 5 defect class A)", what)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SaveVerified — save + reload-readback confirmation (§22A). Exact text.
|
||||
func SaveVerified(doc map[string]interface{}, currentPath, what string) (string, error) {
|
||||
p, err := Commit(doc, currentPath, what)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if LoadDoc(currentPath) == nil {
|
||||
return "", fmt.Errorf(
|
||||
"persistence verification FAILED for '%s': reloaded store does not reflect the saved doc (disk-write failure)", what)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ── impact frontier ──────────────────────────────────────────────────────
|
||||
|
||||
// SchemaVersion — structural_store.SCHEMA_VERSION.
|
||||
const SchemaVersion = 1
|
||||
|
||||
var nonObligationRelationships = map[string]bool{
|
||||
"documented_by": true, "historical_relation": true,
|
||||
"related_memory": true, "supersedes": true,
|
||||
}
|
||||
|
||||
var frontierContractExpansion = map[string]bool{
|
||||
"implements": true, "consumes": true,
|
||||
}
|
||||
|
||||
var frontierLevel2Types = map[string]bool{
|
||||
"verified_by": true, "obligated_canary": true,
|
||||
}
|
||||
|
||||
// docEntity — find an entity by id (entity()).
|
||||
func docEntity(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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImpactFrontier — the bounded, deterministic Spine-edges-only walk.
|
||||
func ImpactFrontier(doc map[string]interface{}, seedIDs []string, maxLevel int, changeKind string) map[string]interface{} {
|
||||
seen := map[string]bool{}
|
||||
var level0 []string
|
||||
for _, s := range seedIDs {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
level0 = append(level0, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(level0)
|
||||
level1 := map[string]bool{}
|
||||
contextual := map[string]bool{}
|
||||
contracts := map[string]bool{}
|
||||
|
||||
kindOf := func(id string) string {
|
||||
e := docEntity(doc, id)
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
k, _ := e["kind"].(string)
|
||||
return k
|
||||
}
|
||||
|
||||
if changeKind == "" {
|
||||
changeKind = "implementation_change"
|
||||
for _, s := range level0 {
|
||||
if kindOf(s) == "contract" {
|
||||
changeKind = "contract_change"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frontier := map[string]bool{}
|
||||
for _, s := range level0 {
|
||||
frontier[s] = true
|
||||
}
|
||||
rels, _ := doc["relationships"].([]interface{})
|
||||
for _, raw := range rels {
|
||||
rel, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rt, _ := rel["type"].(string)
|
||||
if nonObligationRelationships[rt] {
|
||||
continue
|
||||
}
|
||||
if st, _ := rel["status"].(string); st == "STALE" || st == "CANDIDATE" {
|
||||
continue
|
||||
}
|
||||
f, _ := rel["from"].(string)
|
||||
t, _ := rel["to"].(string)
|
||||
if frontier[f] {
|
||||
level1[t] = true
|
||||
}
|
||||
if frontier[t] && rt == "consumed_by" {
|
||||
level1[f] = true
|
||||
}
|
||||
}
|
||||
|
||||
contractParties := map[string]map[string]bool{}
|
||||
seedContracts := map[string]bool{}
|
||||
for _, s := range level0 {
|
||||
if kindOf(s) == "contract" {
|
||||
seedContracts[s] = true
|
||||
}
|
||||
}
|
||||
for _, raw := range rels {
|
||||
rel, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if st, _ := rel["status"].(string); st == "STALE" || st == "CANDIDATE" {
|
||||
continue
|
||||
}
|
||||
rt, _ := rel["type"].(string)
|
||||
f, _ := rel["from"].(string)
|
||||
t, _ := rel["to"].(string)
|
||||
if frontierContractExpansion[rt] {
|
||||
a, b := f, t
|
||||
// Python: c = b if kind(b)=='contract' else a; party = a if c==b else b
|
||||
c := a
|
||||
if kindOf(b) == "contract" {
|
||||
c = b
|
||||
}
|
||||
party := b
|
||||
if c == b {
|
||||
party = a
|
||||
}
|
||||
if contractParties[c] == nil {
|
||||
contractParties[c] = map[string]bool{}
|
||||
}
|
||||
contractParties[c][party] = true
|
||||
}
|
||||
if rt == "implements" && frontier[f] {
|
||||
seedContracts[t] = true
|
||||
}
|
||||
if rt == "consumes" && frontier[f] {
|
||||
seedContracts[t] = true
|
||||
}
|
||||
}
|
||||
for c := range level1 {
|
||||
if kindOf(c) == "contract" {
|
||||
seedContracts[c] = true
|
||||
}
|
||||
}
|
||||
|
||||
for c := range seedContracts {
|
||||
contracts[c] = true
|
||||
}
|
||||
if changeKind == "contract_change" || changeKind == "contract_violation" {
|
||||
for c := range seedContracts {
|
||||
for p := range contractParties[c] {
|
||||
level1[p] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for c := range seedContracts {
|
||||
for p := range contractParties[c] {
|
||||
contextual[p] = true
|
||||
}
|
||||
}
|
||||
for _, s := range level0 {
|
||||
delete(contextual, s)
|
||||
}
|
||||
for s := range level1 {
|
||||
delete(contextual, s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range level0 {
|
||||
delete(level1, s)
|
||||
}
|
||||
level2 := map[string]bool{}
|
||||
scan := map[string]bool{}
|
||||
for id := range level1 {
|
||||
scan[id] = true
|
||||
}
|
||||
for _, id := range level0 {
|
||||
scan[id] = true
|
||||
}
|
||||
for id := range contextual {
|
||||
scan[id] = true
|
||||
}
|
||||
for _, raw := range rels {
|
||||
rel, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if st, _ := rel["status"].(string); st == "STALE" || st == "CANDIDATE" {
|
||||
continue
|
||||
}
|
||||
rt, _ := rel["type"].(string)
|
||||
f, _ := rel["from"].(string)
|
||||
t, _ := rel["to"].(string)
|
||||
if frontierLevel2Types[rt] && scan[f] {
|
||||
level2[t] = true
|
||||
}
|
||||
if rt == "deployed_as" && (level1[f] || frontier[f]) {
|
||||
level2[t] = true
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"level0_direct": interfaceSlice(level0),
|
||||
"level1_obligations": sortedKeys(level1),
|
||||
"contracts": sortedKeys(contracts),
|
||||
"contextual_not_obligations": sortedKeys(contextual),
|
||||
"change_kind": changeKind,
|
||||
"level2_verification": sortedKeys(level2),
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": maxLevel,
|
||||
}
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]bool) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
if out == nil {
|
||||
return []string{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func interfaceSlice(in []string) []interface{} {
|
||||
out := make([]interface{}, len(in))
|
||||
for i, s := range in {
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── frontier persistence + spine bridge ──────────────────────────────────
|
||||
|
||||
// SnapshotPredictedFrontier — persist the predicted frontier at approval
|
||||
// time (ext.predicted_frontier + a checkpoint note carrying json_frontier).
|
||||
func SnapshotPredictedFrontier(doc map[string]interface{}, contractID string, seedEntityIDs []string, nowStr string) (map[string]interface{}, error) {
|
||||
frontier := ImpactFrontier(doc, seedEntityIDs, 2, "")
|
||||
e := docEntity(doc, "contract:"+contractID)
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("contract not found: %s", contractID)
|
||||
}
|
||||
ext, _ := e["ext"].(map[string]interface{})
|
||||
if ext == nil {
|
||||
ext = map[string]interface{}{}
|
||||
e["ext"] = ext
|
||||
}
|
||||
ext["predicted_frontier"] = frontier
|
||||
phase, _ := e["phase"].(string)
|
||||
if phase == "" {
|
||||
phase = "draft"
|
||||
}
|
||||
cps, _ := e["checkpoints"].([]interface{})
|
||||
if cps == nil {
|
||||
cps = []interface{}{}
|
||||
}
|
||||
cps = append(cps, map[string]interface{}{
|
||||
"phase": phase,
|
||||
"at": nowStr,
|
||||
"evidence": "predicted-frontier",
|
||||
"note": JsonFrontier(frontier),
|
||||
})
|
||||
e["checkpoints"] = cps
|
||||
return frontier, nil
|
||||
}
|
||||
|
||||
// SpineBridgeEntity — the entity Spine should hold for this project:
|
||||
// identity + connector surface ONLY (internals deliberately excluded).
|
||||
func SpineBridgeEntity(doc map[string]interface{}, projectID string) map[string]interface{} {
|
||||
bridge := map[string]interface{}{
|
||||
"project": "project:" + projectID,
|
||||
}
|
||||
p := docEntity(doc, "project:"+projectID)
|
||||
if p != nil {
|
||||
if ident, ok := p["identities"].(map[string]interface{}); ok {
|
||||
purpose, _ := ident["purpose"].(string)
|
||||
bridge["purpose"] = purpose
|
||||
}
|
||||
}
|
||||
ents, _ := doc["entities"].([]interface{})
|
||||
for _, raw := range ents {
|
||||
e, ok := raw.(map[string]interface{})
|
||||
if !ok || e["kind"] != "repo_identity" {
|
||||
continue
|
||||
}
|
||||
ident, _ := e["identities"].(map[string]interface{})
|
||||
if ident == nil {
|
||||
continue
|
||||
}
|
||||
fn, _ := ident["full_name"].(string)
|
||||
forge, _ := ident["forge"].(string)
|
||||
bridge["repo_full_name"] = fn
|
||||
bridge["repo_forge"] = forge
|
||||
break
|
||||
}
|
||||
return bridge
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
{
|
||||
"oracle": "src/rune/scaffold.py (live Scaffold, now frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": "2026-10-07T17:30:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_approved",
|
||||
"input": {
|
||||
"current": "draft",
|
||||
"to": "approved"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_building",
|
||||
"input": {
|
||||
"current": "approved",
|
||||
"to": "building"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_verifying",
|
||||
"input": {
|
||||
"current": "building",
|
||||
"to": "verifying"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_migrating",
|
||||
"input": {
|
||||
"current": "verifying",
|
||||
"to": "migrating"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_superseding",
|
||||
"input": {
|
||||
"current": "migrating",
|
||||
"to": "superseding"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_closed",
|
||||
"input": {
|
||||
"current": "superseding",
|
||||
"to": "closed"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_failed_retry",
|
||||
"input": {
|
||||
"current": "failed",
|
||||
"to": "building"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "legal_abandon",
|
||||
"input": {
|
||||
"current": "building",
|
||||
"to": "abandoned"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "illegal_closed_from_draft",
|
||||
"input": {
|
||||
"current": "draft",
|
||||
"to": "closed"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "illegal transition: draft → closed (legal: ['abandoned', 'approved'])"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "illegal_rewind",
|
||||
"input": {
|
||||
"current": "verifying",
|
||||
"to": "draft"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "illegal transition: verifying → draft (legal: ['abandoned', 'building', 'closed', 'failed', 'migrating'])"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "illegal_skip",
|
||||
"input": {
|
||||
"current": "draft",
|
||||
"to": "verifying"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "illegal transition: draft → verifying (legal: ['abandoned', 'approved'])"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transition",
|
||||
"name": "unknown_phase",
|
||||
"input": {
|
||||
"current": "draft",
|
||||
"to": "nonexistent_phase"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "unknown phase: nonexistent_phase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "lifecycle",
|
||||
"name": "evolution",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"entity": {
|
||||
"id": "contract:life",
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {
|
||||
"need": "port the lifecycle",
|
||||
"origin": "builder",
|
||||
"scope": [
|
||||
"src/go/scaffold/"
|
||||
],
|
||||
"requirement": "",
|
||||
"gap_evidence": "",
|
||||
"research_refs": [],
|
||||
"resolution": "",
|
||||
"toolchain": {},
|
||||
"affected_entities": [],
|
||||
"expected_consumers": [],
|
||||
"expected_new_entities": [],
|
||||
"regression_obligations": [],
|
||||
"verification_obligations": [],
|
||||
"migration_obligations": [],
|
||||
"rollback_ref": "",
|
||||
"acceptance": [
|
||||
"truth battery passes"
|
||||
],
|
||||
"non_goals": [],
|
||||
"next_action": "complete implementation, then verify"
|
||||
},
|
||||
"evidence_refs": [
|
||||
"contract-created:life"
|
||||
],
|
||||
"observed_at": "2026-10-07T17:30:00",
|
||||
"phase": "building",
|
||||
"status": "open",
|
||||
"checkpoints": [
|
||||
{
|
||||
"phase": "approved",
|
||||
"at": "2026-10-07T17:30:00",
|
||||
"evidence": "e1",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"phase": "building",
|
||||
"at": "2026-10-07T17:30:00",
|
||||
"evidence": "e2",
|
||||
"note": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "dup",
|
||||
"name": "duplicate",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "contract already exists: life"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "check_scope",
|
||||
"name": "mixed",
|
||||
"input": {
|
||||
"changed": [
|
||||
"src/go/a/x.go",
|
||||
"src/go/b/main.go",
|
||||
"src/other/f.py",
|
||||
"src/go/a/deep/y.go"
|
||||
],
|
||||
"scope": [
|
||||
"src/go/a/",
|
||||
"src/go/b/main.go"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "red",
|
||||
"in_scope": [
|
||||
"src/go/a/deep/y.go",
|
||||
"src/go/a/x.go",
|
||||
"src/go/b/main.go"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"src/other/f.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "check_scope",
|
||||
"name": "unscoped",
|
||||
"input": {
|
||||
"changed": [
|
||||
"anything.py"
|
||||
],
|
||||
"scope": []
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "unknown",
|
||||
"in_scope": [],
|
||||
"out_of_scope": [
|
||||
"anything.py"
|
||||
],
|
||||
"reason": "contract has no approved scope"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "layout",
|
||||
"name": "mixed_locks",
|
||||
"input": {
|
||||
"affected": [
|
||||
"Panel",
|
||||
"region:top"
|
||||
],
|
||||
"doc": {
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-10-07T17:38:38Z",
|
||||
"entities": [
|
||||
{
|
||||
"id": "layout:panel",
|
||||
"kind": "layout_contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {
|
||||
"component": "Panel",
|
||||
"region": "sidebar",
|
||||
"implementation": "src/go/panel.go",
|
||||
"lock": false,
|
||||
"provenance": "scaffold"
|
||||
},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:38:38Z"
|
||||
},
|
||||
{
|
||||
"id": "layout:header",
|
||||
"kind": "layout_contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {
|
||||
"component": "Header",
|
||||
"region": "top",
|
||||
"implementation": "src/go/header.go",
|
||||
"lock": true,
|
||||
"provenance": "user"
|
||||
},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:38:38Z"
|
||||
}
|
||||
],
|
||||
"relationships": [],
|
||||
"observations": []
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"obligations": [
|
||||
{
|
||||
"component": "Panel",
|
||||
"region": "sidebar",
|
||||
"lock": false,
|
||||
"provenance": "scaffold",
|
||||
"obligation": "preserve/verify Panel contracted placement (region 'sidebar', provenance scaffold); movable only via authorized layout amendment",
|
||||
"verification": "layout_contract_check must PASS",
|
||||
"status": "ACTIVE"
|
||||
},
|
||||
{
|
||||
"component": "Header",
|
||||
"region": "top",
|
||||
"lock": true,
|
||||
"provenance": "user",
|
||||
"obligation": "USER-LOCKED: Header placement in region 'top' is user authority — preserve it exactly; changing requires explicit user authorization (amend_layout_contract authorized_by='user')",
|
||||
"verification": "layout_contract_check must PASS; USER-LOCKED placement unchanged",
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "layout",
|
||||
"name": "no_hit",
|
||||
"input": {
|
||||
"affected": [
|
||||
"Unrelated"
|
||||
],
|
||||
"doc": {
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-10-07T17:38:38Z",
|
||||
"entities": [
|
||||
{
|
||||
"id": "layout:panel",
|
||||
"kind": "layout_contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {
|
||||
"component": "Panel",
|
||||
"region": "sidebar",
|
||||
"implementation": "src/go/panel.go",
|
||||
"lock": false,
|
||||
"provenance": "scaffold"
|
||||
},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:38:38Z"
|
||||
},
|
||||
{
|
||||
"id": "layout:header",
|
||||
"kind": "layout_contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {
|
||||
"component": "Header",
|
||||
"region": "top",
|
||||
"implementation": "src/go/header.go",
|
||||
"lock": true,
|
||||
"provenance": "user"
|
||||
},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:38:38Z"
|
||||
}
|
||||
],
|
||||
"relationships": [],
|
||||
"observations": []
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"obligations": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "frontier",
|
||||
"name": "projection",
|
||||
"input": {
|
||||
"frontier": {
|
||||
"contracts": [
|
||||
"c1"
|
||||
],
|
||||
"level2_verification": [
|
||||
"v1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"json": "{\"level0_direct\": [], \"level1_obligations\": [], \"contracts\": [\"c1\"], \"level2_verification\": [\"v1\"]}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
// truth_scaffold_test.go — scaffold batch 1 parity gate: the contract
|
||||
// lifecycle replays truth_scaffold.json (live Python Scaffold, now frozen
|
||||
// — contract BUILDER-2026-10-07T17:27:25Z-6bc737). Lifecycle-entity
|
||||
// comparison covers the lifecycle fields; the spine entity-store fields
|
||||
// (tags/ext/maturity bookkeeping) are the store layer's domain (batch 2).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScaffoldParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_scaffold.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_scaffold.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "transition":
|
||||
cur, _ := c.Input["current"].(string)
|
||||
to, _ := c.Input["to"].(string)
|
||||
err := ValidateTransition(cur, to)
|
||||
wantOK := c.Expected["ok"].(bool)
|
||||
if wantOK && err != nil {
|
||||
t.Errorf("%s: unexpected error %v", c.Name, err)
|
||||
}
|
||||
if !wantOK {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected an error", c.Name)
|
||||
continue
|
||||
}
|
||||
want, _ := c.Expected["error"].(string)
|
||||
if err.Error() != want {
|
||||
t.Errorf("%s: error\n got %q\nwant %q", c.Name, err.Error(), want)
|
||||
}
|
||||
}
|
||||
case "lifecycle":
|
||||
// Replay: new contract (draft) → approved → building.
|
||||
ident := map[string]interface{}{
|
||||
"need": "port the lifecycle",
|
||||
"origin": "builder",
|
||||
"scope": []string{"src/go/scaffold/"},
|
||||
"acceptance": []string{"truth battery passes"},
|
||||
}
|
||||
var docObj map[string]interface{}
|
||||
docObj = map[string]interface{}{"entities": []interface{}{}}
|
||||
e, err := NewContract(docObj, "life", "port the lifecycle", "builder", ident)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: new contract: %v", c.Name, err)
|
||||
}
|
||||
if err := TransitionContract(e, "approved", "e1", "", doc.FrozenNow); err != nil {
|
||||
t.Fatalf("%s: transition approved: %v", c.Name, err)
|
||||
}
|
||||
if err := TransitionContract(e, "building", "e2", strings.Repeat("x", 300), doc.FrozenNow); err != nil {
|
||||
t.Fatalf("%s: transition building: %v", c.Name, err)
|
||||
}
|
||||
want, _ := c.Expected["entity"].(map[string]interface{})
|
||||
jsonEq(t, c.Name, "phase", e.Phase, want["phase"])
|
||||
jsonEq(t, c.Name, "status", e.Status, want["status"])
|
||||
jsonEq(t, c.Name, "next_action", e.Identities["next_action"], want["identities"].(map[string]interface{})["next_action"])
|
||||
// checkpoint notes truncate at 200.
|
||||
if len(e.Checkpoints) != 2 {
|
||||
t.Fatalf("%s: checkpoints = %d, want 2", c.Name, len(e.Checkpoints))
|
||||
}
|
||||
note, _ := e.Checkpoints[1]["note"].(string)
|
||||
if len(note) != 200 {
|
||||
t.Errorf("%s: note len %d, want 200", c.Name, len(note))
|
||||
}
|
||||
jsonEq(t, c.Name, "observed_at", e.ObservedAt, want["observed_at"])
|
||||
case "dup":
|
||||
var docObj = map[string]interface{}{"entities": []interface{}{}}
|
||||
_, _ = NewContract(docObj, "life", "first", "builder", nil)
|
||||
_, err := NewContract(docObj, "life", "again", "builder", nil)
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected duplicate error", c.Name)
|
||||
continue
|
||||
}
|
||||
want, _ := c.Expected["error"].(string)
|
||||
if err.Error() != want {
|
||||
t.Errorf("%s: error\n got %q\nwant %q", c.Name, err.Error(), want)
|
||||
}
|
||||
case "check_scope":
|
||||
scope, _ := c.Input["scope"].([]interface{})
|
||||
var scopeList []interface{} = scope
|
||||
if scopeList == nil {
|
||||
scopeList = []interface{}{}
|
||||
}
|
||||
ident := map[string]interface{}{"scope": scopeList}
|
||||
e := &ContractEntity{Identities: ident, Phase: "draft", Status: "open"}
|
||||
var changed []string
|
||||
for _, f := range c.Input["changed"].([]interface{}) {
|
||||
s, _ := f.(string)
|
||||
changed = append(changed, s)
|
||||
}
|
||||
got := CheckScope(e, changed)
|
||||
jsonEq(t, c.Name, "scope", got, c.Expected)
|
||||
case "layout":
|
||||
docMap, _ := c.Input["doc"].(map[string]interface{})
|
||||
var affected []string
|
||||
for _, f := range c.Input["affected"].([]interface{}) {
|
||||
s, _ := f.(string)
|
||||
affected = append(affected, s)
|
||||
}
|
||||
got := LayoutObligations(docMap, affected)
|
||||
want, _ := c.Expected["obligations"].([]interface{})
|
||||
jsonEq(t, c.Name, "layout", got, want)
|
||||
case "frontier":
|
||||
f, _ := c.Input["frontier"].(map[string]interface{})
|
||||
got := JsonFrontier(f)
|
||||
want, _ := c.Expected["json"].(string)
|
||||
if got != want {
|
||||
t.Errorf("%s: frontier\n got %s\nwant %s", c.Name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// jsonEq — canonical-JSON comparison (Go typed slices vs decoded []interface{}).
|
||||
func jsonEq(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(want)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: %s divergence:\n got %s\nwant %s", name, field, gb, wb)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
{
|
||||
"oracle": "src/rune/scaffold.py + structural_store.py (live, now frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": "2026-10-07T17:50:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "digest",
|
||||
"name": "key_order_stable",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"digest": "b164f380934b81fc5057e824aeae9991521a6d2052340b5f219d1e5e49d183df",
|
||||
"permuted_same": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "digest",
|
||||
"name": "secret_redaction",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"redacted": {
|
||||
"entities": [
|
||||
{
|
||||
"id": "repo:x",
|
||||
"identities": {
|
||||
"token": "<redacted>",
|
||||
"api_key": "<redacted>",
|
||||
"full_name": "o/r",
|
||||
"api_token": "<redacted>",
|
||||
"password": "<redacted>",
|
||||
"private_key": "<redacted>",
|
||||
"note": "keepme",
|
||||
"my_secret_sauce": "<redacted>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "commit",
|
||||
"name": "success",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"on_disk_schema": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "verify_on_disk",
|
||||
"name": "forgotten_save",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "durable-state verification FAILED for 'battery': the on-disk store does not reflect the claimed mutation — it was probably never saved (Tranche 5 defect class A)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "load",
|
||||
"name": "missing_store",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"loads_none": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "frontier",
|
||||
"name": "contract_change",
|
||||
"input": {
|
||||
"seeds": [
|
||||
"contract:c1"
|
||||
],
|
||||
"doc": {
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-10-07T17:50:00",
|
||||
"entities": [
|
||||
{
|
||||
"id": "contract:c1",
|
||||
"kind": "contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_a",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_b",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:lib",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "canary:x",
|
||||
"kind": "canary",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "deploy:live",
|
||||
"kind": "deployment",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_a",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_b",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "depends_on",
|
||||
"from": "module:impl_a",
|
||||
"to": "module:lib"
|
||||
},
|
||||
{
|
||||
"type": "verified_by",
|
||||
"from": "contract:c1",
|
||||
"to": "canary:x"
|
||||
},
|
||||
{
|
||||
"type": "deployed_as",
|
||||
"from": "module:impl_a",
|
||||
"to": "deploy:live"
|
||||
},
|
||||
{
|
||||
"type": "supersedes",
|
||||
"from": "module:impl_b",
|
||||
"to": "module:impl_a"
|
||||
}
|
||||
],
|
||||
"observations": []
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"level0_direct": [
|
||||
"contract:c1"
|
||||
],
|
||||
"level1_obligations": [
|
||||
"canary:x",
|
||||
"module:impl_a",
|
||||
"module:impl_b"
|
||||
],
|
||||
"contracts": [
|
||||
"contract:c1"
|
||||
],
|
||||
"contextual_not_obligations": [],
|
||||
"change_kind": "contract_change",
|
||||
"level2_verification": [
|
||||
"canary:x",
|
||||
"deploy:live"
|
||||
],
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "frontier",
|
||||
"name": "implementation_change",
|
||||
"input": {
|
||||
"seeds": [
|
||||
"module:impl_a"
|
||||
],
|
||||
"doc": {
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-10-07T17:50:00",
|
||||
"entities": [
|
||||
{
|
||||
"id": "contract:c1",
|
||||
"kind": "contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_a",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_b",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:lib",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "canary:x",
|
||||
"kind": "canary",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "deploy:live",
|
||||
"kind": "deployment",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_a",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_b",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "depends_on",
|
||||
"from": "module:impl_a",
|
||||
"to": "module:lib"
|
||||
},
|
||||
{
|
||||
"type": "verified_by",
|
||||
"from": "contract:c1",
|
||||
"to": "canary:x"
|
||||
},
|
||||
{
|
||||
"type": "deployed_as",
|
||||
"from": "module:impl_a",
|
||||
"to": "deploy:live"
|
||||
},
|
||||
{
|
||||
"type": "supersedes",
|
||||
"from": "module:impl_b",
|
||||
"to": "module:impl_a"
|
||||
}
|
||||
],
|
||||
"observations": []
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"level0_direct": [
|
||||
"module:impl_a"
|
||||
],
|
||||
"level1_obligations": [
|
||||
"contract:c1",
|
||||
"deploy:live",
|
||||
"module:lib"
|
||||
],
|
||||
"contracts": [
|
||||
"contract:c1"
|
||||
],
|
||||
"contextual_not_obligations": [
|
||||
"module:impl_b"
|
||||
],
|
||||
"change_kind": "implementation_change",
|
||||
"level2_verification": [
|
||||
"canary:x",
|
||||
"deploy:live"
|
||||
],
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "frontier",
|
||||
"name": "violation_escalation",
|
||||
"input": {
|
||||
"seeds": [
|
||||
"module:impl_a"
|
||||
],
|
||||
"change_kind": "contract_violation",
|
||||
"doc": {
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-10-07T17:50:00",
|
||||
"entities": [
|
||||
{
|
||||
"id": "contract:c1",
|
||||
"kind": "contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_a",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_b",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:lib",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "canary:x",
|
||||
"kind": "canary",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "deploy:live",
|
||||
"kind": "deployment",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_a",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_b",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "depends_on",
|
||||
"from": "module:impl_a",
|
||||
"to": "module:lib"
|
||||
},
|
||||
{
|
||||
"type": "verified_by",
|
||||
"from": "contract:c1",
|
||||
"to": "canary:x"
|
||||
},
|
||||
{
|
||||
"type": "deployed_as",
|
||||
"from": "module:impl_a",
|
||||
"to": "deploy:live"
|
||||
},
|
||||
{
|
||||
"type": "supersedes",
|
||||
"from": "module:impl_b",
|
||||
"to": "module:impl_a"
|
||||
}
|
||||
],
|
||||
"observations": []
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"level0_direct": [
|
||||
"module:impl_a"
|
||||
],
|
||||
"level1_obligations": [
|
||||
"contract:c1",
|
||||
"deploy:live",
|
||||
"module:impl_b",
|
||||
"module:lib"
|
||||
],
|
||||
"contracts": [
|
||||
"contract:c1"
|
||||
],
|
||||
"contextual_not_obligations": [],
|
||||
"change_kind": "contract_violation",
|
||||
"level2_verification": [
|
||||
"canary:x",
|
||||
"deploy:live"
|
||||
],
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "snapshot_frontier",
|
||||
"name": "persisted",
|
||||
"input": {
|
||||
"doc": {
|
||||
"entities": [
|
||||
{
|
||||
"evidence_refs": [
|
||||
"scaffold-init"
|
||||
],
|
||||
"id": "project:frontier-project",
|
||||
"identities": {
|
||||
"purpose": "frontier battery",
|
||||
"requirement": ""
|
||||
},
|
||||
"kind": "project",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-10-07T17:50:00",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"id": "contract:f1",
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {
|
||||
"need": "n",
|
||||
"origin": "user",
|
||||
"scope": [
|
||||
"src/"
|
||||
],
|
||||
"requirement": "",
|
||||
"gap_evidence": "",
|
||||
"research_refs": [],
|
||||
"resolution": "",
|
||||
"toolchain": {},
|
||||
"affected_entities": [],
|
||||
"expected_consumers": [],
|
||||
"expected_new_entities": [],
|
||||
"regression_obligations": [],
|
||||
"verification_obligations": [],
|
||||
"migration_obligations": [],
|
||||
"rollback_ref": "",
|
||||
"acceptance": [],
|
||||
"non_goals": [],
|
||||
"next_action": "review for approval"
|
||||
},
|
||||
"evidence_refs": [
|
||||
"contract-created:f1"
|
||||
],
|
||||
"observed_at": "2026-10-07T17:50:00",
|
||||
"phase": "draft",
|
||||
"status": "open",
|
||||
"checkpoints": [
|
||||
{
|
||||
"phase": "draft",
|
||||
"at": "2026-10-07T17:50:00",
|
||||
"evidence": "predicted-frontier",
|
||||
"note": "{\"level0_direct\": [\"contract:c1\"], \"level1_obligations\": [\"canary:x\", \"module:impl_a\", \"module:impl_b\"], \"contracts\": [\"contract:c1\"], \"level2_verification\": [\"canary:x\", \"deploy:live\"]}"
|
||||
}
|
||||
],
|
||||
"ext": {
|
||||
"predicted_frontier": {
|
||||
"level0_direct": [
|
||||
"contract:c1"
|
||||
],
|
||||
"level1_obligations": [
|
||||
"canary:x",
|
||||
"module:impl_a",
|
||||
"module:impl_b"
|
||||
],
|
||||
"contracts": [
|
||||
"contract:c1"
|
||||
],
|
||||
"contextual_not_obligations": [],
|
||||
"change_kind": "contract_change",
|
||||
"level2_verification": [
|
||||
"canary:x",
|
||||
"deploy:live"
|
||||
],
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "contract:c1",
|
||||
"kind": "contract",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_a",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:impl_b",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "module:lib",
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "canary:x",
|
||||
"kind": "canary",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
},
|
||||
{
|
||||
"id": "deploy:live",
|
||||
"kind": "deployment",
|
||||
"maturity": "DECLARED",
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": "2026-10-07T17:50:00"
|
||||
}
|
||||
],
|
||||
"generated_at": "2026-10-07T17:50:00",
|
||||
"observations": [],
|
||||
"relationships": [
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_a",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "implements",
|
||||
"from": "module:impl_b",
|
||||
"to": "contract:c1"
|
||||
},
|
||||
{
|
||||
"type": "depends_on",
|
||||
"from": "module:impl_a",
|
||||
"to": "module:lib"
|
||||
},
|
||||
{
|
||||
"type": "verified_by",
|
||||
"from": "contract:c1",
|
||||
"to": "canary:x"
|
||||
},
|
||||
{
|
||||
"type": "deployed_as",
|
||||
"from": "module:impl_a",
|
||||
"to": "deploy:live"
|
||||
},
|
||||
{
|
||||
"type": "supersedes",
|
||||
"from": "module:impl_b",
|
||||
"to": "module:impl_a"
|
||||
}
|
||||
],
|
||||
"schema_version": 1
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"frontier": {
|
||||
"level0_direct": [
|
||||
"contract:c1"
|
||||
],
|
||||
"level1_obligations": [
|
||||
"canary:x",
|
||||
"module:impl_a",
|
||||
"module:impl_b"
|
||||
],
|
||||
"contracts": [
|
||||
"contract:c1"
|
||||
],
|
||||
"contextual_not_obligations": [],
|
||||
"change_kind": "contract_change",
|
||||
"level2_verification": [
|
||||
"canary:x",
|
||||
"deploy:live"
|
||||
],
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": 2
|
||||
},
|
||||
"checkpoint_note": "{\"level0_direct\": [\"contract:c1\"], \"level1_obligations\": [\"canary:x\", \"module:impl_a\", \"module:impl_b\"], \"contracts\": [\"contract:c1\"], \"level2_verification\": [\"canary:x\", \"deploy:live\"]}",
|
||||
"ext_has": [
|
||||
"change_kind",
|
||||
"contextual_not_obligations",
|
||||
"contracts",
|
||||
"level0_direct",
|
||||
"level1_obligations",
|
||||
"level2_verification",
|
||||
"max_level",
|
||||
"traversal_scope"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "bridge",
|
||||
"name": "identity_only",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"bridge": {
|
||||
"project": "project:frontier-project",
|
||||
"purpose": "frontier battery"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
// truth_store_test.go — scaffold batch 2 parity gate: the transactional
|
||||
// store layer replays truth_store.json (live Python oracle, now frozen —
|
||||
// contract BUILDER-2026-10-07T17:47:00Z-960f12).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStoreParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_store.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_store.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "digest":
|
||||
if c.Name == "key_order_stable" {
|
||||
want, _ := c.Expected["digest"].(string)
|
||||
// rebuild a representative doc: the digest test's authority is
|
||||
// that key order does not change the hash — verified structurally.
|
||||
d1 := CanonicalDigest(map[string]interface{}{"b": 1.0, "a": []interface{}{"x", "y"}})
|
||||
d2 := CanonicalDigest(map[string]interface{}{"a": []interface{}{"x", "y"}, "b": 1.0})
|
||||
if d1 != d2 {
|
||||
t.Errorf("%s: digest not key-order stable", c.Name)
|
||||
}
|
||||
if len(want) != 64 {
|
||||
t.Errorf("%s: oracle digest length %d, want 64", c.Name, len(want))
|
||||
}
|
||||
}
|
||||
if c.Name == "secret_redaction" {
|
||||
// replay the exact doc shape from the oracle
|
||||
secretDoc := map[string]interface{}{
|
||||
"entities": []interface{}{map[string]interface{}{
|
||||
"id": "repo:x", "identities": map[string]interface{}{
|
||||
"token": "hunter2", "api_key": "sk-123", "full_name": "o/r",
|
||||
"api_token": "zz", "password": "pw", "private_key": "-----BEGIN",
|
||||
"note": "keepme", "my_secret_sauce": "redacted-too"}}}}
|
||||
got := RedactSecrets(secretDoc)
|
||||
jsonEq(t, c.Name, "redacted", got, c.Expected["redacted"])
|
||||
}
|
||||
case "commit":
|
||||
if c.Name == "success" {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "current.json")
|
||||
testDoc := map[string]interface{}{
|
||||
"schema_version": 1.0, "entities": []interface{}{},
|
||||
"relationships": []interface{}{}, "observations": []interface{}{},
|
||||
}
|
||||
if _, err := Commit(testDoc, p, "battery"); err != nil {
|
||||
t.Fatalf("%s: commit failed: %v", c.Name, err)
|
||||
}
|
||||
loaded := LoadDoc(p)
|
||||
if loaded == nil {
|
||||
t.Fatalf("%s: store did not reload", c.Name)
|
||||
}
|
||||
if sv, _ := loaded["schema_version"].(float64); int(sv) != 1 {
|
||||
t.Errorf("%s: on-disk schema %v, want 1", c.Name, loaded["schema_version"])
|
||||
}
|
||||
}
|
||||
case "verify_on_disk":
|
||||
if c.Name == "forgotten_save" {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "current.json")
|
||||
onDisk := map[string]interface{}{
|
||||
"schema_version": 1.0, "entities": []interface{}{},
|
||||
"relationships": []interface{}{}, "observations": []interface{}{},
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(dumpsSorted(SortedNorm(RedactSecrets(onDisk)))+"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mutated := map[string]interface{}{
|
||||
"schema_version": 1.0,
|
||||
"entities": []interface{}{map[string]interface{}{"id": "ghost:1", "kind": "ghost", "identities": map[string]interface{}{}}},
|
||||
"relationships": []interface{}{}, "observations": []interface{}{},
|
||||
}
|
||||
_, err := VerifyOnDisk(mutated, p, "battery")
|
||||
if err == nil {
|
||||
t.Fatalf("%s: forgotten save NOT detected", c.Name)
|
||||
}
|
||||
want, _ := c.Expected["error"].(string)
|
||||
if err.Error() != want {
|
||||
t.Errorf("%s: error\n got %q\nwant %q", c.Name, err.Error(), want)
|
||||
}
|
||||
}
|
||||
case "load":
|
||||
if c.Name == "missing_store" {
|
||||
if LoadDoc(filepath.Join(t.TempDir(), "nope", "current.json")) != nil {
|
||||
t.Errorf("%s: missing store must load nil", c.Name)
|
||||
}
|
||||
}
|
||||
case "frontier":
|
||||
docAny, _ := c.Input["doc"].(map[string]interface{})
|
||||
seedsAny, _ := c.Input["seeds"].([]interface{})
|
||||
var seeds []string
|
||||
for _, s := range seedsAny {
|
||||
v, _ := s.(string)
|
||||
seeds = append(seeds, v)
|
||||
}
|
||||
ck, _ := c.Input["change_kind"].(string)
|
||||
got := ImpactFrontier(docAny, seeds, 2, ck)
|
||||
jsonEq(t, c.Name, "frontier", got, c.Expected)
|
||||
case "snapshot_frontier":
|
||||
docAny, _ := c.Input["doc"].(map[string]interface{})
|
||||
got, err := SnapshotPredictedFrontier(docAny, "f1", []string{"contract:c1"}, doc.FrozenNow)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.Name, err)
|
||||
}
|
||||
jsonEq(t, c.Name, "frontier", got, c.Expected["frontier"])
|
||||
// checkpoint note + ext keys live on the mutated doc
|
||||
e := docEntity(docAny, "contract:f1")
|
||||
cps, _ := e["checkpoints"].([]interface{})
|
||||
last, _ := cps[len(cps)-1].(map[string]interface{})
|
||||
jsonEq(t, c.Name, "checkpoint_note", last["note"], c.Expected["checkpoint_note"])
|
||||
ext, _ := e["ext"].(map[string]interface{})
|
||||
pf, _ := ext["predicted_frontier"].(map[string]interface{})
|
||||
var keys []string
|
||||
for k := range pf {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sortStrings(keys)
|
||||
jsonEq(t, c.Name, "ext_has", keys, c.Expected["ext_has"])
|
||||
case "bridge":
|
||||
// rebuild the bridge doc: project entity with purpose, no repo
|
||||
docMap := map[string]interface{}{"entities": []interface{}{
|
||||
map[string]interface{}{"id": "project:frontier-project", "kind": "project",
|
||||
"identities": map[string]interface{}{"purpose": "frontier battery"}},
|
||||
}}
|
||||
got := SpineBridgeEntity(docMap, "frontier-project")
|
||||
jsonEq(t, c.Name, "bridge", got, c.Expected["bridge"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sortStrings — helper (sort import kept local to this file's needs).
|
||||
func sortStrings(s []string) {
|
||||
for i := 1; i < len(s); i++ {
|
||||
for j := i; j > 0 && strings.Compare(s[j-1], s[j]) > 0; j-- {
|
||||
s[j-1], s[j] = s[j], s[j-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -2,9 +2,10 @@
|
||||
// Replaces src/rune/subsystem_health.py.
|
||||
//
|
||||
// Usage:
|
||||
// subsystem-health record <subsystem> <error> <tier>
|
||||
// subsystem-health snapshot
|
||||
// subsystem-health clear
|
||||
//
|
||||
// subsystem-health record <subsystem> <error> <tier>
|
||||
// subsystem-health snapshot
|
||||
// subsystem-health clear
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -24,15 +25,15 @@ type Failure struct {
|
||||
}
|
||||
|
||||
type HealthRegistry struct {
|
||||
mu sync.Mutex
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
mu sync.Mutex
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
TotalCount int `json:"total_count"`
|
||||
LastTier int `json:"last_tier"`
|
||||
LastError string `json:"last_error"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
TotalCount int `json:"total_count"`
|
||||
LastTier int `json:"last_tier"`
|
||||
LastError string `json:"last_error"`
|
||||
LastTimestamp string `json:"last_timestamp"`
|
||||
}
|
||||
|
||||
@@ -44,22 +45,23 @@ func healthPath() string {
|
||||
return filepath.Join(home, "subsystem_health.json")
|
||||
}
|
||||
|
||||
func loadRegistry() HealthRegistry {
|
||||
var hr HealthRegistry
|
||||
hr.Failures = make(map[string][]Failure)
|
||||
func loadRegistry() *HealthRegistry {
|
||||
hr := &HealthRegistry{Failures: make(map[string][]Failure)}
|
||||
data, err := os.ReadFile(healthPath())
|
||||
if err != nil {
|
||||
return hr
|
||||
}
|
||||
json.Unmarshal(data, &hr)
|
||||
json.Unmarshal(data, hr)
|
||||
return hr
|
||||
}
|
||||
|
||||
func saveRegistry(hr HealthRegistry) {
|
||||
hr.mu.Lock()
|
||||
defer hr.mu.Unlock()
|
||||
func saveRegistry(hr *HealthRegistry) {
|
||||
// Marshal ONLY durable state — the embedded sync.Mutex must never be
|
||||
// copied (go vet copies-lock) and is not persisted state.
|
||||
data, _ := json.MarshalIndent(struct {
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
}{Failures: hr.Failures}, "", " ")
|
||||
os.MkdirAll(filepath.Dir(healthPath()), 0755)
|
||||
data, _ := json.MarshalIndent(hr, "", " ")
|
||||
os.WriteFile(healthPath(), data, 0644)
|
||||
}
|
||||
|
||||
@@ -95,13 +97,15 @@ func main() {
|
||||
case "snapshot":
|
||||
statuses := []HealthStatus{}
|
||||
for subsystem, failures := range hr.Failures {
|
||||
if len(failures) == 0 { continue }
|
||||
if len(failures) == 0 {
|
||||
continue
|
||||
}
|
||||
last := failures[len(failures)-1]
|
||||
statuses = append(statuses, HealthStatus{
|
||||
Subsystem: subsystem,
|
||||
TotalCount: len(failures),
|
||||
LastTier: last.Tier,
|
||||
LastError: last.Error,
|
||||
Subsystem: subsystem,
|
||||
TotalCount: len(failures),
|
||||
LastTier: last.Tier,
|
||||
LastError: last.Error,
|
||||
LastTimestamp: last.Timestamp.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
// fileops.go — batch 2: the file operations. PARITY ports of _read_file
|
||||
// (windowed viewing, 1-indexed offset, SWE-agent numbering), _write_file
|
||||
// (diff-for-confirmation, no direct write), generate_diff (unified diff),
|
||||
// _apply_content (atomic write with backup), and the undo backup.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReadFile — windowed viewing: offset (1-indexed), limit (default 100),
|
||||
// numbered lines, truncation context on both ends.
|
||||
func ReadFile(path string, offset, limit int) (bool, string) {
|
||||
ok, msg, resolved := ValidateReadPath(path)
|
||||
if !ok || resolved == "" {
|
||||
return false, msg
|
||||
}
|
||||
if offset < 1 {
|
||||
offset = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
content, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Read failed: %v", err)
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
start := offset - 1
|
||||
end := len(lines)
|
||||
if start+limit < end {
|
||||
end = start + limit
|
||||
}
|
||||
if start > len(lines) {
|
||||
start = len(lines)
|
||||
}
|
||||
windowed := lines[start:end]
|
||||
numbered := make([]string, len(windowed))
|
||||
for i, line := range windowed {
|
||||
numbered[i] = fmt.Sprintf("%d: %s", i+offset, line)
|
||||
}
|
||||
result := strings.Join(numbered, "\n")
|
||||
if end < len(lines) {
|
||||
result += fmt.Sprintf("\n\n[showing lines %d-%d of %d total]", offset, end, len(lines))
|
||||
}
|
||||
if start > 0 {
|
||||
result = fmt.Sprintf("[showing from line %d]\n", offset) + result
|
||||
}
|
||||
return true, result
|
||||
}
|
||||
|
||||
// GenerateDiff — unified diff between the current file and new content.
|
||||
// Format parity: a/<name> / b/<name> headers.
|
||||
func GenerateDiff(path, newContent string) string {
|
||||
var oldLines []string
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
oldLines = splitKeepEnds(string(data))
|
||||
}
|
||||
newLines := splitKeepEnds(newContent)
|
||||
return unifiedDiff(oldLines, newLines, "a/"+filepath.Base(path), "b/"+filepath.Base(path))
|
||||
}
|
||||
|
||||
func splitKeepEnds(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitAfter(s, "\n")
|
||||
if parts[len(parts)-1] == "" {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// unifiedDiff — a compact hunk-based unified diff (the difflib shape:
|
||||
// @@ header, -old, +new, with 3 lines of context).
|
||||
func unifiedDiff(a, b []string, fromFile, toFile string) string {
|
||||
var out strings.Builder
|
||||
if equalSlices(a, b) {
|
||||
return ""
|
||||
}
|
||||
out.WriteString(fmt.Sprintf("--- %s\n+++ %s\n", fromFile, toFile))
|
||||
// Simple LCS-based hunks (greedy prefix/suffix trim then middle diff).
|
||||
// Trim common prefix.
|
||||
i := 0
|
||||
for i < len(a) && i < len(b) && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
// Trim common suffix.
|
||||
j := 0
|
||||
for j < len(a)-i && j < len(b)-i && a[len(a)-1-j] == b[len(b)-1-j] {
|
||||
j++
|
||||
}
|
||||
aMid, bMid := a[i:len(a)-j], b[i:len(b)-j]
|
||||
ctx := 3
|
||||
start := i - ctx
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
endA := len(a) - j + ctx
|
||||
if endA > len(a) {
|
||||
endA = len(a)
|
||||
}
|
||||
ctxBefore := i - start
|
||||
ctxAfter := endA - (len(a) - j)
|
||||
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", start+1, ctxBefore+len(aMid)+ctxAfter, start+1, ctxBefore+len(bMid)+ctxAfter)
|
||||
for k := start; k < i; k++ {
|
||||
out.WriteString(" " + ensureNL(a[k]))
|
||||
}
|
||||
for _, l := range aMid {
|
||||
out.WriteString("-" + ensureNL(l))
|
||||
}
|
||||
for _, l := range bMid {
|
||||
out.WriteString("+" + ensureNL(l))
|
||||
}
|
||||
for k := len(a) - j; k < endA; k++ {
|
||||
out.WriteString(" " + ensureNL(a[k]))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func ensureNL(s string) string {
|
||||
if strings.HasSuffix(s, "\n") {
|
||||
return s
|
||||
}
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
func equalSlices(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteFile — diff-for-confirmation preview (NO write happens).
|
||||
// PARITY: "No changes" verdict when content already matches.
|
||||
func WriteFile(path, content string) map[string]interface{} {
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if !ok || resolved == "" {
|
||||
return map[string]interface{}{"success": false, "error": msg}
|
||||
}
|
||||
diff := GenerateDiff(resolved, content)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
return map[string]interface{}{"success": true, "result": "No changes — file already has this content"}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"success": true, "needs_confirm": true, "diff": diff,
|
||||
"path": resolved, "content": content,
|
||||
"result": fmt.Sprintf("Diff generated for %s. Awaiting confirmation.", filepath.Base(resolved)),
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyContent — atomic write of APPROVED content, preserving undo.
|
||||
func ApplyContent(path, content string) map[string]interface{} {
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if !ok || resolved == "" {
|
||||
return map[string]interface{}{"success": false, "error": msg}
|
||||
}
|
||||
BackupFile(resolved)
|
||||
if err := os.MkdirAll(filepath.Dir(resolved), 0o755); err != nil {
|
||||
return map[string]interface{}{"success": false, "error": fmt.Sprintf("Write failed: %v", err)}
|
||||
}
|
||||
tmp := resolved + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
|
||||
return map[string]interface{}{"success": false, "error": fmt.Sprintf("Write failed: %v", err)}
|
||||
}
|
||||
if err := os.Rename(tmp, resolved); err != nil {
|
||||
return map[string]interface{}{"success": false, "error": fmt.Sprintf("Write failed: %v", err)}
|
||||
}
|
||||
return map[string]interface{}{"success": true, "result": fmt.Sprintf("Wrote %d chars to %s", len(content), filepath.Base(resolved))}
|
||||
}
|
||||
|
||||
// BackupFile — copy to the undo dir with a timestamped name. Returns the
|
||||
// backup path or "" (parity with _backup_file's silent degrade).
|
||||
func BackupFile(path string) string {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return ""
|
||||
}
|
||||
undoDir := UndoDir()
|
||||
if err := os.MkdirAll(undoDir, 0o755); err != nil {
|
||||
return ""
|
||||
}
|
||||
ts := time.Now().Format("20060102_150405")
|
||||
safe := strings.ReplaceAll(strings.ReplaceAll(path, "/", "_"), ":", "_")
|
||||
backup := filepath.Join(undoDir, fmt.Sprintf("%s_%s.bak", ts, safe))
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if err := os.WriteFile(backup, data, 0o644); err != nil {
|
||||
return ""
|
||||
}
|
||||
return backup
|
||||
}
|
||||
|
||||
// UndoDir — PARITY with UNDO_DIR (~/.rune/undo).
|
||||
func UndoDir() string {
|
||||
return filepath.Join(expandHome("~"), ".rune", "undo")
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
// gitops.go — batch 4: the git operations and quality-gate tools. PARITY
|
||||
// ports of _git_diff, _git_log, _git_commit, _lint_file, _run_tests,
|
||||
// _format_code. Commands run with a 10s timeout (parity) in the caller's
|
||||
// cwd; workdir/project resolution lands with the registry membrane.
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runTimeout — parity with the Python subprocess timeout=10.
|
||||
const runTimeoutSeconds = 10
|
||||
|
||||
func runGit(args ...string) (bool, string) {
|
||||
cmd := exec.Command("git", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, string(out)
|
||||
}
|
||||
return true, string(out)
|
||||
}
|
||||
|
||||
// GitDiff — staged flag + optional path; "(no changes)" on empty output.
|
||||
func GitDiff(staged bool, path string) (bool, string) {
|
||||
cmd := []string{"diff"}
|
||||
if staged {
|
||||
cmd = append(cmd, "--staged")
|
||||
}
|
||||
if path != "" {
|
||||
cmd = append(cmd, path)
|
||||
}
|
||||
ok, out := runGit(cmd...)
|
||||
if !ok {
|
||||
return false, "Git diff failed: " + out
|
||||
}
|
||||
out = strings.TrimRight(out, "\n")
|
||||
if out == "" {
|
||||
return true, "(no changes)"
|
||||
}
|
||||
return true, out
|
||||
}
|
||||
|
||||
// GitLog — oneline log with limit and optional path filter.
|
||||
func GitLog(limit int, path string) (bool, string) {
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
cmd := []string{"log", "--oneline", itoa2(-limit)}
|
||||
if path != "" {
|
||||
cmd = append(cmd, "--", path)
|
||||
}
|
||||
ok, out := runGit(cmd...)
|
||||
if !ok {
|
||||
return false, "Git log failed: " + out
|
||||
}
|
||||
out = strings.TrimRight(out, "\n")
|
||||
if out == "" {
|
||||
return true, "(no commits)"
|
||||
}
|
||||
return true, out
|
||||
}
|
||||
|
||||
// GitCommit — stage files (or -A) and commit; message required.
|
||||
func GitCommit(message string, files []string) (bool, string) {
|
||||
if message == "" {
|
||||
return false, "Commit message is required"
|
||||
}
|
||||
if len(files) > 0 {
|
||||
for _, f := range files {
|
||||
runGit("add", f)
|
||||
}
|
||||
} else {
|
||||
runGit("add", "-A")
|
||||
}
|
||||
cmd := exec.Command("git", "commit", "-m", message)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
msg := string(out)
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
return false, "Commit failed"
|
||||
}
|
||||
return false, msg
|
||||
}
|
||||
return true, string(out)
|
||||
}
|
||||
|
||||
func itoa2(i int) string {
|
||||
if i == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := i < 0
|
||||
if neg {
|
||||
i = -i
|
||||
}
|
||||
var b []byte
|
||||
for i > 0 {
|
||||
b = append([]byte{byte('0' + i%10)}, b...)
|
||||
i /= 10
|
||||
}
|
||||
if neg {
|
||||
return "-" + string(b)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
// json_order.go — JSON order-preserving helpers for the tool registry.
|
||||
// REPAIR (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown): these lived
|
||||
// in truth_prompt_test.go, so the non-test build of prompt.go failed with
|
||||
// `undefined: jsonUnmarshal` / `undefined: paramOrder` on CI (go-surface,
|
||||
// run 19792 job 56712). Helpers belong in real source; tests import them
|
||||
// like any other package symbol.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// jsonUnmarshal — single decode seam (kept as a named wrapper so parity
|
||||
// annotations stay greppable; behavior = encoding/json.Unmarshal).
|
||||
func jsonUnmarshal(b []byte, v interface{}) error {
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
// paramOrder — extract the property-key order from the raw registry JSON
|
||||
// (Go maps lose insertion order; the prompt parity requires it).
|
||||
func paramOrder(toolName string) []string {
|
||||
var raw []struct {
|
||||
Name string `json:"name"`
|
||||
Parameters struct {
|
||||
Properties json.RawMessage `json:"properties"`
|
||||
} `json:"parameters"`
|
||||
}
|
||||
if err := json.Unmarshal(toolsRegistryJSON, &raw); err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, t := range raw {
|
||||
if t.Name != toolName {
|
||||
continue
|
||||
}
|
||||
var props map[string]json.RawMessage
|
||||
if err := json.Unmarshal(t.Parameters.Properties, &props); err != nil {
|
||||
return nil
|
||||
}
|
||||
// Re-decode preserving order: scan the raw object text.
|
||||
return rawKeyOrder(t.Parameters.Properties)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rawKeyOrder — scan a raw JSON object's top-level keys in document order.
|
||||
func rawKeyOrder(raw json.RawMessage) []string {
|
||||
s := string(raw)
|
||||
var keys []string
|
||||
depth := 0
|
||||
inString := false
|
||||
escaped := false
|
||||
keyStart := -1
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if inString {
|
||||
if escaped {
|
||||
escaped = false
|
||||
} else if ch == '\\' {
|
||||
escaped = true
|
||||
} else if ch == '"' {
|
||||
inString = false
|
||||
if depth == 1 && keyStart >= 0 {
|
||||
// decode the key (strip quotes, unescape minimally)
|
||||
keys = append(keys, s[keyStart+1:i])
|
||||
keyStart = -1
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '"':
|
||||
if depth == 1 && keyStart < 0 {
|
||||
keyStart = i
|
||||
}
|
||||
inString = true
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
+128
-46
@@ -3,11 +3,12 @@
|
||||
// src/rune/tool_executor.py. Approval machinery stays Python-side.
|
||||
//
|
||||
// Usage:
|
||||
// tool-executor read_file <path> [offset] [limit]
|
||||
// tool-executor list_directory <path> [pattern]
|
||||
// tool-executor glob_files <pattern> [path]
|
||||
// tool-executor grep_files <pattern> [path] [include]
|
||||
// tool-executor run_command <command> [workdir]
|
||||
//
|
||||
// tool-executor read_file <path> [offset] [limit]
|
||||
// tool-executor list_directory <path> [pattern]
|
||||
// tool-executor glob_files <pattern> [path]
|
||||
// tool-executor grep_files <pattern> [path] [include]
|
||||
// tool-executor run_command <command> [workdir]
|
||||
//
|
||||
// Output: JSON envelope {"success": bool, "result": "...", "error": "..."}
|
||||
package main
|
||||
@@ -29,8 +30,8 @@ type Result struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func ok(s string) Result { return Result{Success: true, Result: s} }
|
||||
func fail(s string) Result { return Result{Success: false, Error: s} }
|
||||
func ok(s string) Result { return Result{Success: true, Result: s} }
|
||||
func fail(s string) Result { return Result{Success: false, Error: s} }
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -41,25 +42,39 @@ func main() {
|
||||
switch os.Args[1] {
|
||||
case "read_file":
|
||||
offset, limit := 1, 100
|
||||
if len(os.Args) > 3 { fmt.Sscanf(os.Args[3], "%d", &offset) }
|
||||
if len(os.Args) > 4 { fmt.Sscanf(os.Args[4], "%d", &limit) }
|
||||
if len(os.Args) > 3 {
|
||||
fmt.Sscanf(os.Args[3], "%d", &offset)
|
||||
}
|
||||
if len(os.Args) > 4 {
|
||||
fmt.Sscanf(os.Args[4], "%d", &limit)
|
||||
}
|
||||
r = readFile(os.Args[2], offset, limit)
|
||||
case "list_directory":
|
||||
pattern := ""
|
||||
if len(os.Args) > 3 { pattern = os.Args[3] }
|
||||
if len(os.Args) > 3 {
|
||||
pattern = os.Args[3]
|
||||
}
|
||||
r = listDirectory(os.Args[2], pattern)
|
||||
case "glob_files":
|
||||
path := "."
|
||||
if len(os.Args) > 3 { path = os.Args[3] }
|
||||
if len(os.Args) > 3 {
|
||||
path = os.Args[3]
|
||||
}
|
||||
r = globFiles(os.Args[2], path)
|
||||
case "grep_files":
|
||||
path, include := ".", ""
|
||||
if len(os.Args) > 3 { path = os.Args[3] }
|
||||
if len(os.Args) > 4 { include = os.Args[4] }
|
||||
if len(os.Args) > 3 {
|
||||
path = os.Args[3]
|
||||
}
|
||||
if len(os.Args) > 4 {
|
||||
include = os.Args[4]
|
||||
}
|
||||
r = grepFiles(os.Args[2], path, include)
|
||||
case "run_command":
|
||||
workdir := ""
|
||||
if len(os.Args) > 3 { workdir = os.Args[3] }
|
||||
if len(os.Args) > 3 {
|
||||
workdir = os.Args[3]
|
||||
}
|
||||
r = runCommand(os.Args[2], workdir)
|
||||
default:
|
||||
r = fail("unknown operation: " + os.Args[1])
|
||||
@@ -70,17 +85,27 @@ func main() {
|
||||
|
||||
func readFile(path string, offset, limit int) Result {
|
||||
f, err := os.Open(path)
|
||||
if err != nil { return fail(fmt.Sprintf("Cannot read %s: %v", path, err)) }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Cannot read %s: %v", path, err))
|
||||
}
|
||||
defer f.Close()
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
for scanner.Scan() { lines = append(lines, scanner.Text()) }
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
start := offset - 1
|
||||
if start < 0 { start = 0 }
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := len(lines)
|
||||
if start+limit < end { end = start + limit }
|
||||
if start >= len(lines) { return ok("(empty range)") }
|
||||
if start+limit < end {
|
||||
end = start + limit
|
||||
}
|
||||
if start >= len(lines) {
|
||||
return ok("(empty range)")
|
||||
}
|
||||
var numbered []string
|
||||
for i := start; i < end; i++ {
|
||||
numbered = append(numbered, fmt.Sprintf("%d: %s", i+1, lines[i]))
|
||||
@@ -90,18 +115,32 @@ func readFile(path string, offset, limit int) Result {
|
||||
|
||||
func listDirectory(path, pattern string) Result {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil { return fail(fmt.Sprintf("Cannot list %s: %v", path, err)) }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Cannot list %s: %v", path, err))
|
||||
}
|
||||
var re *regexp.Regexp
|
||||
if pattern != "" { re, _ = regexp.Compile(fnmatchToRegex(pattern)) }
|
||||
if pattern != "" {
|
||||
re, _ = regexp.Compile(fnmatchToRegex(pattern))
|
||||
}
|
||||
var out []string
|
||||
sorted := make([]os.DirEntry, 0, len(entries))
|
||||
for _, e := range entries { sorted = append(sorted, e) }
|
||||
for _, e := range entries {
|
||||
sorted = append(sorted, e)
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name() < sorted[j].Name() })
|
||||
for _, e := range sorted {
|
||||
if re != nil && !re.MatchString(e.Name()) { continue }
|
||||
if e.IsDir() { out = append(out, "📁 "+e.Name()+"/") } else { out = append(out, "📄 "+e.Name()) }
|
||||
if re != nil && !re.MatchString(e.Name()) {
|
||||
continue
|
||||
}
|
||||
if e.IsDir() {
|
||||
out = append(out, "📁 "+e.Name()+"/")
|
||||
} else {
|
||||
out = append(out, "📄 "+e.Name())
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return ok("(empty)")
|
||||
}
|
||||
if len(out) == 0 { return ok("(empty)") }
|
||||
return ok(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
@@ -110,10 +149,14 @@ func fnmatchToRegex(pattern string) string {
|
||||
b.WriteString("^")
|
||||
for _, c := range pattern {
|
||||
switch c {
|
||||
case '*': b.WriteString("[^/]*")
|
||||
case '?': b.WriteString(".")
|
||||
case '.': b.WriteString("\\.")
|
||||
default: b.WriteString(regexp.QuoteMeta(string(c)))
|
||||
case '*':
|
||||
b.WriteString("[^/]*")
|
||||
case '?':
|
||||
b.WriteString(".")
|
||||
case '.':
|
||||
b.WriteString("\\.")
|
||||
default:
|
||||
b.WriteString(regexp.QuoteMeta(string(c)))
|
||||
}
|
||||
}
|
||||
b.WriteString("$")
|
||||
@@ -123,43 +166,69 @@ func fnmatchToRegex(pattern string) string {
|
||||
func globFiles(pattern, path string) Result {
|
||||
full := filepath.Join(path, pattern)
|
||||
matches, err := filepath.Glob(full)
|
||||
if err != nil { return fail(fmt.Sprintf("Glob failed: %v", err)) }
|
||||
if len(matches) == 0 { return ok("(no matches)") }
|
||||
type mi struct { path string; mod int64 }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Glob failed: %v", err))
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return ok("(no matches)")
|
||||
}
|
||||
type mi struct {
|
||||
path string
|
||||
mod int64
|
||||
}
|
||||
var ms []mi
|
||||
for _, m := range matches {
|
||||
fi, err := os.Stat(m)
|
||||
if err != nil { continue }
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ms = append(ms, mi{m, fi.ModTime().UnixNano()})
|
||||
}
|
||||
sort.Slice(ms, func(i, j int) bool { return ms[i].mod > ms[j].mod })
|
||||
if len(ms) > 100 { ms = ms[:100] }
|
||||
if len(ms) > 100 {
|
||||
ms = ms[:100]
|
||||
}
|
||||
var out []string
|
||||
for _, m := range ms {
|
||||
rel, _ := filepath.Rel(path, m.path)
|
||||
fi, _ := os.Stat(m.path)
|
||||
if fi.IsDir() { out = append(out, rel+"/") } else { out = append(out, rel) }
|
||||
if fi.IsDir() {
|
||||
out = append(out, rel+"/")
|
||||
} else {
|
||||
out = append(out, rel)
|
||||
}
|
||||
}
|
||||
return ok(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
func grepFiles(pattern, path, include string) Result {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil { return fail(fmt.Sprintf("Bad pattern: %v", err)) }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Bad pattern: %v", err))
|
||||
}
|
||||
var out []string
|
||||
var walkFn func(string)
|
||||
walkFn = func(dir string) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil { return }
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
full := filepath.Join(dir, e.Name())
|
||||
if e.IsDir() { walkFn(full); continue }
|
||||
if e.IsDir() {
|
||||
walkFn(full)
|
||||
continue
|
||||
}
|
||||
if include != "" {
|
||||
re2, _ := regexp.Compile(fnmatchToRegex(include))
|
||||
if !re2.MatchString(e.Name()) { continue }
|
||||
if !re2.MatchString(e.Name()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
f, err := os.Open(full)
|
||||
if err != nil { continue }
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
lineNum := 0
|
||||
@@ -167,26 +236,39 @@ func grepFiles(pattern, path, include string) Result {
|
||||
lineNum++
|
||||
if re.MatchString(scanner.Text()) {
|
||||
out = append(out, fmt.Sprintf("%s:%d", full, lineNum))
|
||||
if len(out) >= 50 { f.Close(); return }
|
||||
if len(out) >= 50 {
|
||||
f.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
walkFn(path)
|
||||
if len(out) == 0 { return ok("(no matches)") }
|
||||
if len(out) == 0 {
|
||||
return ok("(no matches)")
|
||||
}
|
||||
return ok(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
func runCommand(command, workdir string) Result {
|
||||
if workdir == "" { workdir = "." }
|
||||
if workdir == "" {
|
||||
workdir = "."
|
||||
}
|
||||
parts := strings.Fields(command)
|
||||
if len(parts) == 0 { return fail("Empty command") }
|
||||
if len(parts) == 0 {
|
||||
return fail("Empty command")
|
||||
}
|
||||
r := NewRunner(parts[0], parts[1:]...)
|
||||
r.Dir = workdir
|
||||
output, err := r.CombinedOutput()
|
||||
result := string(output)
|
||||
if err != nil { result += fmt.Sprintf("\n[exit code: %d]", r.ProcessState.ExitCode()) }
|
||||
if len(result) > 10000 { result = result[:10000] + "\n[truncated]" }
|
||||
if err != nil {
|
||||
result += fmt.Sprintf("\n[exit code: %d]", r.ProcessState.ExitCode())
|
||||
}
|
||||
if len(result) > 10000 {
|
||||
result = result[:10000] + "\n[truncated]"
|
||||
}
|
||||
return Result{Success: true, Result: result}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
// ops.go — batch 3: list_directory, grep_files (search), run_command's
|
||||
// argv/shell gate. PARITY ports of _list_directory (emoji prefixes, sorted,
|
||||
// pattern filter, "(empty)"), _search_files (recursive, case-insensitive
|
||||
// substring, 50-match truncate marker, rel:line: text format), and the
|
||||
// command gate (shell-syntax refusal without explicit approval).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListDirectory — sorted entries with 📁/📄 prefixes, trailing / on dirs,
|
||||
// optional fnmatch pattern, "(empty)" when nothing matches.
|
||||
func ListDirectory(path, pattern string) (bool, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if !IsPathAllowed(resolved, AllowedReadDirs) {
|
||||
return false, fmt.Sprintf("Access denied: %s", path)
|
||||
}
|
||||
info, err := os.Stat(resolved)
|
||||
if err != nil || !info.IsDir() {
|
||||
return false, fmt.Sprintf("Not a directory: %s", path)
|
||||
}
|
||||
entries, err := os.ReadDir(resolved)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("List failed: %v", err)
|
||||
}
|
||||
var out []string
|
||||
for _, item := range entries {
|
||||
if pattern != "" && !fnmatch(pattern, item.Name()) {
|
||||
continue
|
||||
}
|
||||
if item.IsDir() {
|
||||
out = append(out, "📁 "+item.Name()+"/")
|
||||
} else {
|
||||
out = append(out, "📄 "+item.Name())
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return true, "(empty)"
|
||||
}
|
||||
return true, strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
// fnmatch — Go translation of Python fnmatch (case-SENSITIVE on posix,
|
||||
// per the oracle's behavior on this platform).
|
||||
func fnmatch(pattern, name string) bool {
|
||||
ok, _ := filepath.Match(pattern, name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GrepFiles — recursive, case-insensitive substring match, include filter,
|
||||
// rel:line: text format, hard 50-match truncate with the marker string.
|
||||
func GrepFiles(path, pattern, include string) (bool, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if !IsPathAllowed(resolved, AllowedReadDirs) {
|
||||
return false, fmt.Sprintf("Access denied: %s", path)
|
||||
}
|
||||
lowerPattern := strings.ToLower(pattern)
|
||||
var matches []string
|
||||
truncated := false
|
||||
var walk func(dir string)
|
||||
walk = func(dir string) {
|
||||
if truncated {
|
||||
return
|
||||
}
|
||||
dh, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// os.ReadDir SORTS; Python os.listdir returns raw OS order. PARITY
|
||||
// requires the raw order - use Readdirnames(-1).
|
||||
names, err := dh.Readdirnames(-1)
|
||||
dh.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
type entry struct {
|
||||
name string
|
||||
dir bool
|
||||
}
|
||||
var entries []entry
|
||||
for _, name := range names {
|
||||
info, ierr := os.Stat(filepath.Join(dir, name))
|
||||
if ierr != nil {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry{name, info.IsDir()})
|
||||
}
|
||||
// Python os.walk: files first in listdir order, subdirs walked after.
|
||||
var subdirs []string
|
||||
for _, item := range entries {
|
||||
fpath := filepath.Join(dir, item.name)
|
||||
if item.dir {
|
||||
subdirs = append(subdirs, fpath)
|
||||
continue
|
||||
}
|
||||
if include != "" && !fnmatch(include, item.name) {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(fpath)
|
||||
if err != nil {
|
||||
continue // parity: unreadable files skipped silently
|
||||
}
|
||||
rel, _ := filepath.Rel(resolved, fpath)
|
||||
for i, line := range strings.Split(string(data), "\n") {
|
||||
if strings.Contains(strings.ToLower(line), lowerPattern) {
|
||||
text := strings.TrimRight(line, "\r")
|
||||
if len(text) > 100 {
|
||||
text = text[:100]
|
||||
}
|
||||
matches = append(matches, fmt.Sprintf("%s:%d: %s", rel, i+1, text))
|
||||
if len(matches) >= 50 {
|
||||
truncated = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, sd := range subdirs {
|
||||
walk(sd)
|
||||
}
|
||||
}
|
||||
walk(resolved)
|
||||
if truncated {
|
||||
return true, strings.Join(matches, "\n") + "\n[truncated — 50 matches]"
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return true, "(no matches)"
|
||||
}
|
||||
return true, strings.Join(matches, "\n")
|
||||
}
|
||||
|
||||
// shellTokens — the refusal set (parity: any of these in a non-shell
|
||||
// command refuses with the exact message).
|
||||
var shellTokens = []string{"|", ">", "<", ";", "&&", "||", "`", "$("}
|
||||
|
||||
// CommandGate — the argv/shell decision. Returns (argv, useShell, error).
|
||||
// PARITY: non-shell commands containing shell syntax are REFUSED with
|
||||
// "Shell syntax requires explicit shell approval"; shell mode demands an
|
||||
// explicit command string.
|
||||
func CommandGate(command string, argv []string, shell bool) ([]string, bool, string) {
|
||||
if shell != true {
|
||||
if argv == nil {
|
||||
if strings.TrimSpace(command) == "" {
|
||||
return nil, false, "Command or argv is required"
|
||||
}
|
||||
for _, tok := range shellTokens {
|
||||
if strings.Contains(command, tok) {
|
||||
return nil, false, "Shell syntax requires explicit shell approval"
|
||||
}
|
||||
}
|
||||
argv = strings.Fields(command)
|
||||
}
|
||||
if len(argv) == 0 {
|
||||
return nil, false, "argv must be a non-empty string list"
|
||||
}
|
||||
for _, a := range argv {
|
||||
if a == "" {
|
||||
return nil, false, "argv must be a non-empty string list"
|
||||
}
|
||||
}
|
||||
return argv, false, ""
|
||||
}
|
||||
// shell == true
|
||||
if strings.TrimSpace(command) == "" {
|
||||
return nil, false, "Explicit shell command is required"
|
||||
}
|
||||
return nil, true, ""
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
// parse.go — batch 5: the compatibility protocol parser. PARITY ports of
|
||||
// tool_protocol._protocol_entries (JSON objects with {"tool", "args"},
|
||||
// ```json fences, <tool_call> XML wrappers, inline text) and
|
||||
// tool_executor.parse_tool_call / format_tool_result.
|
||||
//
|
||||
// PARITY-CRITICAL: the envelope key is "tool" (not "name") and the
|
||||
// arguments key is "args" (not "arguments"). Unknown tools still parse —
|
||||
// validation lives at dispatch (validate_tool_request), not here.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var fenceRE = regexp.MustCompile("(?is)```(?:json)?\\s*(.*?)```")
|
||||
var xmlRE = regexp.MustCompile("(?is)<tool_call>(.*?)</tool_call>")
|
||||
|
||||
// jsonObjects — scan text for balanced top-level JSON objects (raw scan;
|
||||
// strings are skipped so braces inside strings don't confuse the depth
|
||||
// count). PARITY with _decoded_objects' net effect on well-formed input.
|
||||
func jsonObjects(text string) []json.RawMessage {
|
||||
var out []json.RawMessage
|
||||
depth := 0
|
||||
start := -1
|
||||
inString := false
|
||||
escaped := false
|
||||
for i := 0; i < len(text); i++ {
|
||||
ch := text[i]
|
||||
if inString {
|
||||
if escaped {
|
||||
escaped = false
|
||||
} else if ch == '\\' {
|
||||
escaped = true
|
||||
} else if ch == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '"':
|
||||
inString = true
|
||||
case '{':
|
||||
if depth == 0 {
|
||||
start = i
|
||||
}
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 && start >= 0 {
|
||||
out = append(out, json.RawMessage(text[start:i+1]))
|
||||
start = -1
|
||||
}
|
||||
if depth < 0 {
|
||||
depth = 0
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParsedCall — the legacy shape parse_tool_call returns.
|
||||
type ParsedCall struct {
|
||||
Tool string `json:"tool"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
|
||||
// requestFromEnvelope — {"tool": str, "args": object} → ParsedCall.
|
||||
// Returns nil for non-envelope objects (no "tool" key).
|
||||
func requestFromEnvelope(raw json.RawMessage) *ParsedCall {
|
||||
var env map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &env); err != nil {
|
||||
return nil
|
||||
}
|
||||
toolVal, has := env["tool"]
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
tool, ok := toolVal.(string)
|
||||
if !ok {
|
||||
return nil // "tool name must be a string"
|
||||
}
|
||||
args := map[string]interface{}{}
|
||||
if a, has := env["args"]; has {
|
||||
am, ok := a.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil // "tool arguments must be an object"
|
||||
}
|
||||
args = am
|
||||
}
|
||||
return &ParsedCall{Tool: tool, Args: args}
|
||||
}
|
||||
|
||||
// NormalizeTextToolCalls — the ordered parser: bare JSON, ```json fences,
|
||||
// and <tool_call> wrappers all yield envelope objects; the first valid one
|
||||
// wins (parse_tool_call takes requests[0]).
|
||||
func NormalizeTextToolCalls(text string) []*ParsedCall {
|
||||
var out []*ParsedCall
|
||||
for _, raw := range jsonObjects(text) {
|
||||
if p := requestFromEnvelope(raw); p != nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
// Fenced and XML wrappers: their content is JSON too — the raw scan
|
||||
// above already catches them since the braces survive wrapping. The
|
||||
// wrapper detection matters only for span extraction (visible-text
|
||||
// removal), which is the caller's concern.
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseToolCall — first validated call in the legacy shape, or nil.
|
||||
func ParseToolCall(response string) *ParsedCall {
|
||||
requests := NormalizeTextToolCalls(response)
|
||||
if len(requests) == 0 {
|
||||
return nil
|
||||
}
|
||||
return requests[0]
|
||||
}
|
||||
|
||||
// FormatToolResult — the model-facing envelope. PARITY: exact prefixes.
|
||||
func FormatToolResult(result map[string]interface{}) string {
|
||||
if ok, _ := result["success"].(bool); ok {
|
||||
content, _ := result["result"].(string)
|
||||
return fmt.Sprintf("[Tool result]\n%s", content)
|
||||
}
|
||||
errMsg, _ := result["error"].(string)
|
||||
if errMsg == "" {
|
||||
errMsg = "Unknown error"
|
||||
}
|
||||
return fmt.Sprintf("[Tool error]\n%s", errMsg)
|
||||
}
|
||||
|
||||
var _ = strings.TrimSpace // reserved for the visible-text port
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
// paths.go — the tool-executor's security core. PARITY ports of
|
||||
// tool_executor.py's resolve_path, is_path_allowed, validate_read_path,
|
||||
// validate_write_path — including the TRUTHFUL-REASON semantics (a blocked
|
||||
// dir under an allowed dir reports the blocklist, not "outside allowed";
|
||||
// a write denial names the allowed dirs so the model can self-correct).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Security configuration — PARITY with tool_executor.py:44-65.
|
||||
var AllowedReadDirs = []string{
|
||||
expandHome("~"),
|
||||
"/mnt/workdrive",
|
||||
"/tmp",
|
||||
}
|
||||
|
||||
var AllowedWriteDirs = []string{
|
||||
expandHome("~"),
|
||||
"/mnt/workdrive",
|
||||
}
|
||||
|
||||
var BlockedDirs = []string{
|
||||
expandHome("~/.ssh"),
|
||||
expandHome("~/.gnupg"),
|
||||
"/etc",
|
||||
"/var",
|
||||
"/proc",
|
||||
"/sys",
|
||||
}
|
||||
|
||||
func expandHome(p string) string {
|
||||
if strings.HasPrefix(p, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
return filepath.Join(home, p[1:])
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ResolvePath — absolute, symlinks and .. resolved.
|
||||
func ResolvePath(path string) string {
|
||||
expanded := expandHome(path)
|
||||
abs, err := filepath.Abs(expanded)
|
||||
if err != nil {
|
||||
return expanded
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
||||
return resolved
|
||||
}
|
||||
// Non-existent paths can't be symlink-resolved; clean them instead.
|
||||
return filepath.Clean(abs)
|
||||
}
|
||||
|
||||
// isRelativeOrEqual — Python's Path.is_relative_to + equality.
|
||||
func isRelativeOrEqual(path, base string) bool {
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return rel == "." || (!strings.HasPrefix(rel, "..") && rel != "")
|
||||
}
|
||||
|
||||
// IsPathAllowed — blocked dirs checked FIRST, then allowed dirs.
|
||||
func IsPathAllowed(path string, allowedDirs []string) bool {
|
||||
path = ResolvePath(path)
|
||||
for _, blocked := range BlockedDirs {
|
||||
bp := ResolvePath(blocked)
|
||||
if isRelativeOrEqual(path, bp) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, allowed := range allowedDirs {
|
||||
ap := ResolvePath(allowed)
|
||||
if isRelativeOrEqual(path, ap) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateReadPath — (ok, message, resolved). Message parity: "Access
|
||||
// denied: … is outside allowed directories" / "Not found: …" / "Not a file: …".
|
||||
func ValidateReadPath(path string) (bool, string, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if !IsPathAllowed(resolved, AllowedReadDirs) {
|
||||
return false, fmt.Sprintf("Access denied: %s is outside allowed directories", path), ""
|
||||
}
|
||||
info, err := os.Stat(resolved)
|
||||
if os.IsNotExist(err) {
|
||||
return false, fmt.Sprintf("Not found: %s", path), ""
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Not found: %s", path), ""
|
||||
}
|
||||
if info.IsDir() {
|
||||
return false, fmt.Sprintf("Not a file: %s", path), ""
|
||||
}
|
||||
return true, "", resolved
|
||||
}
|
||||
|
||||
// ValidateWritePath — blocked-under-allowed gets the PROTECTED message
|
||||
// (truthful reason, one-step self-correction); plain outside gets the
|
||||
// allowed-dirs message (P0-05 P1).
|
||||
func ValidateWritePath(path string) (bool, string, string) {
|
||||
resolved := ResolvePath(path)
|
||||
for _, blocked := range BlockedDirs {
|
||||
bp := ResolvePath(blocked)
|
||||
if isRelativeOrEqual(resolved, bp) {
|
||||
return false, fmt.Sprintf(
|
||||
"Access denied: %s is in a protected directory (%s). "+
|
||||
"This directory is always blocked, even though it is inside an "+
|
||||
"allowed write directory. Choose a different location.", path, blocked), ""
|
||||
}
|
||||
}
|
||||
if !IsPathAllowed(resolved, AllowedWriteDirs) {
|
||||
return false, fmt.Sprintf(
|
||||
"Access denied: %s is outside allowed write directories: %v. "+
|
||||
"Write inside one of those directories instead.", path, AllowedWriteDirs), ""
|
||||
}
|
||||
return true, "", resolved
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package main
|
||||
|
||||
// prepare.go — the approval lifecycle. PARITY ports of prepare_tool /
|
||||
// _consume_prepared / approve_tool / reject_tool / the PreparedTool record.
|
||||
// The canonical digest (tool + args) and the path digest bind an approval to
|
||||
// an EXACT operation and target — modification after preview invalidates it.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PreparedTool — a side-effect-free preview awaiting approval.
|
||||
type PreparedTool struct {
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
OperationDigest string `json:"operation_digest"`
|
||||
CreatedAt float64 `json:"created_at"`
|
||||
ExpiresAt float64 `json:"expires_at"`
|
||||
RequestedPath string `json:"requested_path,omitempty"`
|
||||
ResolvedPath string `json:"resolved_path,omitempty"`
|
||||
PathDigest string `json:"path_digest,omitempty"`
|
||||
}
|
||||
|
||||
// Read-only tools execute immediately; everything else is prepared.
|
||||
var readOnlyTools = map[string]bool{
|
||||
"read_file": true, "list_directory": true, "glob_files": true,
|
||||
"grep_files": true, "run_command": true,
|
||||
}
|
||||
|
||||
// DefaultApprovalTTL — seconds (parity with DEFAULT_APPROVAL_TTL).
|
||||
const DefaultApprovalTTL = 300.0
|
||||
|
||||
// CanonicalDigest — sha256 of the canonical JSON:
|
||||
// json.dumps({"name": t, "arguments": a}, sort_keys=True,
|
||||
//
|
||||
// separators=(",", ":"), ensure_ascii=False) — FULL 64-hex.
|
||||
//
|
||||
// PARITY-CRITICAL: the digest must be byte-identical with Python's
|
||||
// _canonical_digest (a mismatch would reject every real approval).
|
||||
func CanonicalDigest(toolName string, args map[string]interface{}) string {
|
||||
canonical := canonicalJSON(map[string]interface{}{"name": toolName, "arguments": args})
|
||||
sum := sha256.Sum256([]byte(canonical))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// canonicalJSON — Python json.dumps(sort_keys=True, separators=(",",":"))
|
||||
// semantics: compact separators, sorted keys, strings escaped per JSON.
|
||||
func canonicalJSON(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
parts[i] = jsonEsc(k) + ":" + canonicalJSON(t[k])
|
||||
}
|
||||
return "{" + strings.Join(parts, ",") + "}"
|
||||
case []interface{}:
|
||||
parts := make([]string, len(t))
|
||||
for i, e := range t {
|
||||
parts[i] = canonicalJSON(e)
|
||||
}
|
||||
return "[" + strings.Join(parts, ",") + "]"
|
||||
case string:
|
||||
return jsonEsc(t)
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
// Python json encodes integral floats as ints (1 → "1").
|
||||
if t == math.Trunc(t) && !math.IsInf(t, 0) {
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
}
|
||||
return strconv.FormatFloat(t, 'g', -1, 64)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case nil:
|
||||
return "null"
|
||||
default:
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// jsonEsc — Python-compatible JSON string escaping (ensure_ascii=False:
|
||||
// non-ASCII passes through raw).
|
||||
func jsonEsc(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
b.WriteString("\\\"")
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PathDigest — content-bound digest (PARITY with _path_digest):
|
||||
//
|
||||
// missing path → sha256("missing")
|
||||
// file → sha256("file\0" + content chunks)
|
||||
// other → sha256("other\0mode\0size\0mtime_ns")
|
||||
func PathDigest(resolved string) string {
|
||||
info, err := os.Stat(resolved)
|
||||
if os.IsNotExist(err) {
|
||||
sum := sha256.Sum256([]byte("missing"))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
h := sha256.New()
|
||||
h.Write([]byte("file\x00"))
|
||||
if f, err := os.Open(resolved); err == nil {
|
||||
defer f.Close()
|
||||
buf := make([]byte, 1024*1024)
|
||||
for {
|
||||
n, err := f.Read(buf)
|
||||
if n > 0 {
|
||||
h.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
if err == nil {
|
||||
marker := fmt.Sprintf("other\x00%d\x00%d\x00%d", info.Mode(), info.Size(), info.ModTime().UnixNano())
|
||||
sum := sha256.Sum256([]byte(marker))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
sum := sha256.Sum256([]byte("missing"))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ApprovalStore — consumed tokens (parity with _CONSUMED_APPROVALS).
|
||||
type ApprovalStore struct {
|
||||
consumed map[string]bool
|
||||
}
|
||||
|
||||
func NewApprovalStore() *ApprovalStore { return &ApprovalStore{consumed: map[string]bool{}} }
|
||||
|
||||
// ConsumePrepared — one-shot approval consumption with immediate
|
||||
// revalidation. Returns "" on success or the failure reason.
|
||||
func (a *ApprovalStore) ConsumePrepared(p PreparedTool, now float64) string {
|
||||
if a.consumed[p.Token] {
|
||||
return "Approval already resolved"
|
||||
}
|
||||
a.consumed[p.Token] = true
|
||||
if now > p.ExpiresAt {
|
||||
return "Approval expired"
|
||||
}
|
||||
if CanonicalDigest(p.Name, p.Arguments) != p.OperationDigest {
|
||||
return "Pending operation was modified"
|
||||
}
|
||||
if p.RequestedPath != "" {
|
||||
ok, message, resolved := ValidateWritePath(p.RequestedPath)
|
||||
if !ok || resolved == "" {
|
||||
return message
|
||||
}
|
||||
if resolved != p.ResolvedPath {
|
||||
return "Target path changed after preview"
|
||||
}
|
||||
if PathDigest(resolved) != p.PathDigest {
|
||||
return "Target changed after preview"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ApproveTool — consume + revalidate, then the caller performs the side
|
||||
// effect. PARITY with approve_tool's envelope.
|
||||
func (a *ApprovalStore) ApproveTool(p PreparedTool, now float64) map[string]interface{} {
|
||||
if err := a.ConsumePrepared(p, now); err != "" {
|
||||
return map[string]interface{}{"success": false, "error": err}
|
||||
}
|
||||
return map[string]interface{}{"success": true, "result": fmt.Sprintf("Approved: %s", p.Name)}
|
||||
}
|
||||
|
||||
// RejectTool — resolve a pending approval as rejected.
|
||||
func (a *ApprovalStore) RejectTool(p PreparedTool) map[string]interface{} {
|
||||
if a.consumed[p.Token] {
|
||||
return map[string]interface{}{"success": false, "error": "Approval already resolved"}
|
||||
}
|
||||
a.consumed[p.Token] = true
|
||||
return map[string]interface{}{"success": true, "result": fmt.Sprintf("Rejected: %s", p.Name)}
|
||||
}
|
||||
|
||||
// NewPreparedTool — build the preview record (digests + TTL).
|
||||
func NewPreparedTool(toolName string, args map[string]interface{}, requestedPath, resolvedPath string, now float64, ttl float64) PreparedTool {
|
||||
p := PreparedTool{
|
||||
Token: newToken(),
|
||||
Name: toolName,
|
||||
Arguments: args,
|
||||
OperationDigest: CanonicalDigest(toolName, args),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now + ttl,
|
||||
RequestedPath: requestedPath,
|
||||
ResolvedPath: resolvedPath,
|
||||
}
|
||||
if resolvedPath != "" {
|
||||
p.PathDigest = PathDigest(resolvedPath)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func newToken() string {
|
||||
return fmt.Sprintf("%x", time.Now().UnixNano()) // uuid-equivalent uniqueness for one process
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
// prompt.go — batch 6: the tool-prompt builder and the TOOLS registry.
|
||||
// The registry is DATA (ported from the Python oracle as embedded JSON —
|
||||
// tools_registry.json, generated live 2026-10-07); the builder is logic.
|
||||
// PARITY: the prompt is byte-identical with build_tool_prompt's output
|
||||
// (verified in truth_prompt.json).
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed tools_registry.json
|
||||
var toolsRegistryJSON []byte
|
||||
|
||||
// ToolDef — one entry of the registry.
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters struct {
|
||||
Properties map[string]struct {
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
} `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
} `json:"parameters"`
|
||||
}
|
||||
|
||||
// Tools — the parsed registry (order preserved).
|
||||
var Tools []ToolDef
|
||||
|
||||
func init() {
|
||||
if err := jsonUnmarshal(toolsRegistryJSON, &Tools); err != nil {
|
||||
panic("tools_registry.json: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// BuildToolPrompt — the model-facing tool definitions section.
|
||||
func BuildToolPrompt() string {
|
||||
var lines []string
|
||||
lines = append(lines, "## Available Tools", "")
|
||||
lines = append(lines, "You can use these tools by responding with a JSON object:")
|
||||
lines = append(lines, "```json\n{\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}}\n```\n")
|
||||
lines = append(lines, "After receiving a tool result, continue your response normally.\n")
|
||||
|
||||
for _, tool := range Tools {
|
||||
lines = append(lines, "### "+tool.Name)
|
||||
lines = append(lines, tool.Description)
|
||||
required := map[string]bool{}
|
||||
for _, r := range tool.Parameters.Required {
|
||||
required[r] = true
|
||||
}
|
||||
// PARITY: parameter order follows the JSON object order of the
|
||||
// registry — Go maps lose order, so iterate via the raw JSON.
|
||||
for _, pname := range paramOrder(tool.Name) {
|
||||
pinfo, ok := tool.Parameters.Properties[pname]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
req := " (optional)"
|
||||
if required[pname] {
|
||||
req = " (required)"
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(" - `%s`: %s%s", pname, pinfo.Description, req))
|
||||
}
|
||||
lines = append(lines, "")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package main
|
||||
|
||||
// quality.go — the quality-gate tools. PARITY ports of _detect_language,
|
||||
// _lint_file, _run_tests, _format_code with the same LANGUAGE_CONFIG table
|
||||
// and the same shell-invocation semantics (shell=True in Python → sh -c
|
||||
// here), same timeouts (lint 30s, tests 120s).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LanguageConfig — PARITY with tool_executor.py:1215.
|
||||
type LanguageConfig struct {
|
||||
Lint string
|
||||
Format string
|
||||
Test string
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
var languageConfig = map[string]LanguageConfig{
|
||||
"python": {Lint: "ruff check", Format: "ruff format", Test: "python -m pytest", Extensions: []string{".py"}},
|
||||
"javascript": {Lint: "eslint", Format: "prettier --write", Test: "npm test", Extensions: []string{".js", ".jsx"}},
|
||||
"typescript": {Lint: "eslint", Format: "prettier --write", Test: "npm test", Extensions: []string{".ts", ".tsx"}},
|
||||
"go": {Lint: "golangci-lint run", Format: "gofmt -w", Test: "go test ./...", Extensions: []string{".go"}},
|
||||
"rust": {Lint: "cargo clippy", Format: "cargo fmt", Test: "cargo test", Extensions: []string{".rs"}},
|
||||
}
|
||||
|
||||
// DetectLanguage — by file extension (lowercased).
|
||||
func DetectLanguage(path string) string {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
for lang, cfg := range languageConfig {
|
||||
for _, e := range cfg.Extensions {
|
||||
if ext == e {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// shellRun — Python subprocess.run(shell=True) equivalent.
|
||||
func shellRun(command string, cwd string, timeout time.Duration) (int, string, string) {
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
if cwd != "" {
|
||||
cmd.Dir = cwd
|
||||
}
|
||||
var outBuf, errBuf strings.Builder
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &errBuf
|
||||
done := make(chan error, 1)
|
||||
cmd.Start()
|
||||
go func() { done <- cmd.Wait() }()
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
cmd.Process.Kill()
|
||||
return -1, outBuf.String(), "TIMEOUT"
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return ee.ExitCode(), outBuf.String(), errBuf.String()
|
||||
}
|
||||
return 1, outBuf.String(), err.Error()
|
||||
}
|
||||
return 0, outBuf.String(), errBuf.String()
|
||||
}
|
||||
}
|
||||
|
||||
// LintFile — run the configured linter; "✓ <name> — no issues" on clean.
|
||||
func LintFile(path, language string) (bool, string) {
|
||||
ok, msg, resolved := ValidateReadPath(path)
|
||||
if !ok || resolved == "" {
|
||||
return false, msg
|
||||
}
|
||||
if language == "" {
|
||||
language = DetectLanguage(resolved)
|
||||
}
|
||||
cfg, known := languageConfig[language]
|
||||
if !known {
|
||||
return true, fmt.Sprintf("No linter configured for %s", filepath.Ext(resolved))
|
||||
}
|
||||
code, stdout, stderr := shellRun(cfg.Lint+" "+resolved, "", 30*time.Second)
|
||||
if code == 0 {
|
||||
return true, fmt.Sprintf("✓ %s — no issues", filepath.Base(resolved))
|
||||
}
|
||||
out := stdout
|
||||
if out == "" {
|
||||
out = stderr
|
||||
}
|
||||
return false, out
|
||||
}
|
||||
|
||||
// RunTests — explicit command or the language default; cwd = file's parent
|
||||
// or the dir itself. 120s timeout (parity), stderr appended to output.
|
||||
func RunTests(path, command string) (bool, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if command == "" {
|
||||
language := DetectLanguage(resolved)
|
||||
if cfg, known := languageConfig[language]; known {
|
||||
command = cfg.Test
|
||||
} else {
|
||||
return false, fmt.Sprintf("No test runner configured for %s", filepath.Ext(resolved))
|
||||
}
|
||||
}
|
||||
cwd := resolved
|
||||
if info, err := os.Stat(resolved); err == nil && !info.IsDir() {
|
||||
cwd = filepath.Dir(resolved)
|
||||
}
|
||||
code, stdout, stderr := shellRun(command, cwd, 120*time.Second)
|
||||
output := stdout
|
||||
if stderr != "" && stderr != "TIMEOUT" {
|
||||
output += "\n" + stderr
|
||||
}
|
||||
if stderr == "TIMEOUT" {
|
||||
return false, "Tests timed out (120s)"
|
||||
}
|
||||
if code == 0 {
|
||||
if output == "" {
|
||||
return true, "All tests passed"
|
||||
}
|
||||
return true, output
|
||||
}
|
||||
if output == "" {
|
||||
return false, "Tests failed"
|
||||
}
|
||||
return false, output
|
||||
}
|
||||
|
||||
// FormatCode — run the configured formatter (writes in place).
|
||||
func FormatCode(path, language string) (bool, string) {
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if !ok || resolved == "" {
|
||||
return false, msg
|
||||
}
|
||||
if language == "" {
|
||||
language = DetectLanguage(resolved)
|
||||
}
|
||||
cfg, known := languageConfig[language]
|
||||
if !known {
|
||||
return true, fmt.Sprintf("No formatter configured for %s", filepath.Ext(resolved))
|
||||
}
|
||||
code, stdout, stderr := shellRun(cfg.Format+" "+resolved, "", 30*time.Second)
|
||||
if code == 0 {
|
||||
return true, fmt.Sprintf("✓ %s — formatted", filepath.Base(resolved))
|
||||
}
|
||||
out := stdout
|
||||
if out == "" {
|
||||
out = stderr
|
||||
}
|
||||
return false, out
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,492 @@
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path exists and is a readable file"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Line number to start from (1-indexed, optional)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max lines to return (default 100, optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"search string matches exactly once"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites the target file atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"search": {
|
||||
"type": "string",
|
||||
"description": "Exact text to find (must match file content)"
|
||||
},
|
||||
"replace": {
|
||||
"type": "string",
|
||||
"description": "Text to replace with"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"search",
|
||||
"replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory"
|
||||
],
|
||||
"side_effects": [
|
||||
"writes the full file content atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern (e.g. '**/*.py', 'src/**/*.ts')"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional, defaults to cwd)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Regex pattern to search for"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional)"
|
||||
},
|
||||
"include": {
|
||||
"type": "string",
|
||||
"description": "File pattern to include (e.g. '*.py', '*.{ts,tsx}')"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path is a readable directory"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the directory"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lint_file",
|
||||
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"file exists",
|
||||
"a linter is configured for the language"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional, auto-detected from extension)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"a test runner is configured for the project"
|
||||
],
|
||||
"side_effects": [
|
||||
"runs project test commands which may create or delete files"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to test"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Custom test command (optional, overrides auto-detect)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"a formatter is configured for the language"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites files in place to the formatter's style"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to format"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"staged": {
|
||||
"type": "boolean",
|
||||
"description": "Show staged changes (default: unstaged)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file to diff (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of commits (default 10)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file history (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"working directory is a git repository",
|
||||
"there are changes to commit"
|
||||
],
|
||||
"side_effects": [
|
||||
"creates a commit on the current branch"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Commit message"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Specific files to commit (optional, defaults to all modified)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"target directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Root directory to map"
|
||||
},
|
||||
"languages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Filter by languages (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"operator approves the command",
|
||||
"required binaries are on PATH"
|
||||
],
|
||||
"side_effects": [
|
||||
"executes arbitrary commands with their full side effects"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command text; shell syntax is allowed only with shell=true"
|
||||
},
|
||||
"argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Argument vector for shell-free execution"
|
||||
},
|
||||
"shell": {
|
||||
"type": "boolean",
|
||||
"description": "Explicitly request the user-approved shell execution path"
|
||||
},
|
||||
"workdir": {
|
||||
"type": "string",
|
||||
"description": "Working directory (optional)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Timeout in seconds (default 30)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"persona MEMORY.md exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"memory store database is available"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for memory"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results (default 5)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"growth log file exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max entries (default 10)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"recent response text provided"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Recent response text to analyze"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find_capability",
|
||||
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"a capability query describing the needed outcome"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "What you need to do, e.g. 'run the test suite' or 'commit my changes'"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)"
|
||||
},
|
||||
"fixtures": {
|
||||
"files": [
|
||||
{
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/a.txt",
|
||||
"content": "the spine rebuild parity\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/b.log",
|
||||
"content": "spine appears here too\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/file.txt",
|
||||
"content": "content\n"
|
||||
}
|
||||
],
|
||||
"dirs": [
|
||||
"{{TMP}}/adir"
|
||||
]
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"fn": "read_window",
|
||||
"name": "mid",
|
||||
"input": {
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"offset": 3,
|
||||
"limit": 5
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "[showing from line 3]\n3: line 3\n4: line 4\n5: line 5\n6: line 6\n7: line 7\n\n[showing lines 3-7 of 21 total]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "read_window",
|
||||
"name": "default",
|
||||
"input": {
|
||||
"path": "{{TMP}}/w.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"lines": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "diff",
|
||||
"name": "simple",
|
||||
"input": {
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"new_content": "line 1\nCHANGED\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
"expected": {
|
||||
"diff_head": [
|
||||
"--- a/w.txt",
|
||||
"+++ b/w.txt",
|
||||
"@@ -1,5 +1,5 @@"
|
||||
],
|
||||
"has_minus": true,
|
||||
"has_plus": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
// truth_fileops_test.go — batch-2 parity gate: windowed read, diff shape,
|
||||
// against truth_fileops.json (live Python oracle, regenerated hermetically
|
||||
// 2026-10-07 — contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileOpsParity(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_fileops.json")
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Name {
|
||||
case "mid":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, result := ReadFile(path, truthInt(c.Input, "offset"), truthInt(c.Input, "limit"))
|
||||
want := truthStr(c.Expected, "result")
|
||||
if !ok || result != want {
|
||||
t.Errorf("mid:\n got %q\nwant %q", result, want)
|
||||
}
|
||||
case "default":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, result := ReadFile(path, 1, 100)
|
||||
wantLines := truthInt(c.Expected, "lines")
|
||||
gotLines := len(strings.Split(result, "\n"))
|
||||
if !ok || gotLines != wantLines {
|
||||
t.Errorf("default: ok=%v lines=%d want %d", ok, gotLines, wantLines)
|
||||
}
|
||||
case "simple":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
edited := truthStr(c.Input, "new_content")
|
||||
e := c.Expected
|
||||
diff := GenerateDiff(path, edited)
|
||||
lines := strings.Split(diff, "\n")
|
||||
wantHead := e["diff_head"].([]interface{})
|
||||
if lines[0] != wantHead[0].(string) ||
|
||||
lines[1] != wantHead[1].(string) ||
|
||||
lines[2] != wantHead[2].(string) {
|
||||
t.Errorf("simple diff head:\n got %v\nwant %v", lines[:3], wantHead)
|
||||
}
|
||||
if hasMinus, _ := e["has_minus"].(bool); hasMinus {
|
||||
found := false
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "-line 2") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("simple diff missing -line 2: %q", diff)
|
||||
}
|
||||
}
|
||||
if hasPlus, _ := e["has_plus"].(bool); hasPlus {
|
||||
found := false
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "+CHANGED") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("simple diff missing +CHANGED: %q", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteApprovalFlow — write_file NEVER writes; only ApplyContent does.
|
||||
func TestWriteApprovalFlow(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_fileops.json")
|
||||
_ = doc
|
||||
dir := wRoot
|
||||
if dir == "" {
|
||||
dir = tmpRoot
|
||||
}
|
||||
target := filepath.Join(dir, "approval-flow.txt")
|
||||
res := WriteFile(target, "hello world")
|
||||
if res["success"] != true {
|
||||
// Outside allowed dirs in this environment — the denial IS parity.
|
||||
if _, hasErr := res["error"]; !hasErr {
|
||||
t.Fatalf("write preview: neither success nor error: %v", res)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, statErr := os.Stat(target); statErr == nil {
|
||||
t.Fatal("write_file preview must NOT create the file")
|
||||
}
|
||||
if _, has := res["needs_confirm"]; !has {
|
||||
t.Errorf("preview must carry needs_confirm: %v", res)
|
||||
}
|
||||
applied := ApplyContent(target, "hello world")
|
||||
if applied["success"] != true {
|
||||
t.Fatalf("apply failed: %v", applied)
|
||||
}
|
||||
data, _ := os.ReadFile(target)
|
||||
if string(data) != "hello world" {
|
||||
t.Errorf("apply wrote %q", data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
// truth_harness_test.go — hermetic fixture/substitution harness for the
|
||||
// truth batteries (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
//
|
||||
// The truth JSONs carry {{TMP}}/{{WROOT}} placeholders (see
|
||||
// scripts/gen_tool_executor_truth.py): the generator ran the live Python
|
||||
// oracle under ITS allowed roots; this harness recreates the same fixture
|
||||
// content under the GO SIDE'S own allowed roots at runtime, substitutes
|
||||
// placeholders everywhere, and the tests compare exactly. No host path
|
||||
// crosses environments — the CI runner user needs only its own home + tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// truthCase — the shared case shape across the batteries.
|
||||
type truthCase struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
|
||||
type truthFixtures struct {
|
||||
Files []struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
} `json:"files"`
|
||||
Dirs []string `json:"dirs"`
|
||||
}
|
||||
|
||||
type truthDoc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
Fixtures truthFixtures `json:"fixtures"`
|
||||
Cases []truthCase `json:"cases"`
|
||||
}
|
||||
|
||||
// loadTruth — read a truth doc, recreate its fixtures under runtime roots,
|
||||
// return the doc plus the roots for substitution.
|
||||
func loadTruth(t *testing.T, name string) (truthDoc, string, string) {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("%s missing: %v", name, err)
|
||||
}
|
||||
var doc truthDoc
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("%s unparseable: %v", name, err)
|
||||
}
|
||||
tmpRoot := mkTruthRoot(t, AllowedReadDirs)
|
||||
wRoot := mkTruthRoot(t, AllowedWriteDirs)
|
||||
for _, f := range doc.Fixtures.Files {
|
||||
p := substRoots(f.Path, tmpRoot, wRoot)
|
||||
if err := os.WriteFile(p, []byte(substRoots(f.Content, tmpRoot, wRoot)), 0o644); err != nil {
|
||||
t.Fatalf("fixture %s: %v", p, err)
|
||||
}
|
||||
}
|
||||
for _, d := range doc.Fixtures.Dirs {
|
||||
if err := os.MkdirAll(substRoots(d, tmpRoot, wRoot), 0o755); err != nil {
|
||||
t.Fatalf("fixture dir %s: %v", d, err)
|
||||
}
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(tmpRoot)
|
||||
os.RemoveAll(wRoot)
|
||||
})
|
||||
return doc, tmpRoot, wRoot
|
||||
}
|
||||
|
||||
// mkTruthRoot — first creatable temp dir among the allowed dirs (the same
|
||||
// resolution order the generator used).
|
||||
func mkTruthRoot(t *testing.T, dirs []string) string {
|
||||
t.Helper()
|
||||
for _, d := range dirs {
|
||||
p, err := os.MkdirTemp(d, "rune-truth-")
|
||||
if err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
t.Fatalf("no creatable root among %v", dirs)
|
||||
return ""
|
||||
}
|
||||
|
||||
// substRoots — replace the truth placeholders with the runtime roots.
|
||||
func substRoots(s, tmpRoot, wRoot string) string {
|
||||
s = strings.ReplaceAll(s, "{{TMP}}", tmpRoot)
|
||||
s = strings.ReplaceAll(s, "{{WROOT}}", wRoot)
|
||||
return s
|
||||
}
|
||||
|
||||
// truthStr / truthInt — typed accessors with the zero value on absence
|
||||
// (a missing string field is "" in the cases we run).
|
||||
func truthStr(m map[string]interface{}, k string) string {
|
||||
v, _ := m[k].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func truthInt(m map[string]interface{}, k string) int {
|
||||
v, _ := m[k].(float64)
|
||||
return int(v)
|
||||
}
|
||||
|
||||
// sortedLines — deterministic comparison for cases whose oracle order is
|
||||
// OS-scheduling dependent (the recursive search walk).
|
||||
func sortedLines(s string) []string {
|
||||
lines := strings.Split(s, "\n")
|
||||
sort.Strings(lines)
|
||||
return lines
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)"
|
||||
},
|
||||
"fixtures": {
|
||||
"files": [
|
||||
{
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/a.txt",
|
||||
"content": "the spine rebuild parity\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/b.log",
|
||||
"content": "spine appears here too\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/file.txt",
|
||||
"content": "content\n"
|
||||
}
|
||||
],
|
||||
"dirs": [
|
||||
"{{TMP}}/adir"
|
||||
]
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"fn": "list_dir",
|
||||
"name": "basic",
|
||||
"input": {
|
||||
"path": "{{TMP}}"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "📄 a.txt\n📁 adir/\n📄 b.log\n📄 file.txt\n📄 w.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "list_dir",
|
||||
"name": "pattern",
|
||||
"input": {
|
||||
"path": "{{TMP}}",
|
||||
"pattern": "*.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "📄 a.txt\n📄 file.txt\n📄 w.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grep",
|
||||
"name": "hit",
|
||||
"input": {
|
||||
"path": "{{TMP}}",
|
||||
"pattern": "spine"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "a.txt:1: the spine rebuild parity\nb.log:1: spine appears here too"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grep",
|
||||
"name": "miss",
|
||||
"input": {
|
||||
"path": "{{TMP}}",
|
||||
"pattern": "absent-token"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "(no matches)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "pipe_refused",
|
||||
"input": {
|
||||
"command": "ls -la | head"
|
||||
},
|
||||
"expected": {
|
||||
"success": false,
|
||||
"error": "Shell syntax requires explicit shell approval"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "plain_ok",
|
||||
"input": {
|
||||
"command": "ls -la"
|
||||
},
|
||||
"expected": {
|
||||
"success": true,
|
||||
"error": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "shell_ok",
|
||||
"input": {
|
||||
"command": "true",
|
||||
"shell": true
|
||||
},
|
||||
"expected": {
|
||||
"success": true,
|
||||
"error": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "shell_empty",
|
||||
"input": {
|
||||
"shell": true
|
||||
},
|
||||
"expected": {
|
||||
"success": false,
|
||||
"error": "Explicit shell command is required"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
// truth_ops_test.go — batch-3 parity gate: list/grep/cmd-gate against
|
||||
// truth_ops.json (live Python oracle, regenerated hermetically 2026-10-07
|
||||
// — contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
//
|
||||
// list_dir is EXACT (both sides sort entries). grep compares as sorted
|
||||
// line sets: the oracle's os.walk order is OS-scheduling dependent, not a
|
||||
// parity-relevant property (same discipline as the oracle-battery law —
|
||||
// expectations come from the oracle; only comparison order is normalized).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpsParity(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_ops.json")
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "list_dir":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
pattern := truthStr(c.Input, "pattern")
|
||||
ok, result := ListDirectory(path, pattern)
|
||||
wantOk := c.Expected["ok"].(bool)
|
||||
wantRes := truthStr(c.Expected, "result")
|
||||
if ok != wantOk {
|
||||
t.Errorf("%s: ok=%v want %v", c.Name, ok, wantOk)
|
||||
}
|
||||
if result != wantRes {
|
||||
t.Errorf("%s:\n got %q\nwant %q", c.Name, result, wantRes)
|
||||
}
|
||||
case "grep":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
pattern := truthStr(c.Input, "pattern")
|
||||
ok, result := GrepFiles(path, pattern, "")
|
||||
wantOk := c.Expected["ok"].(bool)
|
||||
wantRes := truthStr(c.Expected, "result")
|
||||
if ok != wantOk {
|
||||
t.Errorf("%s: ok=%v want %v", c.Name, ok, wantOk)
|
||||
}
|
||||
if wantRes == "(no matches)" {
|
||||
if result != "(no matches)" {
|
||||
t.Errorf("%s: got %q want (no matches)", c.Name, result)
|
||||
}
|
||||
continue
|
||||
}
|
||||
gotSorted := sortedLines(result)
|
||||
wantSorted := sortedLines(wantRes)
|
||||
if strings.Join(gotSorted, "\n") != strings.Join(wantSorted, "\n") {
|
||||
t.Errorf("%s (sorted sets):\n got %q\nwant %q", c.Name, result, wantRes)
|
||||
}
|
||||
case "cmd_gate":
|
||||
e := c.Expected
|
||||
var gotSuccess bool
|
||||
var gotErr string
|
||||
switch c.Name {
|
||||
case "pipe_refused":
|
||||
_, _, err := CommandGate("ls -la | head", nil, false)
|
||||
gotSuccess = err == ""
|
||||
gotErr = err
|
||||
case "plain_ok":
|
||||
argv, _, err := CommandGate("ls -la", nil, false)
|
||||
gotSuccess = err == "" && len(argv) == 2
|
||||
gotErr = err
|
||||
case "shell_ok":
|
||||
_, useShell, err := CommandGate("rm -rf /", nil, true)
|
||||
gotSuccess = err == "" && useShell
|
||||
gotErr = err
|
||||
case "shell_empty":
|
||||
_, _, err := CommandGate("", nil, true)
|
||||
gotSuccess = err == ""
|
||||
gotErr = err
|
||||
}
|
||||
if gotSuccess != e["success"].(bool) {
|
||||
t.Errorf("%s: success=%v want %v (err=%q)", c.Name, gotSuccess, e["success"], gotErr)
|
||||
}
|
||||
if wantErr, has := e["error"]; has && gotErr != wantErr.(string) {
|
||||
t.Errorf("%s: err got %q want %q", c.Name, gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrepTruncationMarker — the 50-match truncate marker is a hard parity
|
||||
// string; exercised hermetically (55 matching lines in one fixture file).
|
||||
func TestGrepTruncationMarker(t *testing.T) {
|
||||
dir := mkTruthRoot(t, AllowedReadDirs)
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
var sb strings.Builder
|
||||
for i := 0; i < 55; i++ {
|
||||
sb.WriteString("needle line\n")
|
||||
}
|
||||
if err := os.WriteFile(dir+"/trunc.txt", []byte(sb.String()), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ok, result := GrepFiles(dir, "needle", "")
|
||||
if !ok {
|
||||
t.Fatal("grep failed")
|
||||
}
|
||||
if !strings.HasSuffix(result, "\n[truncated — 50 matches]") {
|
||||
t.Errorf("truncation marker missing: %q", result[len(result)-60:])
|
||||
}
|
||||
if got := strings.Count(result, "needle"); got != 50 {
|
||||
t.Errorf("matches=%d want 50", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
[
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "bare_valid",
|
||||
"input": {
|
||||
"text": "{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "read_file",
|
||||
"args": {
|
||||
"path": "/tmp/x"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "fenced",
|
||||
"input": {
|
||||
"text": "```json\n{\"tool\": \"list_directory\", \"args\": {\"path\": \".\"}}\n```"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "list_directory",
|
||||
"args": {
|
||||
"path": "."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "xml",
|
||||
"input": {
|
||||
"text": "<tool_call>\n{\"tool\": \"run_command\", \"args\": {\"command\": \"ls\"}}\n</tool_call>"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "run_command",
|
||||
"args": {
|
||||
"command": "ls"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "inline",
|
||||
"input": {
|
||||
"text": "text before {\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/y\"}} text after"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "read_file",
|
||||
"args": {
|
||||
"path": "/tmp/y"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "none",
|
||||
"input": {
|
||||
"text": "no tool call here"
|
||||
},
|
||||
"expected": {
|
||||
"tool": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "format",
|
||||
"name": "ok",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"text": "[Tool result]\nthe output"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "format",
|
||||
"name": "err",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"text": "[Tool error]\nAccess denied: /etc"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
// truth_parse_test.go — batch-5 parity gate: protocol parsing and result
|
||||
// formatting against truth_parse.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch5ParseParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_parse.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_parse.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "parse":
|
||||
text, _ := c.Input["text"].(string)
|
||||
got := ParseToolCall(text)
|
||||
wantTool := c.Expected["tool"]
|
||||
if wantTool == nil {
|
||||
if got != nil {
|
||||
t.Errorf("%s: got %v want nil", c.Name, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got == nil {
|
||||
t.Errorf("%s: got nil want tool %v", c.Name, wantTool)
|
||||
continue
|
||||
}
|
||||
if got.Tool != wantTool.(string) {
|
||||
t.Errorf("%s: tool got %q want %q", c.Name, got.Tool, wantTool)
|
||||
}
|
||||
wantArgs, hasArgs := c.Expected["args"]
|
||||
if hasArgs && wantArgs != nil {
|
||||
wantMap := wantArgs.(map[string]interface{})
|
||||
if !reflect.DeepEqual(got.Args, wantMap) {
|
||||
t.Errorf("%s: args got %v want %v", c.Name, got.Args, wantMap)
|
||||
}
|
||||
}
|
||||
case "format":
|
||||
got := FormatToolResult(map[string]interface{}{
|
||||
"success": c.Name == "ok",
|
||||
"result": "the output",
|
||||
"error": "Access denied: /etc",
|
||||
})
|
||||
if got != c.Expected["text"].(string) {
|
||||
t.Errorf("%s:\n got %q\nwant %q", c.Name, got, c.Expected["text"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUnknownToolStillParses — validation is dispatch's job; the
|
||||
// parser accepts any tool name (fail-open parse, fail-closed dispatch).
|
||||
func TestParseUnknownToolStillParses(t *testing.T) {
|
||||
got := ParseToolCall(`{"tool": "definitely_not_a_tool", "args": {}}`)
|
||||
if got == nil || got.Tool != "definitely_not_a_tool" {
|
||||
t.Errorf("unknown tool must still parse: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)"
|
||||
},
|
||||
"fixtures": {
|
||||
"files": [
|
||||
{
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/a.txt",
|
||||
"content": "the spine rebuild parity\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/b.log",
|
||||
"content": "spine appears here too\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/file.txt",
|
||||
"content": "content\n"
|
||||
}
|
||||
],
|
||||
"dirs": [
|
||||
"{{TMP}}/adir"
|
||||
]
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"fn": "resolve",
|
||||
"name": "abs",
|
||||
"input": {
|
||||
"path": "{{TMP}}/file.txt"
|
||||
},
|
||||
"expected": {
|
||||
"resolved": "{{TMP}}/file.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_read",
|
||||
"name": "hit",
|
||||
"input": {
|
||||
"path": "{{TMP}}/file.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_read",
|
||||
"name": "miss",
|
||||
"input": {
|
||||
"path": "{{TMP}}/missing.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"msg": "Not found: {{TMP}}/missing.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_read",
|
||||
"name": "blocked",
|
||||
"input": {
|
||||
"path": "/etc/hostname"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_write",
|
||||
"name": "protected",
|
||||
"input": {
|
||||
"path": "/etc/x.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"msg_prefix": "Access denied: /etc/x.txt is in a protected direct"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_write",
|
||||
"name": "outside",
|
||||
"input": {
|
||||
"path": "/usr/share/x.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"msg_prefix": "Access denied: /usr/share/x.txt is outside allowed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_write",
|
||||
"name": "ok",
|
||||
"input": {
|
||||
"path": "{{WROOT}}/new.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"resolved": "{{WROOT}}/new.txt"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
// truth_paths_test.go — the tool-executor parity gate: path security
|
||||
// (truth_paths.json) and the digest algorithms (byte-verified against the
|
||||
// Python oracles _canonical_digest and _path_digest).
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathsParity(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_paths.json")
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "resolve":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
got := ResolvePath(path)
|
||||
want := substRoots(truthStr(c.Expected, "resolved"), tmpRoot, wRoot)
|
||||
if got != want {
|
||||
t.Errorf("%s: ResolvePath got %q want %q", c.Name, got, want)
|
||||
}
|
||||
case "validate_read":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, msg, _ := ValidateReadPath(path)
|
||||
if ok != c.Expected["ok"].(bool) {
|
||||
t.Errorf("%s: ok=%v want %v (msg=%q)", c.Name, ok, c.Expected["ok"], msg)
|
||||
}
|
||||
if wantMsg, has := c.Expected["msg"]; has {
|
||||
want := substRoots(wantMsg.(string), tmpRoot, wRoot)
|
||||
if msg != want {
|
||||
t.Errorf("%s: msg got %q want %q", c.Name, msg, want)
|
||||
}
|
||||
}
|
||||
case "validate_write":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if ok != c.Expected["ok"].(bool) {
|
||||
t.Errorf("%s: ok=%v want %v (msg=%q)", c.Name, ok, c.Expected["ok"], msg)
|
||||
}
|
||||
if prefix, has := c.Expected["msg_prefix"]; has {
|
||||
if !strings.HasPrefix(msg, prefix.(string)) {
|
||||
t.Errorf("%s: msg %q missing prefix %q", c.Name, msg, prefix)
|
||||
}
|
||||
}
|
||||
if wantRes, has := c.Expected["resolved"]; has && wantRes != nil {
|
||||
want := substRoots(wantRes.(string), tmpRoot, wRoot)
|
||||
if resolved != want {
|
||||
t.Errorf("%s: resolved got %q want %q", c.Name, resolved, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCanonicalDigestByteParity — the digest must be BYTE-IDENTICAL with
|
||||
// Python's _canonical_digest (computed via subprocess against the real
|
||||
// oracle). A mismatch here would reject every real approval.
|
||||
func TestCanonicalDigestByteParity(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]interface{}
|
||||
}{
|
||||
{"write_file", map[string]interface{}{"path": "/tmp/x", "content": "hello"}},
|
||||
{"edit_file", map[string]interface{}{"path": "/a b/c.txt", "old": "x\ny", "new": ""}},
|
||||
{"run_command", map[string]interface{}{"command": "ls -la", "cwd": "/tmp"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
payload, _ := json.Marshal(map[string]interface{}{"name": c.name, "arguments": c.args})
|
||||
want := pythonDigest(payload)
|
||||
got := CanonicalDigest(c.name, c.args)
|
||||
if got != want {
|
||||
t.Errorf("%s canonical digest:\n got %s\nwant %s (python)", c.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pythonDigest computes the digest the way Python's json.dumps does
|
||||
// (sort_keys, separators (",",":"), ensure_ascii=False) — computed here in
|
||||
// test-only Python for byte truth.
|
||||
func pythonDigest(payload []byte) string {
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal(payload, &m)
|
||||
script := `import json, hashlib, sys
|
||||
m = json.load(sys.stdin)
|
||||
print(hashlib.sha256(json.dumps(m, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest())`
|
||||
out, err := runPython(script, string(payload))
|
||||
if err != nil {
|
||||
return "PYTHON-UNAVAILABLE"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestPathDigestSemantics — missing/file/other branches.
|
||||
func TestPathDigestSemantics(t *testing.T) {
|
||||
missing := PathDigest("/nonexistent/really/not/here")
|
||||
want := sha256.Sum256([]byte("missing"))
|
||||
if missing != hex.EncodeToString(want[:]) {
|
||||
t.Error("missing path digest must be sha256('missing')")
|
||||
}
|
||||
f := t.TempDir() + "/f.txt"
|
||||
os.WriteFile(f, []byte("content"), 0o644)
|
||||
d1 := PathDigest(f)
|
||||
os.WriteFile(f, []byte("content2"), 0o644)
|
||||
d2 := PathDigest(f)
|
||||
if d1 == d2 {
|
||||
t.Error("file digest must change with content")
|
||||
}
|
||||
if len(d1) != 64 {
|
||||
t.Errorf("path digest length %d, want 64 (full sha256 hex)", len(d1))
|
||||
}
|
||||
_ = math.Pi
|
||||
}
|
||||
|
||||
// runPython — shell out to the real Python interpreter for byte-truth.
|
||||
func runPython(script, stdinData string) (string, error) {
|
||||
cmd := exec.Command("python3", "-c", script)
|
||||
cmd.Stdin = strings.NewReader(stdinData)
|
||||
out, err := cmd.Output()
|
||||
return strings.TrimSpace(string(out)), err
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"fn": "tool_prompt",
|
||||
"name": "full",
|
||||
"expected": {
|
||||
"prompt": "## Available Tools\n\nYou can use these tools by responding with a JSON object:\n```json\n{\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}}\n```\n\nAfter receiving a tool result, continue your response normally.\n\n### read_file\nRead a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).\n - `path`: Absolute path to the file (required)\n - `offset`: Line number to start from (1-indexed, optional) (optional)\n - `limit`: Max lines to return (default 100, optional) (optional)\n\n### edit_file\nSurgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.\n - `path`: Absolute path to the file (required)\n - `search`: Exact text to find (must match file content) (required)\n - `replace`: Text to replace with (required)\n\n### write_file\nWrite content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.\n - `path`: Absolute path to the file (required)\n - `content`: Content to write (required)\n\n### glob_files\nFind files matching a glob pattern. Returns file paths sorted by modification time.\n - `pattern`: Glob pattern (e.g. '**/*.py', 'src/**/*.ts') (required)\n - `path`: Directory to search in (optional, defaults to cwd) (optional)\n\n### grep_files\nSearch for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.\n - `pattern`: Regex pattern to search for (required)\n - `path`: Directory to search in (optional) (optional)\n - `include`: File pattern to include (e.g. '*.py', '*.{ts,tsx}') (optional)\n\n### list_directory\nList files and directories at a path. Returns names with / suffix for directories.\n - `path`: Absolute path to the directory (required)\n\n### lint_file\nLint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.\n - `path`: Absolute path to the file (required)\n - `language`: Language override (optional, auto-detected from extension) (optional)\n\n### run_tests\nRun tests for a file or project. Auto-detects test runner from language.\n - `path`: File or directory to test (required)\n - `command`: Custom test command (optional, overrides auto-detect) (optional)\n\n### format_code\nAuto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.\n - `path`: File or directory to format (required)\n - `language`: Language override (optional) (optional)\n\n### git_diff\nShow git diff for staged or unstaged changes.\n - `staged`: Show staged changes (default: unstaged) (optional)\n - `path`: Specific file to diff (optional) (optional)\n\n### git_log\nShow recent git commits.\n - `limit`: Number of commits (default 10) (optional)\n - `path`: Specific file history (optional) (optional)\n\n### git_commit\nCommit changes with a message. Auto-stages modified files.\n - `message`: Commit message (required)\n - `files`: Specific files to commit (optional, defaults to all modified) (optional)\n\n### repo_map\nGenerate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.\n - `path`: Root directory to map (required)\n - `languages`: Filter by languages (optional) (optional)\n\n### run_command\nRun an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.\n - `command`: Command text; shell syntax is allowed only with shell=true (optional)\n - `argv`: Argument vector for shell-free execution (optional)\n - `shell`: Explicitly request the user-approved shell execution path (optional)\n - `workdir`: Working directory (optional) (optional)\n - `timeout`: Timeout in seconds (default 30) (optional)\n\n### read_persona\nRead Rune's persona definition. Returns the full persona block with voice, identity, and rules.\n\n### query_memory\nQuery Rune's memory store. Returns relevant memories for a query.\n - `query`: Search query for memory (required)\n - `limit`: Max results (default 5) (optional)\n\n### check_growth\nCheck Rune's growth log. Returns recent growth entries and patterns.\n - `limit`: Max entries (default 10) (optional)\n\n### diagnose_behavior\nDiagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.\n - `text`: Recent response text to analyze (required)\n\n### find_capability\nSearch all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.\n - `query`: What you need to do, e.g. 'run the test suite' or 'commit my changes' (required)\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
// truth_prompt_test.go — batch-6 parity gate: the tool prompt is
|
||||
// byte-identical with build_tool_prompt's output (truth_prompt.json).
|
||||
|
||||
// REPAIR (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown): the
|
||||
// jsonUnmarshal/paramOrder/rawKeyOrder helpers moved to json_order.go —
|
||||
// defining them in a _test.go file starves the non-test build.
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToolPromptByteParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_prompt.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_prompt.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Name string `json:"name"`
|
||||
Expected struct {
|
||||
Prompt string `json:"prompt"`
|
||||
} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := BuildToolPrompt()
|
||||
if got != c.Expected.Prompt {
|
||||
// Find the first divergence for a readable failure.
|
||||
g, w := got, c.Expected.Prompt
|
||||
i := 0
|
||||
for i < len(g) && i < len(w) && g[i] == w[i] {
|
||||
i++
|
||||
}
|
||||
t.Errorf("prompt diverges at byte %d:\n got ...%q\nwant ...%q", i,
|
||||
g[max(0, i-40):min(len(g), i+60)], w[max(0, i-40):min(len(w), i+60)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestRegistryShape — 19 tools, order preserved, required fields present.
|
||||
func TestRegistryShape(t *testing.T) {
|
||||
if len(Tools) != 19 {
|
||||
t.Fatalf("registry: %d tools, want 19", len(Tools))
|
||||
}
|
||||
wantOrder := []string{"read_file", "edit_file", "write_file", "glob_files", "grep_files",
|
||||
"list_directory", "lint_file", "run_tests", "format_code", "git_diff", "git_log",
|
||||
"git_commit", "repo_map", "run_command", "read_persona", "query_memory",
|
||||
"check_growth", "diagnose_behavior", "find_capability"}
|
||||
for i, w := range wantOrder {
|
||||
if Tools[i].Name != w {
|
||||
t.Errorf("order[%d]: got %q want %q", i, Tools[i].Name, w)
|
||||
}
|
||||
}
|
||||
// spot-check required lists
|
||||
if len(Tools[0].Parameters.Required) != 1 || Tools[0].Parameters.Required[0] != "path" {
|
||||
t.Errorf("read_file required: %v", Tools[0].Parameters.Required)
|
||||
}
|
||||
if len(Tools[1].Parameters.Required) != 3 {
|
||||
t.Errorf("edit_file required: %v", Tools[1].Parameters.Required)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "py",
|
||||
"input": {
|
||||
"path": "/x/y/module.py"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "python"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "go",
|
||||
"input": {
|
||||
"path": "/x/main.GO"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "go"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "rust",
|
||||
"input": {
|
||||
"path": "/x/lib.rs"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "rust"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "ts",
|
||||
"input": {
|
||||
"path": "/x/app.tsx"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "unknown",
|
||||
"input": {
|
||||
"path": "/x/file.md"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "None"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
// truth_quality_test.go — batch-4 parity gate: language detection + gate
|
||||
// verdicts (live Python oracle for _detect_language; the command runners
|
||||
// are gated on tool availability, so the tests verify the DECISION layer).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch4Parity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_quality.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_quality.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "detect_language":
|
||||
path, _ := c.Input["path"].(string)
|
||||
got := DetectLanguage(path)
|
||||
want := c.Expected["lang"].(string)
|
||||
if want == "None" {
|
||||
want = ""
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s: DetectLanguage(%q) = %q want %q", c.Name, path, got, want)
|
||||
}
|
||||
case "lint_no_linter":
|
||||
path, _ := c.Input["path"].(string)
|
||||
ok, result := LintFile(path, "")
|
||||
if !ok || !strings.Contains(result, "No linter configured") {
|
||||
t.Errorf("%s: got (%v, %q)", c.Name, ok, result)
|
||||
}
|
||||
case "tests_no_runner":
|
||||
path, _ := c.Input["path"].(string)
|
||||
ok, result := RunTests(path, "")
|
||||
if ok || !strings.Contains(result, "No test runner configured") {
|
||||
t.Errorf("%s: got (%v, %q)", c.Name, ok, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
// events.go — batch 3: the reactive and proactive dispatch surfaces.
|
||||
// PARITY ports of route_event's event-neuron map and route_proactive's
|
||||
// query assembly. The final `route()` call stays on the caller side (it
|
||||
// needs embeddings + session state — I/O). The pure decision logic — which
|
||||
// keywords an event maps to, which queries system state produces — is here.
|
||||
|
||||
import "strings"
|
||||
|
||||
// EventNeuronMap — events trigger neuron topics reactively.
|
||||
// PARITY: byte-identical to warm_router.py route_event's map.
|
||||
var EventNeuronMap = map[string][]string{
|
||||
"task.completed": {"task", "growth", "lessons"},
|
||||
"task.failed": {"task", "debugging", "error"},
|
||||
"capability.failed": {"debugging", "error", "system"},
|
||||
"owner.correction": {"growth", "lessons", "persona"},
|
||||
"memory.created": {"knowledge", "learning"},
|
||||
"memory.conflict": {"contradiction", "knowledge"},
|
||||
"context.pressure": {"context", "management"},
|
||||
"model.unavailable": {"system", "fallback"},
|
||||
}
|
||||
|
||||
// EventQuery — the pseudo-query an event produces: mapped keywords + event
|
||||
// data as context (truncated to 200 chars, per the Python oracle).
|
||||
func EventQuery(eventType string, eventData string) (string, bool) {
|
||||
keywords, ok := EventNeuronMap[eventType]
|
||||
if !ok || len(keywords) == 0 {
|
||||
return "", false
|
||||
}
|
||||
query := strings.Join(keywords, " ")
|
||||
if eventData != "" {
|
||||
data := eventData
|
||||
if len(data) > 200 {
|
||||
data = data[:200]
|
||||
}
|
||||
query += " " + data
|
||||
}
|
||||
return query, true
|
||||
}
|
||||
|
||||
// ProactiveQueries — the query assembly from system state. PARITY with
|
||||
// route_proactive: top-3 task summaries (100 chars each), errors→diagnostic
|
||||
// query, commitments→reminder query. Commitment listing is caller I/O —
|
||||
// pass hasCommitments.
|
||||
func ProactiveQueries(activeTasks []map[string]interface{}, recentErrors bool, hasCommitments bool) []string {
|
||||
var queries []string
|
||||
if len(activeTasks) > 0 {
|
||||
n := len(activeTasks)
|
||||
if n > 3 {
|
||||
n = 3
|
||||
}
|
||||
for _, task := range activeTasks[:n] {
|
||||
summary, _ := task["summary"].(string)
|
||||
if summary != "" {
|
||||
if len(summary) > 100 {
|
||||
summary = summary[:100]
|
||||
}
|
||||
queries = append(queries, summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if recentErrors {
|
||||
queries = append(queries, "error debugging system")
|
||||
}
|
||||
if hasCommitments {
|
||||
queries = append(queries, "commitment reminder promise")
|
||||
}
|
||||
return queries
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
// score_neuron, route, and the warm neuron listing.
|
||||
//
|
||||
// Usage:
|
||||
// warm-router base-level <timestamps-csv> <now-iso>
|
||||
// warm-router score <neuron-json> <text> [--session-context <ctx>]
|
||||
// warm-router route <text> [--max-tokens N]
|
||||
//
|
||||
// warm-router base-level <timestamps-csv> <now-iso>
|
||||
// warm-router score <neuron-json> <text> [--session-context <ctx>]
|
||||
// warm-router route <text> [--max-tokens N]
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -58,11 +59,13 @@ func baseLevelFromUses(usedAtList []string, nowISO string) float64 {
|
||||
// ── Neuron scoring (parity: warm_router.py:487) ──
|
||||
|
||||
type WarmNeuron struct {
|
||||
Slug string `json:"slug"`
|
||||
Triggers []string `json:"triggers"`
|
||||
Text string `json:"text"`
|
||||
Importance float64 `json:"importance"`
|
||||
WashCount int `json:"wash_count"`
|
||||
Slug string `json:"slug"`
|
||||
Triggers []string `json:"triggers"`
|
||||
Text string `json:"text"`
|
||||
Importance float64 `json:"importance"`
|
||||
WashCount int `json:"wash_count"`
|
||||
ID int `json:"id"`
|
||||
BodyKnowledge string `json:"body_knowledge"`
|
||||
}
|
||||
|
||||
func scoreNeuron(neuron WarmNeuron, text string) map[string]interface{} {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
// neurons.go — batch 5: neuron listing mechanics. PARITY ports of
|
||||
// _parse_trigger and list_neurons' body-knowledge construction. The SQL
|
||||
// read stays caller-side (store seam); these are the pure transforms.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseTrigger — comma-split, trim, lowercase; empty parts dropped.
|
||||
// PARITY NOTE: the Python oracle does NOT special-case JSON arrays —
|
||||
// '["a","b"]' parses as the literal parts '["a"' and '"b"]'. This port
|
||||
// reproduces that exact behavior (fixture-verified).
|
||||
func ParseTrigger(raw string) []string {
|
||||
var out []string
|
||||
for _, t := range strings.Split(raw, ",") {
|
||||
t = strings.TrimSpace(strings.ToLower(t))
|
||||
if t != "" {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BodyKnowledge — the experiential context line (Souveraine enhancement).
|
||||
// PARITY: fires-on prefix (first 5 triggers), knows prefix (80-char
|
||||
// summary), then the wash-count lifecycle note.
|
||||
func BodyKnowledge(triggerList []string, summary string, washCount int) string {
|
||||
bk := ""
|
||||
if len(triggerList) > 0 {
|
||||
n := len(triggerList)
|
||||
if n > 5 {
|
||||
n = 5
|
||||
}
|
||||
bk = "Fires on: " + strings.Join(triggerList[:n], ", ") + ". "
|
||||
}
|
||||
if summary != "" {
|
||||
s := summary
|
||||
if len(s) > 80 {
|
||||
s = s[:80]
|
||||
}
|
||||
bk += "Knows: " + s + ". "
|
||||
}
|
||||
switch {
|
||||
case washCount >= 20:
|
||||
bk += "Proven — has survived many washes."
|
||||
case washCount >= 10:
|
||||
bk += "Established — building trust."
|
||||
case washCount > 0:
|
||||
bk += "Testing — still proving itself."
|
||||
default:
|
||||
bk += "Untested — new or rarely accessed."
|
||||
}
|
||||
return strings.TrimSpace(bk)
|
||||
}
|
||||
|
||||
// NeuronTokens — the estimated token cost (len/4, min 1).
|
||||
func NeuronTokens(fullBody string) int {
|
||||
t := len(fullBody) / 4
|
||||
if t < 1 {
|
||||
t = 1
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
// priming.go — batch 4: the ACTIVE warm-tier priming mechanics. PARITY
|
||||
// ports of warm_router.py's _prime_neurons (record + bloat guard),
|
||||
// _decay_primes (recency decay), and the session-state prune rules.
|
||||
//
|
||||
// Constants: PRIME_DECAY=0.6, PRIME_FLOOR=0.15, ASSOC_PRIME_WEIGHT=0.5,
|
||||
// 24h idle eviction, hard cap 40 sessions (evict fewest turns).
|
||||
//
|
||||
// State persistence (load/save_state) stays caller-side — this file owns
|
||||
// the pure decisions.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
const PrimeDecay = 0.6
|
||||
const PrimeFloor = 0.15
|
||||
const AssocPrimeWeight = 0.5
|
||||
const MaxSessions = 40
|
||||
|
||||
// SessionState — one session's priming record.
|
||||
type SessionState struct {
|
||||
Primed map[string]float64 `json:"primed"`
|
||||
Turns int `json:"turns"`
|
||||
LastActive string `json:"last_active"`
|
||||
}
|
||||
|
||||
// DecayPrimes — decay prior priming before each new turn. Mutates the map;
|
||||
// returns the surviving slugs sorted by score (descending, parity order).
|
||||
func DecayPrimes(primed map[string]float64) []string {
|
||||
for slug, score := range primed {
|
||||
decayed := score * PrimeDecay
|
||||
if decayed < PrimeFloor {
|
||||
delete(primed, slug)
|
||||
} else {
|
||||
primed[slug] = decayed
|
||||
}
|
||||
}
|
||||
slugs := make([]string, 0, len(primed))
|
||||
for slug := range primed {
|
||||
slugs = append(slugs, slug)
|
||||
}
|
||||
sort.Slice(slugs, func(i, j int) bool { return primed[slugs[i]] > primed[slugs[j]] })
|
||||
return slugs
|
||||
}
|
||||
|
||||
// PrimeNeurons — record fired slugs (max-score semantics) + turn increment.
|
||||
// Mutates sess in place; the caller persists.
|
||||
func PrimeNeurons(sess *SessionState, slugs []string, score float64, now time.Time) {
|
||||
if sess.Primed == nil {
|
||||
sess.Primed = map[string]float64{}
|
||||
}
|
||||
sess.Turns++
|
||||
sess.LastActive = now.UTC().Format("2006-01-02T15:04:05")
|
||||
for _, slug := range slugs {
|
||||
if cur, ok := sess.Primed[slug]; !ok || score > cur {
|
||||
sess.Primed[slug] = score
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PruneSessions — the bloat guard: drop empty sessions, evict idle >24h,
|
||||
// hard-cap at 40 (fewest turns evicted first). Mutates in place.
|
||||
func PruneSessions(sessions map[string]*SessionState, now time.Time) {
|
||||
var stale []string
|
||||
for s, v := range sessions {
|
||||
if len(v.Primed) == 0 {
|
||||
stale = append(stale, s)
|
||||
continue
|
||||
}
|
||||
if last, err := time.Parse("2006-01-02T15:04:05", v.LastActive); err == nil {
|
||||
if now.Sub(last).Hours() > 24 {
|
||||
stale = append(stale, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range stale {
|
||||
delete(sessions, s)
|
||||
}
|
||||
if len(sessions) > MaxSessions {
|
||||
type kv struct {
|
||||
key string
|
||||
turns int
|
||||
}
|
||||
var all []kv
|
||||
for s, v := range sessions {
|
||||
all = append(all, kv{s, v.Turns})
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].turns < all[j].turns })
|
||||
for _, kv := range all[:len(sessions)-MaxSessions] {
|
||||
delete(sessions, kv.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
// route_alloc.go — the pure budget-allocation slice of route(). PARITY with
|
||||
// warm_router.py's fired-loop decision: the top-scoring relevant neuron
|
||||
// fires its FULL body (relevance outranks budget — what the user asked
|
||||
// about is never compressed); everything after fires its AAAK summary when
|
||||
// available; token cost = max(1, len(body)/4); over-budget neurons are
|
||||
// SKIPPED (not truncated — the next candidate gets the chance).
|
||||
//
|
||||
// The I/O parts of route() (embedding gather, session state, priming,
|
||||
// association walks, use_history writes) stay on the caller side — the
|
||||
// dichotomy: Go owns the reactive loop, the pure decision is here.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// RouteCandidate — one scored neuron entering the allocation.
|
||||
type RouteCandidate struct {
|
||||
Slug string
|
||||
Body string
|
||||
Summary string // "" = no summary (full body always)
|
||||
Score float64
|
||||
}
|
||||
|
||||
// FiredNeuron — the allocation verdict for one neuron.
|
||||
type FiredNeuron struct {
|
||||
Slug string
|
||||
Body string
|
||||
Score float64
|
||||
Tokens int
|
||||
Compressed bool
|
||||
Primed bool
|
||||
}
|
||||
|
||||
// RouteAllocate — the budget-constrained firing decision.
|
||||
func RouteAllocate(cands []RouteCandidate, maxTokens int) ([]FiredNeuron, int) {
|
||||
sorted := make([]RouteCandidate, len(cands))
|
||||
copy(sorted, cands)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Score > sorted[j].Score })
|
||||
|
||||
var fired []FiredNeuron
|
||||
used := 0
|
||||
topFired := false
|
||||
for _, n := range sorted {
|
||||
if n.Score <= 0 {
|
||||
continue
|
||||
}
|
||||
body := n.Body
|
||||
if topFired && n.Summary != "" {
|
||||
body = n.Summary
|
||||
}
|
||||
tokens := len(body) / 4
|
||||
if tokens < 1 {
|
||||
tokens = 1
|
||||
}
|
||||
if len(fired) > 0 && used+tokens > maxTokens {
|
||||
continue
|
||||
}
|
||||
fired = append(fired, FiredNeuron{
|
||||
Slug: n.Slug, Body: body, Score: n.Score, Tokens: tokens,
|
||||
Compressed: body != n.Body,
|
||||
})
|
||||
used += tokens
|
||||
topFired = true
|
||||
}
|
||||
return fired, used
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
// scoring.go — the warm-router's deterministic scoring core. PARITY ports
|
||||
// of warm_router.py's _py_score_gate and _py_score_finish (the H3 slice-3
|
||||
// pure oracles). I/O signals (embeddings, SQLite base-level, cache file)
|
||||
// arrive as arguments — the caller gathers them; this code is pure.
|
||||
//
|
||||
// Truth battery: truth_gate.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScoreGate — pure gate: (fires, total, keywordHits).
|
||||
// Signals 1 (keyword) and 3 (context) are pure; signal 2 (semantic) arrives
|
||||
// as an argument because embeddings are I/O.
|
||||
func ScoreGate(neuron WarmNeuron, lowText string, sessionContext string, semanticScore float64) (bool, float64, int) {
|
||||
keywordHits := 0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && strings.Contains(lowText, t) {
|
||||
keywordHits++
|
||||
}
|
||||
}
|
||||
|
||||
contextScore := 0.0
|
||||
if sessionContext != "" {
|
||||
contextLow := strings.ToLower(sessionContext)
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && strings.Contains(contextLow, t) {
|
||||
contextScore += 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GATED COMBINATION: keywords are the primary gate; semantic is a
|
||||
// secondary boost (>0.3); context provides turn continuity.
|
||||
if keywordHits > 0 {
|
||||
keywordComponent := float64(keywordHits) * 1.0
|
||||
semanticBoost := 0.0
|
||||
if semanticScore > 0.3 {
|
||||
semanticBoost = math.Max(0, semanticScore-0.3) * 0.5
|
||||
}
|
||||
contextComponent := math.Min(contextScore, 1.0)
|
||||
return true, keywordComponent + semanticBoost + contextComponent, keywordHits
|
||||
} else if semanticScore > 0.55 {
|
||||
// No keywords, strong semantic — secondary gate (higher threshold).
|
||||
return true, semanticScore * 0.7, keywordHits
|
||||
}
|
||||
return false, 0.0, keywordHits
|
||||
}
|
||||
|
||||
// ScoreFinish — pure finish: bonuses, penalties, protected boost atop total.
|
||||
// b_i / cachePriority (nil = skip) arrive as arguments: SQLite and the cache
|
||||
// file are I/O — the wrapper only gathers them on the fired path.
|
||||
func ScoreFinish(neuron WarmNeuron, lowText string, total float64, bi *float64, cachePriority *float64, keywordHits int) float64 {
|
||||
score := total + neuron.Importance
|
||||
|
||||
// Signal 4: base-level learning contribution (capped at 0.3).
|
||||
if neuron.ID != 0 && bi != nil {
|
||||
bContribution := math.Max(0, math.Min(*bi*0.1, 0.3))
|
||||
score += bContribution
|
||||
}
|
||||
|
||||
// Signal 5: wash count (drift-with-integrity lifecycle).
|
||||
if neuron.WashCount > 0 {
|
||||
washBoost := math.Min(0.3, math.Log1p(float64(neuron.WashCount))/math.Log1p(20)*0.3)
|
||||
score += washBoost
|
||||
}
|
||||
|
||||
// Signal 6: cache priority.
|
||||
if neuron.ID != 0 && cachePriority != nil {
|
||||
score += math.Min(0.2, *cachePriority*0.1)
|
||||
}
|
||||
|
||||
// Signal 7: body-knowledge relevance.
|
||||
if neuron.BodyKnowledge != "" && lowText != "" {
|
||||
bkLower := strings.ToLower(neuron.BodyKnowledge)
|
||||
qWords := strings.Fields(strings.ToLower(lowText))
|
||||
bkWords := strings.Fields(bkLower)
|
||||
overlap := 0
|
||||
for _, qw := range qWords {
|
||||
for _, bw := range bkWords {
|
||||
if qw == bw {
|
||||
overlap++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if overlap > 0 {
|
||||
score += math.Min(0.15, float64(overlap)*0.03)
|
||||
}
|
||||
}
|
||||
|
||||
// FAN EFFECT PENALTY (ACT-R, Anderson 1974): broad neurons dilute.
|
||||
triggerCount := 0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" {
|
||||
triggerCount++
|
||||
}
|
||||
}
|
||||
if triggerCount > 10 {
|
||||
fanPenalty := math.Max(0.5, 1.0-float64(triggerCount-10)*0.1)
|
||||
score *= fanPenalty
|
||||
}
|
||||
|
||||
// Length bonus for keyword matches.
|
||||
if keywordHits > 0 {
|
||||
lengthBonus := 0.0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && len(t) > 6 && strings.Contains(lowText, t) {
|
||||
lengthBonus += 0.25
|
||||
}
|
||||
}
|
||||
score += lengthBonus
|
||||
}
|
||||
|
||||
// Protected neurons get a boost.
|
||||
if ProtectedNeurons[neuron.Slug] {
|
||||
score += ProtectedBoost
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// The protected set + boost — PARITY with warm_router.py:60-61.
|
||||
var ProtectedNeurons = map[string]bool{"persona-alfred": true, "philosophy-sagan-core": true}
|
||||
|
||||
const ProtectedBoost = 4.0
|
||||
@@ -0,0 +1,99 @@
|
||||
[
|
||||
{
|
||||
"fn": "event_map",
|
||||
"name": "full_map",
|
||||
"expected": {
|
||||
"map": {
|
||||
"task.completed": [
|
||||
"task",
|
||||
"growth",
|
||||
"lessons"
|
||||
],
|
||||
"task.failed": [
|
||||
"task",
|
||||
"debugging",
|
||||
"error"
|
||||
],
|
||||
"capability.failed": [
|
||||
"debugging",
|
||||
"error",
|
||||
"system"
|
||||
],
|
||||
"owner.correction": [
|
||||
"growth",
|
||||
"lessons",
|
||||
"persona"
|
||||
],
|
||||
"memory.created": [
|
||||
"knowledge",
|
||||
"learning"
|
||||
],
|
||||
"memory.conflict": [
|
||||
"contradiction",
|
||||
"knowledge"
|
||||
],
|
||||
"context.pressure": [
|
||||
"context",
|
||||
"management"
|
||||
],
|
||||
"model.unavailable": [
|
||||
"system",
|
||||
"fallback"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "event_query",
|
||||
"name": "known",
|
||||
"input": {
|
||||
"event_type": "task.failed",
|
||||
"event_data": "build 19571"
|
||||
},
|
||||
"expected": {
|
||||
"query": "task debugging error build 19571"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "event_query",
|
||||
"name": "unknown",
|
||||
"input": {
|
||||
"event_type": "not.an.event"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "event_query",
|
||||
"name": "truncated_data",
|
||||
"input": {
|
||||
"event_type": "memory.created",
|
||||
"event_data": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||
},
|
||||
"expected": {
|
||||
"query": "knowledge learning dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "proactive_queries",
|
||||
"name": "full",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"queries": [
|
||||
"TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT",
|
||||
"second task",
|
||||
"error debugging system",
|
||||
"commitment reminder promise"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "proactive_queries",
|
||||
"name": "empty",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"queries": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
// truth_event_test.go — batch-3 parity gate: the event map and proactive
|
||||
// query assembly against truth_event.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEventMapParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_event.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_event.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "full_map":
|
||||
// The map must be byte-identical: same keys, same keyword lists.
|
||||
want := c.Expected["map"].(map[string]interface{})
|
||||
if len(want) != len(EventNeuronMap) {
|
||||
t.Fatalf("event map size: got %d want %d", len(EventNeuronMap), len(want))
|
||||
}
|
||||
for event, kws := range want {
|
||||
got, ok := EventNeuronMap[event]
|
||||
if !ok {
|
||||
t.Errorf("missing event %q", event)
|
||||
continue
|
||||
}
|
||||
wl := kws.([]interface{})
|
||||
if len(got) != len(wl) {
|
||||
t.Errorf("%s: got %v want %v", event, got, wl)
|
||||
continue
|
||||
}
|
||||
for i, kw := range wl {
|
||||
if got[i] != kw.(string) {
|
||||
t.Errorf("%s[%d]: got %q want %q", event, i, got[i], kw.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "known":
|
||||
et, _ := c.Input["event_type"].(string)
|
||||
ed, _ := c.Input["event_data"].(string)
|
||||
q, ok := EventQuery(et, ed)
|
||||
if !ok {
|
||||
t.Errorf("%s: event should be known", c.Name)
|
||||
}
|
||||
if q != c.Expected["query"].(string) {
|
||||
t.Errorf("%s: got %q want %q", c.Name, q, c.Expected["query"])
|
||||
}
|
||||
case "unknown":
|
||||
et, _ := c.Input["event_type"].(string)
|
||||
_, ok := EventQuery(et, "")
|
||||
if ok {
|
||||
t.Errorf("%s: unknown event must not produce a query", c.Name)
|
||||
}
|
||||
case "truncated_data":
|
||||
et, _ := c.Input["event_type"].(string)
|
||||
ed, _ := c.Input["event_data"].(string)
|
||||
q, ok := EventQuery(et, ed)
|
||||
if !ok {
|
||||
t.Errorf("%s: event should be known", c.Name)
|
||||
}
|
||||
if q != c.Expected["query"].(string) {
|
||||
t.Errorf("%s: got len %d want len %d", c.Name, len(q), len(c.Expected["query"].(string)))
|
||||
}
|
||||
case "full":
|
||||
// ProactiveQueries with tasks + errors + commitments.
|
||||
tasks := []map[string]interface{}{
|
||||
{"summary": strings.Repeat("T", 150)},
|
||||
{"summary": "second task"},
|
||||
}
|
||||
q := ProactiveQueries(tasks, true, true)
|
||||
want := c.Expected["queries"].([]interface{})
|
||||
if len(q) != len(want) {
|
||||
t.Errorf("%s: got %d queries want %d", c.Name, len(q), len(want))
|
||||
continue
|
||||
}
|
||||
for i := range want {
|
||||
if q[i] != want[i].(string) {
|
||||
t.Errorf("%s[%d]: got %q want %q", c.Name, i, q[i], want[i].(string))
|
||||
}
|
||||
}
|
||||
case "empty":
|
||||
q := ProactiveQueries(nil, false, false)
|
||||
if len(q) != 0 {
|
||||
t.Errorf("%s: got %v want empty", c.Name, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "kw_hit",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 2.0,
|
||||
"kw": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "no_fire",
|
||||
"expected": {
|
||||
"fires": false,
|
||||
"total": 0.0,
|
||||
"kw": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "sem_only",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 1.25,
|
||||
"kw": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "ctx",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 1.3,
|
||||
"kw": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fin_full",
|
||||
"expected": {
|
||||
"score": 3.14
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fin_min",
|
||||
"expected": {
|
||||
"score": 2.55
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fan_penalty",
|
||||
"expected": {
|
||||
"score": 1.26
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
// truth_gate_test.go — parity gate for the scoring core. Replays
|
||||
// truth_gate.json (live Python oracle, 2026-10-07) through ScoreGate and
|
||||
// ScoreFinish.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScoreParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_gate.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_gate.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
neuron := WarmNeuron{
|
||||
Slug: "test-neuron", Triggers: []string{"spine", "rebuild"},
|
||||
Importance: 0.8, ID: 42,
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "kw_hit":
|
||||
fires, total, kw := ScoreGate(neuron, "the spine rebuild verified parity", "", 0.0)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("kw_hit: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "no_fire":
|
||||
fires, total, kw := ScoreGate(neuron, "something unrelated", "", 0.2)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("no_fire: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "sem_only":
|
||||
fires, total, kw := ScoreGate(neuron, "the spinal column rebuild", "", 0.8)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("sem_only: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "ctx":
|
||||
fires, total, kw := ScoreGate(neuron, "spine", "earlier we discussed the spine in detail", 0.0)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("ctx: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "fin_full":
|
||||
bi := 0.5
|
||||
cp := 0.4
|
||||
got := ScoreFinish(neuron, "the spine rebuild verified parity", 2.0, &bi, &cp, 2)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fin_full: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
case "fin_min":
|
||||
got := ScoreFinish(neuron, "the spine rebuild verified parity", 1.5, nil, nil, 1)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fin_min: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
case "fan_penalty":
|
||||
n2 := neuron
|
||||
triggers := []string{}
|
||||
for i := 0; i < 12; i++ {
|
||||
triggers = append(triggers, "t"+string(rune('0'+i%10)))
|
||||
}
|
||||
triggers = append(triggers, "spine")
|
||||
n2.Triggers = triggers
|
||||
got := ScoreFinish(n2, "spine appears", 1.0, nil, nil, 1)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fan_penalty: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown truth case %q", c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProtectedBoost — the persona anchors must outscore everything on
|
||||
// their topic (that is the entire point of the boost).
|
||||
func TestProtectedBoost(t *testing.T) {
|
||||
protected := WarmNeuron{Slug: "persona-alfred", Triggers: []string{"composure"}, Importance: 0.8}
|
||||
ordinary := WarmNeuron{Slug: "ordinary", Triggers: []string{"composure"}, Importance: 0.8}
|
||||
p := ScoreFinish(protected, "composure under pressure", 1.0, nil, nil, 1)
|
||||
o := ScoreFinish(ordinary, "composure under pressure", 1.0, nil, nil, 1)
|
||||
if p-o != ProtectedBoost {
|
||||
t.Errorf("protected delta = %v, want %v", p-o, ProtectedBoost)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
[
|
||||
{
|
||||
"fn": "parse_trigger",
|
||||
"name": "comma",
|
||||
"input": {
|
||||
"raw": "spine, rebuild, parity"
|
||||
},
|
||||
"expected": {
|
||||
"triggers": [
|
||||
"spine",
|
||||
"rebuild",
|
||||
"parity"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse_trigger",
|
||||
"name": "json",
|
||||
"input": {
|
||||
"raw": "[\"a\",\"b\"]"
|
||||
},
|
||||
"expected": {
|
||||
"triggers": [
|
||||
"[\"a\"",
|
||||
"\"b\"]"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse_trigger",
|
||||
"name": "empty",
|
||||
"input": {
|
||||
"raw": ""
|
||||
},
|
||||
"expected": {
|
||||
"triggers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "body_knowledge",
|
||||
"name": "proven",
|
||||
"input": {
|
||||
"triggers": [
|
||||
"spine",
|
||||
"rebuild"
|
||||
],
|
||||
"summary": "the spine mechanics",
|
||||
"wc": 20
|
||||
},
|
||||
"expected": {
|
||||
"bk": "Fires on: spine, rebuild. Knows: the spine mechanics. Proven \u2014 has survived many washes."
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "body_knowledge",
|
||||
"name": "untested",
|
||||
"input": {
|
||||
"triggers": [],
|
||||
"summary": "",
|
||||
"wc": 0
|
||||
},
|
||||
"expected": {
|
||||
"bk": "Untested \u2014 new or rarely accessed."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
// truth_list_test.go — batch-5 parity gate: trigger parsing and the
|
||||
// body-knowledge construction against truth_list.json (live oracle).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch5ListParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_list.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_list.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "comma":
|
||||
raw, _ := c.Input["raw"].(string)
|
||||
got := ParseTrigger(raw)
|
||||
want := ifaceStrings(c.Expected["triggers"].([]interface{}))
|
||||
eqStrs(t, c.Name, got, want)
|
||||
case "json":
|
||||
// The oracle does NOT json-decode — reproduce the literal split.
|
||||
raw, _ := c.Input["raw"].(string)
|
||||
got := ParseTrigger(raw)
|
||||
want := ifaceStrings(c.Expected["triggers"].([]interface{}))
|
||||
eqStrs(t, c.Name, got, want)
|
||||
case "empty":
|
||||
got := ParseTrigger("")
|
||||
if len(got) != 0 {
|
||||
t.Errorf("empty: got %v want []", got)
|
||||
}
|
||||
case "proven":
|
||||
got := BodyKnowledge([]string{"spine", "rebuild"}, "the spine mechanics", 20)
|
||||
if got != c.Expected["bk"].(string) {
|
||||
t.Errorf("proven:\n got %q\nwant %q", got, c.Expected["bk"].(string))
|
||||
}
|
||||
case "untested":
|
||||
got := BodyKnowledge(nil, "", 0)
|
||||
if got != c.Expected["bk"].(string) {
|
||||
t.Errorf("untested: got %q want %q", got, c.Expected["bk"].(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ifaceStrings(list []interface{}) []string {
|
||||
out := make([]string, len(list))
|
||||
for i, v := range list {
|
||||
out[i] = v.(string)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func eqStrs(t *testing.T, name string, got, want []string) {
|
||||
t.Helper()
|
||||
if len(got) != len(want) {
|
||||
t.Errorf("%s: got %v want %v", name, got, want)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("%s[%d]: got %q want %q", name, i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNeuronTokens — min 1, len/4 otherwise.
|
||||
func TestNeuronTokens(t *testing.T) {
|
||||
if NeuronTokens("") != 1 {
|
||||
t.Error("empty body must cost 1 token")
|
||||
}
|
||||
if NeuronTokens("x") != 1 {
|
||||
t.Error("1-char body must cost 1 token")
|
||||
}
|
||||
if NeuronTokens(string(make([]byte, 400))) != 100 {
|
||||
t.Error("400-char body must cost 100 tokens")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"fn": "decay_primes",
|
||||
"name": "mix",
|
||||
"expected": {
|
||||
"primed": {
|
||||
"a": 0.3,
|
||||
"b": 0.18
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "prune_sessions",
|
||||
"name": "stale",
|
||||
"expected": {
|
||||
"kept": [
|
||||
"active"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "prune_sessions",
|
||||
"name": "cap40",
|
||||
"expected": {
|
||||
"count": 40,
|
||||
"kept_turns_min": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
// truth_prime_test.go — batch-4 parity gate: priming decay + session prune
|
||||
// against truth_prime.json (live Python walk, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBatch4PrimeParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_prime.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_prime.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "mix":
|
||||
primed := map[string]float64{"a": 0.5, "b": 0.3, "c": 0.24, "d": 0.2}
|
||||
DecayPrimes(primed)
|
||||
want := c.Expected["primed"].(map[string]interface{})
|
||||
if len(primed) != len(want) {
|
||||
t.Fatalf("mix: kept %d, want %d (%v)", len(primed), len(want), primed)
|
||||
}
|
||||
for slug, w := range want {
|
||||
got, ok := primed[slug]
|
||||
if !ok || math.Abs(got-w.(float64)) > 1e-9 {
|
||||
t.Errorf("mix[%s]: got %v want %v", slug, got, w)
|
||||
}
|
||||
}
|
||||
case "stale":
|
||||
now := time.Now().UTC()
|
||||
sessions := map[string]*SessionState{
|
||||
"active": {Primed: map[string]float64{"x": 0.5}, LastActive: now.Format("2006-01-02T15:04:05"), Turns: 3},
|
||||
"empty": {Primed: map[string]float64{}, LastActive: now.Format("2006-01-02T15:04:05"), Turns: 1},
|
||||
"idle": {Primed: map[string]float64{"y": 0.4}, LastActive: now.Add(-25 * time.Hour).Format("2006-01-02T15:04:05"), Turns: 2},
|
||||
}
|
||||
PruneSessions(sessions, now)
|
||||
want := c.Expected["kept"].([]interface{})
|
||||
if len(sessions) != len(want) {
|
||||
t.Fatalf("stale: kept %d, want %d", len(sessions), len(want))
|
||||
}
|
||||
for _, k := range want {
|
||||
if _, ok := sessions[k.(string)]; !ok {
|
||||
t.Errorf("stale: %q should have been kept", k.(string))
|
||||
}
|
||||
}
|
||||
case "cap40":
|
||||
now := time.Now().UTC()
|
||||
sessions := map[string]*SessionState{}
|
||||
for i := 0; i < 42; i++ {
|
||||
key := "s" + string(rune('0'+i/10)) + string(rune('0'+i%10))
|
||||
sessions[key] = &SessionState{
|
||||
Primed: map[string]float64{"x": 0.5},
|
||||
LastActive: now.Format("2006-01-02T15:04:05"),
|
||||
Turns: i,
|
||||
}
|
||||
}
|
||||
PruneSessions(sessions, now)
|
||||
if len(sessions) != 40 {
|
||||
t.Errorf("cap40: kept %d, want 40", len(sessions))
|
||||
}
|
||||
minTurns := 1 << 30
|
||||
for _, v := range sessions {
|
||||
if v.Turns < minTurns {
|
||||
minTurns = v.Turns
|
||||
}
|
||||
}
|
||||
if minTurns != int(c.Expected["kept_turns_min"].(float64)) {
|
||||
t.Errorf("cap40: min turns kept = %d, want %v", minTurns, c.Expected["kept_turns_min"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrimeNeuronsMaxSemantics — re-firing never LOWERS a priming score
|
||||
// (Python: max(score, existing)).
|
||||
func TestPrimeNeuronsMaxSemantics(t *testing.T) {
|
||||
sess := &SessionState{Primed: map[string]float64{"a": 0.8}}
|
||||
now := time.Now().UTC()
|
||||
PrimeNeurons(sess, []string{"a", "b"}, 0.4, now)
|
||||
if sess.Primed["a"] != 0.8 {
|
||||
t.Errorf("re-fire lowered a: %v, want 0.8", sess.Primed["a"])
|
||||
}
|
||||
if sess.Primed["b"] != 0.4 {
|
||||
t.Errorf("new fire b: %v, want 0.4", sess.Primed["b"])
|
||||
}
|
||||
if sess.Turns != 1 {
|
||||
t.Errorf("turns: %d, want 1", sess.Turns)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"fn": "route_alloc",
|
||||
"name": "budget",
|
||||
"max_tokens": 300,
|
||||
"expected": {
|
||||
"fired": [
|
||||
{
|
||||
"slug": "big-primary",
|
||||
"tokens": 100,
|
||||
"compressed": false,
|
||||
"body_len": 400
|
||||
},
|
||||
{
|
||||
"slug": "medium",
|
||||
"tokens": 3,
|
||||
"compressed": true,
|
||||
"body_len": 14
|
||||
},
|
||||
{
|
||||
"slug": "small",
|
||||
"tokens": 12,
|
||||
"compressed": false,
|
||||
"body_len": 50
|
||||
}
|
||||
],
|
||||
"used": 115
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "route_alloc",
|
||||
"name": "generous",
|
||||
"max_tokens": 10000,
|
||||
"expected": {
|
||||
"fired": [
|
||||
{
|
||||
"slug": "big-primary",
|
||||
"tokens": 100,
|
||||
"compressed": false,
|
||||
"body_len": 400
|
||||
},
|
||||
{
|
||||
"slug": "medium",
|
||||
"tokens": 3,
|
||||
"compressed": true,
|
||||
"body_len": 14
|
||||
},
|
||||
{
|
||||
"slug": "small",
|
||||
"tokens": 12,
|
||||
"compressed": false,
|
||||
"body_len": 50
|
||||
}
|
||||
],
|
||||
"used": 115
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
// truth_route_test.go — batch-2 parity gate: the budget-allocation decision
|
||||
// against truth_route.json (live Python walk, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRouteAllocParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_route.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_route.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Name string `json:"name"`
|
||||
Expected struct {
|
||||
Fired []struct {
|
||||
Slug string `json:"slug"`
|
||||
Tokens int `json:"tokens"`
|
||||
Compressed bool `json:"compressed"`
|
||||
BodyLen int `json:"body_len"`
|
||||
} `json:"fired"`
|
||||
Used int `json:"used"`
|
||||
} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
cands := []RouteCandidate{
|
||||
{Slug: "big-primary", Body: string(make([]byte, 400)), Summary: "primary summary", Score: 2.5},
|
||||
{Slug: "medium", Body: string(make([]byte, 200)), Summary: "medium summary", Score: 1.2},
|
||||
{Slug: "small", Body: string(make([]byte, 50)), Summary: "", Score: 0.8},
|
||||
}
|
||||
for _, c := range cases {
|
||||
fired, _ := RouteAllocate(cands, c.Expected.Used+1000) // generous budget
|
||||
if len(fired) != len(c.Expected.Fired) {
|
||||
t.Fatalf("%s: fired %d, want %d", c.Name, len(fired), len(c.Expected.Fired))
|
||||
}
|
||||
for i, f := range fired {
|
||||
w := c.Expected.Fired[i]
|
||||
if f.Slug != w.Slug || f.Tokens != w.Tokens || f.Compressed != w.Compressed || len(f.Body) != w.BodyLen {
|
||||
t.Errorf("%s[%d]: got {slug=%s tokens=%d compressed=%v bodyLen=%d} want {slug=%s tokens=%d compressed=%v bodyLen=%d}",
|
||||
c.Name, i, f.Slug, f.Tokens, f.Compressed, len(f.Body), w.Slug, w.Tokens, w.Compressed, w.BodyLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteBudgetSkip — the over-budget behavior: skipped, not truncated,
|
||||
// and the next candidate gets the chance.
|
||||
func TestRouteBudgetSkip(t *testing.T) {
|
||||
cands := []RouteCandidate{
|
||||
{Slug: "primary", Body: string(make([]byte, 400)), Score: 2.5},
|
||||
{Slug: "huge", Body: string(make([]byte, 2000)), Score: 1.5},
|
||||
{Slug: "fits", Body: string(make([]byte, 80)), Score: 1.0},
|
||||
}
|
||||
fired, used := RouteAllocate(cands, 200)
|
||||
slugs := []string{}
|
||||
for _, f := range fired {
|
||||
slugs = append(slugs, f.Slug)
|
||||
}
|
||||
if len(slugs) != 2 || slugs[0] != "primary" || slugs[1] != "fits" {
|
||||
t.Errorf("budget skip: got %v (used %d), want [primary fits]", slugs, used)
|
||||
}
|
||||
if used > 200 {
|
||||
t.Errorf("budget exceeded: used %d > 200", used)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTopNeverCompressed — parity law: the top hit fires its full body even
|
||||
// when a summary exists.
|
||||
func TestTopNeverCompressed(t *testing.T) {
|
||||
cands := []RouteCandidate{
|
||||
{Slug: "top", Body: string(make([]byte, 400)), Summary: "a summary", Score: 2.0},
|
||||
}
|
||||
fired, _ := RouteAllocate(cands, 50) // tiny budget — top still full
|
||||
if len(fired) != 1 || fired[0].Compressed || fired[0].Tokens != 100 {
|
||||
t.Errorf("top compressed under tiny budget: %+v", fired[0])
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.108.2"
|
||||
__version__ = "0.123.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.108.2"' in rune_version,
|
||||
'__version__ = "0.123.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drift ledger — drift protection for long projects.
|
||||
|
||||
A persistent checksum ledger of the five core blocks. Each sleep-time / compaction
|
||||
run records the sha256 and byte length of every block. Two signals are tracked:
|
||||
|
||||
- CUMULATIVE VOLUME: how far each block has moved from its *anchor* (a ledger
|
||||
record labelled with a source directive or an explicit user confirmation).
|
||||
Blocks near/over the threshold without an anchor source are flagged.
|
||||
- SOURCE TYPE: how each byte of change is justified. Legitimate growth carries
|
||||
a journaled operation (a user directive, a 3-sighting ledger promotion,
|
||||
an ADD/UPDATE/DELETE applied by the curator). Unexplained bulk is drift.
|
||||
|
||||
The ANGLE of this file: growth belongs to the *harness* (blocks, ledger, skills,
|
||||
rules), NOT the engine (the swappable LLM). When the model swaps, the harness and
|
||||
its drift bounds must persist untouched. This ledger is part of the harness.
|
||||
|
||||
Usage:
|
||||
drift-ledger.py snapshot # record block states, return JSON
|
||||
drift-ledger.py check [--strict] # compare to last snapshot; emit DRIFT-ALERT
|
||||
drift-ledger.py anchor <label> <reason> # re-anchor a block after a
|
||||
# confirmed, authorised change
|
||||
Exit 0 = healthy, 1 = drift flagged.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import sys, os
|
||||
_SD = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SD not in sys.path: sys.path.insert(0, _SD)
|
||||
try:
|
||||
from . import memory_env
|
||||
except ImportError:
|
||||
import memory_env
|
||||
|
||||
INDEX_FILE = os.path.join(memory_env.memory_dir(), "index", "blocks_meta.json")
|
||||
DB = memory_env.store_db()
|
||||
|
||||
|
||||
def ledger_path():
|
||||
# The ledger lives beside the index, so overriding INDEX_FILE (as the
|
||||
# curator gate and the canary do for isolation) moves the ledger too.
|
||||
return os.path.join(os.path.dirname(INDEX_FILE), "drift_ledger.json")
|
||||
JOURNAL_DIR = os.path.join(memory_env.memory_dir(), "journal")
|
||||
|
||||
# Max cumulative byte drift from an anchored/authorised state before we refuse.
|
||||
DRIFT_LIMIT = 0.30 # 30% of block size
|
||||
# Blocks whose changes are always allowed to grow fast (their job is state).
|
||||
FAST_BLOCKS = ("project", "operating")
|
||||
# The constitution is even more constrained — it changes ONLY by directive.
|
||||
CONSTITUTION_LIMIT = 0.05 # 5% — anything more than a directive entry is drift
|
||||
|
||||
BLOCKS = ("constitution", "persona", "human", "operating", "project")
|
||||
|
||||
|
||||
def _sha(text):
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def read_block(label):
|
||||
"""Read a core block from the STORE (new schema) — the always-on entries
|
||||
for that topic, serialized as text. The old markdown files are retired;
|
||||
the store is the source of truth, so drift protection guards store content.
|
||||
"""
|
||||
try:
|
||||
import sqlite3
|
||||
db = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
|
||||
rows = db.execute(
|
||||
"SELECT text FROM entries WHERE topic=? AND always_on=1 "
|
||||
"AND status='active' ORDER BY priority ASC, id ASC", (label,)
|
||||
).fetchall()
|
||||
db.close()
|
||||
return "\n".join(r[0] for r in rows)
|
||||
except Exception: # noqa: BLE001 — intentional: drift check degrades silently when a block is unreadable; parking is never wrong
|
||||
return ""
|
||||
|
||||
|
||||
def load_ledger():
|
||||
if os.path.exists(ledger_path()):
|
||||
with open(ledger_path()) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def save_ledger(ledger):
|
||||
path = ledger_path()
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(ledger, f, indent=2)
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def journal(msg):
|
||||
os.makedirs(JOURNAL_DIR, exist_ok=True)
|
||||
month = datetime.now(timezone.utc).strftime("%Y-%m")
|
||||
ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
with open(os.path.join(JOURNAL_DIR, f"{month}.md"), "a") as f:
|
||||
f.write(f"`{ts}` **DRIFT-ALERT** → harness\n - {msg}\n\n")
|
||||
|
||||
|
||||
def snapshot():
|
||||
ledger = load_ledger()
|
||||
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
entry = {"when": now, "blocks": {}}
|
||||
for label in BLOCKS:
|
||||
raw = read_block(label)
|
||||
entry["blocks"][label] = {
|
||||
"sha": _sha(raw),
|
||||
"size": len(raw),
|
||||
"content": raw, # last-good content backup for diffing + retro-fix
|
||||
}
|
||||
ledger["last_snapshot"] = entry
|
||||
save_ledger(ledger)
|
||||
return entry
|
||||
|
||||
|
||||
def anchor(label, reason):
|
||||
"""Record that the current state of `label` is authorised (a user directive,
|
||||
a confirmed review, a 3-sighting promotion). Subsequent drift is measured
|
||||
from here — this is the re-anchor after legitimate change."""
|
||||
ledger = load_ledger()
|
||||
raw = read_block(label)
|
||||
ledger.setdefault("anchors", {})[label] = {
|
||||
"sha": _sha(raw),
|
||||
"size": len(raw),
|
||||
"content": raw, # authorised content backup for root-cause diffing
|
||||
"reason": reason,
|
||||
"when": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
}
|
||||
save_ledger(ledger)
|
||||
return f"anchored {label}: {reason}"
|
||||
|
||||
|
||||
def check(strict=False, autofix=True):
|
||||
ledger = load_ledger()
|
||||
snap = ledger.get("last_snapshot")
|
||||
if not snap:
|
||||
# No baseline yet — record one and treat as healthy (first contact).
|
||||
snapshot()
|
||||
print("no prior snapshot; baseline recorded")
|
||||
return 0
|
||||
|
||||
alerts = []
|
||||
now_blocks = {label: read_block(label) for label in BLOCKS}
|
||||
prev = snap.get("blocks", {})
|
||||
for label in BLOCKS:
|
||||
cur = now_blocks[label]
|
||||
cur_size = len(cur)
|
||||
anchor_meta = ledger.get("anchors", {}).get(label)
|
||||
if anchor_meta:
|
||||
# An anchored block is measured against its authorised state — the
|
||||
# re-anchor after legitimate change becomes the new reference.
|
||||
base = anchor_meta.get("size", cur_size)
|
||||
base_sha = anchor_meta.get("sha")
|
||||
delta = cur_size - base
|
||||
# Root-cause: diff current content against the anchored content.
|
||||
ref_content = anchor_meta.get("content")
|
||||
else:
|
||||
prev_meta = prev.get(label, {})
|
||||
prev_size = prev_meta.get("size", cur_size)
|
||||
base = max(prev_size, 1)
|
||||
base_sha = None
|
||||
delta = cur_size - prev_size
|
||||
ref_content = prev_meta.get("content")
|
||||
pct = abs(delta) / max(base, 1)
|
||||
limit = CONSTITUTION_LIMIT if label == "constitution" else DRIFT_LIMIT
|
||||
if pct > limit:
|
||||
alerts.append({
|
||||
"label": label,
|
||||
"delta": delta,
|
||||
"pct": round(pct, 3),
|
||||
"limit": round(limit, 3),
|
||||
"base": base,
|
||||
"changed": diff_blocks(ref_content, cur),
|
||||
})
|
||||
if alerts:
|
||||
for a in alerts:
|
||||
msg = (f"{a['label']}: {a['delta']:+d} bytes "
|
||||
f"({a['pct']*100:.1f}% vs limit {a['limit']*100:.1f}%) "
|
||||
f"[{a['changed']}]")
|
||||
print(f"DRIFT-ALERT {msg}")
|
||||
# AUTOMATIC FIX on detection (user directive): the system fixes
|
||||
# drift itself, not just flags it.
|
||||
if autofix:
|
||||
auto_result = auto_fix(a["label"], ledger, strict)
|
||||
print(f" auto-fix: {auto_result['action']} — {auto_result['reason']}")
|
||||
if strict:
|
||||
journal(f"{msg}; auto-fix: {auto_result['action']} — "
|
||||
f"{auto_result['reason']}")
|
||||
else:
|
||||
print(f" (autofix disabled — observe only)")
|
||||
return 1
|
||||
print("no drift flagged (within limits)")
|
||||
return 0
|
||||
|
||||
|
||||
def auto_fix(label, ledger, strict):
|
||||
"""AUTOMATIC retroactive fix upon drift detection.
|
||||
|
||||
Policy:
|
||||
- constitution: read-only, changes ONLY by explicit directive -> never
|
||||
auto-fix, alert + journal for human review.
|
||||
- anchored block (authorised): the growth is legitimate -> auto re-anchor
|
||||
to the new state (the fix is approving it), so future drift is measured
|
||||
from here. This prevents the SAME change being flagged repeatedly.
|
||||
- unanchored block: drift is unauthorised -> restore to last-good content
|
||||
(the retroactive fix), removing whatever caused it.
|
||||
|
||||
Returns {action, reason}.
|
||||
"""
|
||||
if label == "constitution":
|
||||
return {"action": "alert-only",
|
||||
"reason": "constitution changes only by explicit directive; "
|
||||
"needs human review"}
|
||||
if label in ledger.get("anchors", {}):
|
||||
# Authorised growth: re-anchor to the current (approved) state.
|
||||
raw = read_block(label)
|
||||
ledger.setdefault("anchors", {})[label] = {
|
||||
"sha": _sha(raw), "size": len(raw), "content": raw,
|
||||
"reason": "auto re-anchor after authorised growth",
|
||||
"when": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
}
|
||||
save_ledger(ledger)
|
||||
return {"action": "re-anchored",
|
||||
"reason": "anchored block grew (authorised) — re-baselined"}
|
||||
# Unauthorised drift: restore to last-good content (new schema — restore
|
||||
# the store's always-on core entries for this topic).
|
||||
block_meta = ledger.get("last_snapshot", {}).get("blocks", {}).get(label)
|
||||
if block_meta and "content" in block_meta:
|
||||
restore_topic(label, block_meta["content"])
|
||||
return {"action": "restored",
|
||||
"reason": "unauthorised drift — restored to last-good content"}
|
||||
return {"action": "alert-only",
|
||||
"reason": "no last-good content available to restore"}
|
||||
|
||||
|
||||
def diff_blocks(ref, cur):
|
||||
"""Root-cause: what changed between the last-good content and now.
|
||||
Returns a short list of added/removed lines (the drift's cause)."""
|
||||
if not ref:
|
||||
return "no prior content (first drift from empty baseline)"
|
||||
try:
|
||||
ref_lines = ref.splitlines()
|
||||
cur_lines = cur.splitlines()
|
||||
ref_set = {l.strip() for l in ref_lines if l.strip()}
|
||||
cur_set = {l.strip() for l in cur_lines if l.strip()}
|
||||
added = [l for l in cur_lines if l.strip() and l.strip() not in ref_set][:6]
|
||||
removed = [l for l in ref_lines if l.strip() and l.strip() not in cur_set][:3]
|
||||
parts = []
|
||||
if added:
|
||||
parts.append("ADDED: " + " | ".join(l.strip()[:60] for l in added))
|
||||
if removed:
|
||||
parts.append("REMOVED: " + " | ".join(l.strip()[:60] for l in removed))
|
||||
return "; ".join(parts) if parts else "content changed (unclear from diff)"
|
||||
except Exception: # noqa: BLE001 — intentional: diff summarizing is best-effort evidence only; the verdict never depends on it
|
||||
return "diff unavailable"
|
||||
|
||||
|
||||
def restore_topic(label, content):
|
||||
"""Restore a topic's store entries from serialized content (new schema).
|
||||
Replaces the always-on entries for `label` with those parsed from
|
||||
`content` (the body format `read_block` produces: `- text` lines)."""
|
||||
try:
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
db = sqlite3.connect(DB)
|
||||
db.execute("DELETE FROM entries WHERE topic=? AND always_on=1 "
|
||||
"AND COALESCE(method, '') != 'directive'", (label,))
|
||||
ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
priority = {"constitution": 0, "safety": 0, "identity": 1,
|
||||
"operating": 2, "human": 3, "project": 3}.get(label, 5)
|
||||
for line in content.splitlines():
|
||||
text = line.strip().lstrip("-* ").strip()
|
||||
if not text or len(text) < 5:
|
||||
continue
|
||||
db.execute(
|
||||
"INSERT INTO entries (text, importance, created_at, "
|
||||
"last_accessed, topic, source, method, status, valid_from, "
|
||||
"confidence, temperature, always_on, priority) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(text, 0.9, ts, ts, label, "drift-restore", "curator",
|
||||
"active", ts, 0.9, 1.0, 1, priority))
|
||||
db.commit()
|
||||
db.close()
|
||||
except Exception: # noqa: BLE001, S110 — intentional: best-effort drift-restore write; a failed restore must not mask the original gate verdict
|
||||
pass
|
||||
|
||||
|
||||
def fix(label):
|
||||
"""RETROACTIVE FIX (#B): restore a drifted block to its last-good content.
|
||||
|
||||
Only restores when the drift exceeds the limit AND the last-good content is
|
||||
available. If the drift was legitimate (authorised growth), `anchor` instead
|
||||
— fix() refuses to silently revert an anchored block.
|
||||
Returns {fixed: bool, reason}.
|
||||
"""
|
||||
ledger = load_ledger()
|
||||
snap = ledger.get("last_snapshot", {})
|
||||
block_meta = snap.get("blocks", {}).get(label)
|
||||
if not block_meta or "content" not in block_meta:
|
||||
return {"fixed": False, "reason": f"no last-good content for {label}"}
|
||||
# Never silently revert an anchored block — it was authorised.
|
||||
if label in ledger.get("anchors", {}):
|
||||
return {"fixed": False,
|
||||
"reason": f"{label} is anchored (authorised); use anchor to "
|
||||
f"re-baseline instead of fix"}
|
||||
good = block_meta["content"]
|
||||
cur = read_block(label)
|
||||
if _sha(cur) == block_meta["sha"]:
|
||||
return {"fixed": False, "reason": f"{label} already matches last-good"}
|
||||
# New schema: restore the store's always-on entries for the topic (the old
|
||||
# markdown block files are retired).
|
||||
restore_topic(label, good)
|
||||
journal(f"{label} restored to last-good content (retroactive drift fix)")
|
||||
return {"fixed": True, "reason": f"{label} restored to last-good state"}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("cmd", choices=["snapshot", "check", "anchor", "fix"])
|
||||
ap.add_argument("label", nargs="?", default=None,
|
||||
help="block label for anchor")
|
||||
ap.add_argument("reason", nargs="?", default="", help="anchor reason")
|
||||
ap.add_argument("--strict", action="store_true",
|
||||
help="journal DRIFT-ALERT on violation")
|
||||
ap.add_argument("--no-autofix", action="store_true",
|
||||
help="check only — don't apply automatic fix (observe only)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.cmd == "snapshot":
|
||||
print(json.dumps(snapshot()))
|
||||
sys.exit(0)
|
||||
if args.cmd == "check":
|
||||
sys.exit(check(strict=args.strict, autofix=not args.no_autofix))
|
||||
if args.cmd == "fix":
|
||||
if not args.label:
|
||||
print("fix needs <label>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(json.dumps(fix(args.label)))
|
||||
sys.exit(0)
|
||||
if args.cmd == "anchor":
|
||||
if not args.label:
|
||||
print("anchor needs <label>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(anchor(args.label, args.reason or "authorised by the user"))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""lexicon_evolution.py — the lexicon meta-judgement applied to EVOLUTION.
|
||||
|
||||
Retrieval already applies the meta-judgement ("a shared word is not a
|
||||
relation; a shared DISTINCTIVE word is a signal"): conjunctive matching, rare-
|
||||
word gating, calibrated floors. Evolution decisions (persona coherence, identity
|
||||
grounding, constitution promotion) must use the SAME lexicon discipline —
|
||||
otherwise a personality can "evolve" on a coincidental shared word exactly the
|
||||
way recall used to misfire on "black coffee" vs "black holes".
|
||||
|
||||
This module provides the shared lexical signal for the curator's three growth
|
||||
paths, using identical definitions to the recall gate (same stopword set, same
|
||||
distinctiveness threshold), so growth and retrieval speak one lexicon.
|
||||
|
||||
Decisions:
|
||||
coherence(fact, existing_values) -> "consistent" | "conflict" | "novel"
|
||||
Does a new persona/identity value COHERE with what already exists?
|
||||
- "conflict": shares a DISTINCTIVE term AND a negation marker on it
|
||||
(a real contradiction, e.g. "never jokes" vs "dry humor").
|
||||
- "consistent": shares a distinctive term without negation (reinforces).
|
||||
- "novel": shares nothing distinctive — a new, additive axis (still
|
||||
promotable, but it does NOT rest on a shared-word coincidence).
|
||||
A shared COMMON word ("work", "always", "way") is never a relation.
|
||||
|
||||
grounded(fact, source_texts) -> bool
|
||||
Does the fact trace to real absorbed material via a DISTINCTIVE shared
|
||||
term (conjunctive), not just vector proximity or a common word?
|
||||
Lexical twin of the recall gate's dense check.
|
||||
|
||||
promote_eligible(proposal, existing, evidence_strength, min_strength) -> dict
|
||||
A constitution/persona promotion is eligible only when BOTH hold:
|
||||
- the evidence threshold is met (existing durability gate), AND
|
||||
- the proposal shares a DISTINCTIVE term with the existing block OR is
|
||||
a fully novel axis (a real new principle, not a word-coincidence).
|
||||
Returns {eligible, reason}.
|
||||
|
||||
Usage:
|
||||
from lexicon_evolution import coherence, grounded, promote_eligible
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
# SAME stopword set as memory_store._STOPWORDS (single source of truth would be
|
||||
# ideal; keep in sync — see memory_store.py).
|
||||
_STOP = {"the", "and", "for", "with", "that", "this", "from", "into",
|
||||
"when", "then", "were", "have", "been", "will", "was", "are",
|
||||
"but", "not", "you", "your", "also", "its", "his", "her", "him",
|
||||
"over", "under", "them", "they", "there", "about", "after",
|
||||
"what", "which", "who", "how", "why", "where", "while", "just",
|
||||
"more", "each", "than", "then", "very", "such", "some", "only",
|
||||
"prefers", "needs", "wants", "likes"}
|
||||
|
||||
# Hard stop for evolution: these generic framing words never carry a relation
|
||||
# ("work", "always", "way") — a persona built on them would be a coincidence.
|
||||
_FRAMING = {"work", "always", "way", "thing", "value", "identity", "persona",
|
||||
"someone", "something", "never", "always",
|
||||
"before", "after", "then", "when", "while", "must", "should",
|
||||
"will", "would", "could", "can", "have", "has", "had", "been",
|
||||
"being", "used", "use", "uses", "make", "makes", "made", "keep",
|
||||
"keeps", "take", "takes", "took", "get", "gets", "got", "going"}
|
||||
|
||||
_NEGATION = {"not", "never", "don't", "doesn't", "cannot", "can't", "no",
|
||||
"nor", "neither", "without", "avoid", "reject", "against",
|
||||
"refuses", "refuse", "opposed"}
|
||||
|
||||
_DISTINCTIVE_DF = 0.05 # same rare-word threshold as recall/associations
|
||||
|
||||
|
||||
def content_words(text):
|
||||
"""Significant content words (length > 3, not stop/framing)."""
|
||||
return [w for w in re.findall(r"[a-z]{4,}", (text or "").lower())
|
||||
if w not in _STOP and w not in _FRAMING]
|
||||
|
||||
|
||||
def distinctive_terms(fact, corpus_texts):
|
||||
"""The terms in `fact` that are RARE across the corpus (< 5% df).
|
||||
|
||||
Same signal as the recall gate: a term common in the store ("black",
|
||||
"physics") is NOT a relation; a rare term ("celesta", "oatmilk") is."""
|
||||
total = max(len(corpus_texts), 1)
|
||||
fact_terms = set(content_words(fact))
|
||||
if not fact_terms:
|
||||
return set()
|
||||
# stem-aware document frequency (migrate/migration count as one term)
|
||||
doc_terms = {}
|
||||
for t in corpus_texts:
|
||||
stems = {_stem(w) for w in re.findall(r"[a-z]{4,}", (t or "").lower())}
|
||||
for s in stems:
|
||||
doc_terms[s] = doc_terms.get(s, 0) + 1
|
||||
return {t for t in fact_terms
|
||||
if doc_terms.get(_stem(t), 0) < max(total * _DISTINCTIVE_DF, 1.5)}
|
||||
|
||||
|
||||
def shared_distinctive(fact, existing_values, corpus_texts):
|
||||
"""Distinctive terms shared between `fact` and existing values."""
|
||||
distinct = distinctive_terms(fact, corpus_texts)
|
||||
if not distinct:
|
||||
return set()
|
||||
shared = set()
|
||||
existing_stems = [(_stem(w), val) for val in existing_values
|
||||
for w in re.findall(r"[a-z]{4,}", (val or "").lower())]
|
||||
for term in distinct:
|
||||
ts = _stem(term)
|
||||
for s, _val in existing_stems:
|
||||
if s == ts:
|
||||
shared.add(term)
|
||||
break
|
||||
return shared
|
||||
|
||||
|
||||
def coherence(fact, existing_values, corpus_texts=()):
|
||||
"""Classify a new value against existing ones, lexicon-aware.
|
||||
|
||||
Returns "conflict" | "consistent" | "novel".
|
||||
"""
|
||||
values = [v for v in existing_values if v and v.strip()]
|
||||
if not values:
|
||||
return "novel"
|
||||
corpus = corpus_texts or values
|
||||
shared = shared_distinctive(fact, values, corpus)
|
||||
if not shared:
|
||||
# No distinctive shared term: the value is a NEW axis. This is NOT a
|
||||
# contradiction (a shared common word like "work" is meaningless), and
|
||||
# NOT a reinforcement — it adds independently.
|
||||
return "novel"
|
||||
low = fact.lower()
|
||||
has_neg = any(n in low for n in _NEGATION)
|
||||
if has_neg and any(f" {t} " in f" {low} " for t in shared):
|
||||
return "conflict"
|
||||
return "consistent"
|
||||
|
||||
|
||||
def grounded(fact, source_texts, min_distinct=1):
|
||||
"""Is `fact` grounded in real material via a DISTINCTIVE shared term?
|
||||
|
||||
Conjunctive/rare-word check (lexical twin of the recall gate): a fact that
|
||||
shares only common words or pure vector proximity with sources is NOT
|
||||
grounded — invented concepts must not shape personality/identity.
|
||||
|
||||
Morphology-aware: a term is grounded if its STEM appears in the sources
|
||||
("migrated" matches "migration", "retroactively" matches "retroactive"),
|
||||
using a light suffix fold — exact word identity is too strict for natural
|
||||
sources.
|
||||
"""
|
||||
srcs = [s for s in source_texts if s and s.strip()]
|
||||
if not srcs:
|
||||
return False
|
||||
fact_terms = set(content_words(fact))
|
||||
if not fact_terms:
|
||||
return False
|
||||
joined = " ".join(s.lower() for s in srcs)
|
||||
stems = {_stem(t) for t in re.findall(r"[a-z]{4,}", joined)}
|
||||
hits = 0
|
||||
for t in fact_terms:
|
||||
if _stem(t) in stems:
|
||||
hits += 1
|
||||
if hits >= min_distinct:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _stem(w):
|
||||
"""Light morphological fold for grounding: drop common English suffixes.
|
||||
Enough to equate migrate/migration/retroactively/retroactive and
|
||||
honest/honesty without a full stemmer (no external deps).
|
||||
|
||||
The suffix rules are ordered longest-first and grouped so that the 5-char
|
||||
participle forms ('ating', 'ated') are stripped before their 3-char base
|
||||
('ing', 'ed'), keeping migrate/migrating/migrated/migration on one root.
|
||||
"""
|
||||
# (suffix, optional extra fold applied after stripping)
|
||||
rules = [
|
||||
("ationally", ""), ("ableness", ""), ("iveness", ""),
|
||||
("ation", ""), ("ating", ""), ("ative", "e"),
|
||||
("atively", "e"), ("ability", ""), ("ities", "y"),
|
||||
("ingly", ""), ("fully", ""), ("ness", ""), ("ment", ""),
|
||||
("itive", "e"), ("atable", "e"), ("ated", ""), ("ates", "e"),
|
||||
("ions", ""), ("ings", ""), ("able", "e"), ("ible", "e"),
|
||||
("ing", "e"), ("ed", ""), ("es", ""), ("ly", ""), ("s", ""), ("e", ""),
|
||||
]
|
||||
orig = w
|
||||
for suf, fold in rules:
|
||||
if len(w) - len(suf) >= 4 and w.endswith(suf):
|
||||
w = w[: len(w) - len(suf)]
|
||||
if fold:
|
||||
w = w + fold
|
||||
break
|
||||
# y -> i fold (honesty -> honest)
|
||||
if len(w) > 4 and w.endswith("y") and w[-2] not in "aeiouy":
|
||||
w = w[:-1]
|
||||
return w
|
||||
|
||||
|
||||
def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
|
||||
corpus_texts=()):
|
||||
"""Is a persona/constitution promotion eligible, lexicon-aware?
|
||||
|
||||
Eligibility = evidence threshold AND (shares a distinctive term with the
|
||||
existing block OR is a genuinely novel axis grounded in the corpus). A
|
||||
proposal that rests only on common words is a word-coincidence and is
|
||||
parked, no matter how often it is sighted — mirroring the durability gate's
|
||||
"3+ references" rule but with a lexical floor. A "novel axis" is only
|
||||
eligible when it carries a DISTINCTIVE term that actually appears in the
|
||||
real absorbed material (the corpus), so arbitrary trivia ("pizza on
|
||||
fridays") cannot be promoted as a principle no matter the sighting count.
|
||||
"""
|
||||
if evidence_strength < min_strength:
|
||||
return {"eligible": False,
|
||||
"reason": f"evidence strength {evidence_strength} < {min_strength}"}
|
||||
existing = [e for e in existing if e and e.strip()]
|
||||
if not existing:
|
||||
return {"eligible": True, "reason": "empty block — first principle"}
|
||||
corpus = corpus_texts or existing
|
||||
shared = shared_distinctive(proposal, existing, corpus)
|
||||
if shared:
|
||||
return {"eligible": True,
|
||||
"reason": f"shares distinctive term(s): {sorted(shared)[:3]}"}
|
||||
# No distinctive shared term: a NOVEL axis is eligible only if it is
|
||||
# GROUNDED in the absorbed corpus via a distinctive term (a real principle
|
||||
# names something real), and the proposal is not trivial framing.
|
||||
terms = content_words(proposal)
|
||||
if not terms:
|
||||
return {"eligible": False, "reason": "no lexical signal"}
|
||||
if grounded(proposal, corpus, min_distinct=1):
|
||||
return {"eligible": True,
|
||||
"reason": f"novel axis grounded in corpus ({len(terms)} terms)"}
|
||||
return {"eligible": False,
|
||||
"reason": "novel axis not grounded in absorbed material — parked"}
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.108.2
|
||||
Version: v0.123.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1948,7 +1948,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.108.2 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.123.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -20,13 +20,13 @@ pub fn language_placement(project: &str, characteristics: &[&str]) -> serde_json
|
||||
|
||||
if project.to_lowercase() == "rune" {
|
||||
if !go_hits.is_empty() && rust_hits.is_empty() {
|
||||
return json!({"placement": "go", "rule": "reactive/initiating markers", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
return json!({"placement": "go", "rule": "rune dichotomy: reactive/initiating markers", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
} else if !rust_hits.is_empty() && go_hits.is_empty() {
|
||||
return json!({"placement": "rust", "rule": "deterministic/state markers", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
return json!({"placement": "rust", "rule": "rune dichotomy: deterministic/state markers", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
} else if !rust_hits.is_empty() && !go_hits.is_empty() {
|
||||
return json!({"placement": "hybrid", "rule": "both marker classes — contracted membrane junction", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
return json!({"placement": "hybrid", "rule": "rune dichotomy: both marker classes — a contracted membrane junction", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
}
|
||||
return json!({"placement": "rust", "rule": "pure compute defaults to the deterministic backend", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
return json!({"placement": "rust", "rule": "rune dichotomy: pure compute defaults to the deterministic backend", "go_signals": go_hits, "rust_signals": rust_hits});
|
||||
}
|
||||
json!({"placement": "unassessed", "rule": "no placement signals"})
|
||||
}
|
||||
|
||||
+168919
File diff suppressed because it is too large.
Load diff
+385
-292
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,159 @@
|
||||
"""test_lexicon_contract.py — the recovered provenance modules ship with
|
||||
coverage (henxel: new rune modules ship with tests or a canary).
|
||||
|
||||
lexicon_evolution.py + drift-ledger.py were recovered from ~/odysseus
|
||||
(0308ccc6 / rework-product-api HEAD) in commit 5749029 — the contract here
|
||||
pins their core semantics so a future regression cannot silently re-shape
|
||||
the lexicon discipline or the drift gate.
|
||||
|
||||
CONTRACT (contract BUILDER-2026-10-07T16:26:19Z-71624c + 16:31:17Z-997c9d):
|
||||
- coherence: conflict needs a DISTINCTIVE shared term AND a negation on it;
|
||||
a shared common word is never a relation ("black coffee" vs "black holes")
|
||||
- promote_eligible: evidence threshold AND (distinctive share OR novel axis
|
||||
grounded in the corpus); inventions park
|
||||
- drift-ledger: loads via the curator's importlib pattern and check() runs
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "src" / "rune"))
|
||||
|
||||
import lexicon_evolution as le # noqa: E402
|
||||
|
||||
|
||||
CORPUS = [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust",
|
||||
]
|
||||
|
||||
|
||||
class TestLexiconCoherence:
|
||||
def test_conflict_needs_negation_on_distinctive_term(self):
|
||||
assert (
|
||||
le.coherence("nick never writes Rust code",
|
||||
["nick writes Go and Rust code daily"], CORPUS)
|
||||
== "conflict"
|
||||
)
|
||||
|
||||
def test_shared_distinctive_without_negation_reinforces(self):
|
||||
assert (
|
||||
le.coherence("nick writes Rust code on weekends",
|
||||
["nick writes Go and Rust code daily"], CORPUS)
|
||||
== "consistent"
|
||||
)
|
||||
|
||||
def test_nothing_shared_is_novel(self):
|
||||
# Oracle-confirmed fixture: no distinctive term shared (in a
|
||||
# Nick-centric corpus a "nick"-bearing fact WOULD share "nick" —
|
||||
# df-1 distinctive there — so the novel-axis probe avoids it).
|
||||
assert (
|
||||
le.coherence("someone plays bass guitar in a band",
|
||||
["nick writes Go and Rust code daily"], CORPUS)
|
||||
== "novel"
|
||||
)
|
||||
|
||||
def test_common_word_is_never_a_relation(self):
|
||||
# "black" is common in the corpus — no distinctive share, so novel.
|
||||
assert (
|
||||
le.coherence("nick likes black coffee", ["black holes bend light"],
|
||||
["black coffee in the morning", "black holes bend light"])
|
||||
== "novel"
|
||||
)
|
||||
|
||||
def test_empty_existing_is_novel(self):
|
||||
assert le.coherence("nick writes Rust code daily", [], []) == "novel"
|
||||
|
||||
|
||||
class TestLexiconGrounding:
|
||||
def test_stem_aware_grounding(self):
|
||||
# migrated / migration share the stem.
|
||||
assert le.grounded("nick migrated the modules", CORPUS) is True
|
||||
|
||||
def test_invented_token_never_grounded(self):
|
||||
# Oracle-confirmed fixture: a fact sharing NO corpus stem parks
|
||||
# (framing words don't count; nothing here traces to the corpus).
|
||||
assert (
|
||||
le.grounded("someone collects vintage railway timetables", CORPUS)
|
||||
is False
|
||||
)
|
||||
|
||||
def test_empty_sources_not_grounded(self):
|
||||
assert le.grounded("nick migrated the modules", []) is False
|
||||
|
||||
|
||||
class TestPromoteEligible:
|
||||
def test_strength_gate_parks_first(self):
|
||||
r = le.promote_eligible("add support for retries",
|
||||
["support for retries in the fetch loop"], 2, 3, ())
|
||||
assert r == {"eligible": False, "reason": "evidence strength 2 < 3"}
|
||||
|
||||
def test_shared_distinctive_is_eligible(self):
|
||||
r = le.promote_eligible("add retries to the fetch loop",
|
||||
["support for retries in the fetch loop"], 3, 3, ())
|
||||
assert r["eligible"] is True
|
||||
assert "shares distinctive term(s)" in r["reason"]
|
||||
|
||||
def test_novel_axis_not_grounded_parks(self):
|
||||
# Oracle-confirmed fixture: nothing in the proposal traces to the
|
||||
# corpus (a "nick"-bearing fact WOULD ground — nick is df-1
|
||||
# distinctive in this corpus), so the novel axis parks.
|
||||
r = le.promote_eligible("someone collects vintage railway timetables",
|
||||
["nick writes Go and Rust code daily"], 3, 3, CORPUS)
|
||||
assert r["eligible"] is False
|
||||
assert r["reason"] == "novel axis not grounded in absorbed material — parked"
|
||||
|
||||
def test_first_principle_on_empty_block(self):
|
||||
r = le.promote_eligible("parking is never wrong for growth", [], 3, 3, ())
|
||||
assert r == {"eligible": True, "reason": "empty block — first principle"}
|
||||
|
||||
def test_no_lexical_signal(self):
|
||||
r = le.promote_eligible("work things and ways", ["nick writes code"], 3, 3, ())
|
||||
# framing-only proposals carry no signal
|
||||
assert r["eligible"] in (True, False) # oracle decides; must not raise
|
||||
assert isinstance(r["reason"], str) and r["reason"]
|
||||
|
||||
|
||||
class TestDriftLedgerLoadsAndChecks:
|
||||
def _load(self):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"drift_ledger_under_test",
|
||||
str(ROOT / "src" / "rune" / "drift-ledger.py"),
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
def test_module_loads_via_curator_pattern(self):
|
||||
mod = self._load()
|
||||
assert hasattr(mod, "check")
|
||||
assert hasattr(mod, "INDEX_FILE") and hasattr(mod, "DB")
|
||||
|
||||
def test_check_runs_on_synthetic_store(self, tmp_path):
|
||||
mod = self._load()
|
||||
# isolate every artifact the gate touches (the curator patches the
|
||||
# same attrs before calling check — parity with run_plan).
|
||||
blocks = tmp_path / "blocks"
|
||||
blocks.mkdir()
|
||||
(blocks / "project.md").write_text(
|
||||
"---\nanchor: test\n---\n- the project keeps a test anchor line\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
index = tmp_path / "index"
|
||||
index.mkdir()
|
||||
(index / "blocks_meta.json").write_text("{}", encoding="utf-8")
|
||||
journal = tmp_path / "journal"
|
||||
journal.mkdir()
|
||||
mod.BLOCKS_DIR = str(blocks)
|
||||
mod.INDEX_FILE = str(index / "blocks_meta.json")
|
||||
mod.JOURNAL_DIR = str(journal)
|
||||
mod.DB = str(tmp_path / "store.db")
|
||||
rc = mod.check(strict=False)
|
||||
assert rc in (0, 1) # healthy or flagged — never a crash
|
||||
Reference in new issue
Block a user