Compare commits
22
Commits
v0.268.0
...
tmp-detach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da481e744d | ||
|
|
c66d4babbe | ||
|
|
201530bfc4 | ||
|
|
57fc6a61cf | ||
|
|
4ed47a7332 | ||
|
|
63304b0984 | ||
|
|
8119914fc3 | ||
|
|
a829834f8d | ||
|
|
eda992ade8 | ||
|
|
facbb52b7e | ||
|
|
f4f099bcde | ||
|
|
7fd1df00be | ||
|
|
12cca6e682 | ||
|
|
d00f07d3ea | ||
|
|
92761be44b | ||
|
|
709c2ca4e5 | ||
|
|
150f5b86cb | ||
|
|
527dc7749e | ||
|
|
61e6d8fbd0 | ||
|
|
1f0e61d460 | ||
|
|
8d435ce7e4 | ||
|
|
c6148532d7 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.268.0**
|
||||
**Current release: v0.271.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.**
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**FULL completion: 26%** (50/192 runtime modules at FULL)
|
||||
**FULL completion: 28%** (54/192 runtime modules at FULL)
|
||||
|
||||
Function-level detail: 51% (1219/2375 runtime functions matched) — secondary; includes in-flight work
|
||||
Function-level detail: 54% (1290/2376 runtime functions matched) — secondary; includes in-flight work
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 50 | 18817 | 80%+ of functions ported |
|
||||
| PARTIAL | 97 | 43133 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 45 | 11390 | no name-preserving implementation |
|
||||
| Total runtime | 192 | 73340 | |
|
||||
| FULL | 54 | 21059 | 80%+ of functions ported |
|
||||
| PARTIAL | 94 | 40967 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 44 | 11346 | no name-preserving implementation |
|
||||
| Total runtime | 192 | 73372 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -53,7 +53,6 @@ Function-level detail: 51% (1219/2375 runtime functions matched) — secondary;
|
||||
| wait_guard_corpus | 46 | 0 | RUST |
|
||||
| canary_cognitive_strata | 44 | 1 | TEST-INFRA |
|
||||
| research_budget | 44 | 3 | unclassified |
|
||||
| self_model | 44 | 3 | unclassified |
|
||||
| evidence_capsule | 43 | 3 | unclassified |
|
||||
| builder_bridge_cli | 42 | 1 | GO |
|
||||
| __init__ | 40 | 1 | RUST |
|
||||
@@ -69,19 +68,15 @@ Function-level detail: 51% (1219/2375 runtime functions matched) — secondary;
|
||||
|---|---|---|---|
|
||||
| memory_store | 2333 | 30/78 | _ctx_budget, now_iso, ensure_memory_type_column, _embed, _temp_json, _entities_to_json |
|
||||
| rune_operator | 2746 | 25/68 | _build_model_router, _sorted_tool_names, _filter_safety_noise, _wire_auto_filer, _reconcile_guarded, get_operator |
|
||||
| context_broker | 870 | 6/28 | _py_default_estimator, _cc_core, _py_default_budget, _py_default_memory_budget, _py_memory_disabled_value, reset_discovered_windows |
|
||||
| construction_interface | 1193 | 6/27 | _to_stage_check, _guard_specs_from_obligations, t_layout, _obligations_projection, _obligation_scope_files, main |
|
||||
| structural_store | 690 | 13/34 | _ss_core, _sorted, new_doc, _redact_secrets, save, canonical_digest |
|
||||
| provider_registry | 679 | 11/28 | _ctx_budget, _format_tools_openai, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
|
||||
| warm_router | 1518 | 19/36 | _ensure_use_history_table, record_outcome, record_neuron_fire, _py_base_level_from_uses, base_level_activation, _db |
|
||||
| construction_decision | 985 | 4/20 | normalize_need, _sc_core, _py_language_placement, resolve_surface, ss_entity, implemented_contracts |
|
||||
| work_manager | 594 | 9/25 | group_by_file, load_config, save_config, load_batches, save_batches, create_work |
|
||||
| growth_delta | 449 | 5/20 | _ctx_budget, _store_db, _llm, _parse_json, _load_deltas, _save_deltas |
|
||||
| provider_budget | 357 | 5/20 | get_tracker, track_usage, check_provider, get_best_provider, failover, get_status |
|
||||
| safety_override | 574 | 7/22 | _keyword_stopwords, _cc_probe, cc_loader_pin, self_test, _py_matches, escalation_rate |
|
||||
| spine | 975 | 20/35 | _py_sorted, _module_imports, _py_declared_surfaces, _py_project_meta_surfaces, _gather_meta_inputs, _rust_core |
|
||||
| cron_engine | 440 | 13/27 | get_cron_engine, run_task, start_engine, stop_engine, to_dict, fire_on_task_complete |
|
||||
| night_mode | 421 | 5/19 | _ctx_budget, get_night_mode, start_night_mode, stop_night_mode, get_status, get_report |
|
||||
| tool_executor | 1836 | 33/47 | _execute_tool_now, serialize_prepared_tool, deserialize_prepared_tool, confirm_write, _resolve_execution_cwd, _run_command |
|
||||
| cache | 492 | 6/17 | _record_hit, _record_miss, flush_stats, get_cache, put_cache, invalidate_scope |
|
||||
| elastic_cognition | 774 | 3/14 | _ctx_budget, _build_constitution, _get_constitution, _load_cloud_config, execute_at_tier, _execute_deterministic |
|
||||
@@ -164,6 +159,7 @@ Function-level detail: 51% (1219/2375 runtime functions matched) — secondary;
|
||||
| meaningful_events | 102 | 3/4 | _kernel |
|
||||
| pane_authority | 98 | 2/3 | _kernel |
|
||||
| recall_router | 165 | 2/3 | handle_hook |
|
||||
| self_model | 76 | 3/4 | _core |
|
||||
|
||||
## FULL — ported, parity-gated
|
||||
|
||||
@@ -171,16 +167,18 @@ Function-level detail: 51% (1219/2375 runtime functions matched) — secondary;
|
||||
|---|---|---|---|
|
||||
| curator | 1838 | 43/43 | GO |
|
||||
| sleep_time | 1056 | 26/27 | GO |
|
||||
| context_broker | 870 | 28/28 | GO |
|
||||
| local_tool_runtime | 809 | 30/30 | GO |
|
||||
| memory_consolidation | 770 | 23/23 | GO |
|
||||
| persona_gate | 724 | 26/26 | GO |
|
||||
| abstraction | 656 | 17/17 | GO |
|
||||
| system_registry | 603 | 14/16 | RUST |
|
||||
| work_manager | 594 | 25/25 | GO |
|
||||
| construction_intent | 580 | 13/16 | RUST |
|
||||
| capability_ledger | 571 | 18/19 | RUST |
|
||||
| drift_ledger | 548 | 20/22 | RUST |
|
||||
| failure_auto_filer | 527 | 24/24 | GO |
|
||||
| build_mode | 521 | 32/33 | GO |
|
||||
| build_mode | 521 | 33/33 | GO |
|
||||
| event_wait | 518 | 14/14 | GO |
|
||||
| worthiness | 469 | 12/14 | unclassified |
|
||||
| tool_protocol | 461 | 22/27 | GO |
|
||||
@@ -189,8 +187,10 @@ Function-level detail: 51% (1219/2375 runtime functions matched) — secondary;
|
||||
| goal | 453 | 20/20 | GO |
|
||||
| release_notes | 446 | 16/16 | GO |
|
||||
| model_routing | 432 | 13/13 | GO |
|
||||
| night_mode | 421 | 19/19 | GO |
|
||||
| build_basis | 375 | 11/13 | RUST |
|
||||
| attention_capsule | 365 | 6/7 | RUST |
|
||||
| provider_budget | 357 | 20/20 | GO |
|
||||
| drift-ledger | 355 | 14/14 | unclassified |
|
||||
| outcome_ledger | 318 | 7/8 | RUST |
|
||||
| message_queue | 316 | 13/13 | GO |
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
"auto_tasks",
|
||||
"behaviour_ledger",
|
||||
"build_basis",
|
||||
"build_mode",
|
||||
"capability_ledger",
|
||||
"concept_topology",
|
||||
"construction_intent",
|
||||
"construction_patterns_builtin",
|
||||
"context_broker",
|
||||
"curator",
|
||||
"drift-ledger",
|
||||
"drift_ledger",
|
||||
@@ -23,11 +25,14 @@
|
||||
"forge_client",
|
||||
"goal",
|
||||
"impact_preview",
|
||||
"inference_paths",
|
||||
"local_tool_runtime",
|
||||
"meaningful_events",
|
||||
"memory_consolidation",
|
||||
"message_graph",
|
||||
"message_queue",
|
||||
"model_routing",
|
||||
"night_mode",
|
||||
"openai_compat_transport",
|
||||
"outcome_ledger",
|
||||
"outcome_utility",
|
||||
@@ -36,10 +41,12 @@
|
||||
"programme_continuation",
|
||||
"programme_gates",
|
||||
"project_registry",
|
||||
"provider_budget",
|
||||
"provider_context",
|
||||
"release_notes",
|
||||
"route_risk_flags",
|
||||
"rune_spinner",
|
||||
"self_model",
|
||||
"self_modify",
|
||||
"shard_manager",
|
||||
"sleep_time",
|
||||
@@ -50,32 +57,31 @@
|
||||
"toolchain",
|
||||
"verification_layer",
|
||||
"wait_guard",
|
||||
"work_manager",
|
||||
"worthiness"
|
||||
],
|
||||
"in_flight": {
|
||||
"inference_paths": "rune-wave-bb",
|
||||
"skill_compiler": "rune-wave-bc",
|
||||
"meaningful_events": "rune-wave-bc",
|
||||
"memory_types": "rune-wave-bd",
|
||||
"paths": "rune-wave-bd",
|
||||
"growth_pipeline": "rune-wave-bd",
|
||||
"wait_guard_corpus": "rune-wave-be",
|
||||
"self_model": "rune-wave-be",
|
||||
"research_budget": "rune-wave-be",
|
||||
"evidence_capsule": "rune-wave-be",
|
||||
"builder_bridge_cli": "rune-wave-be",
|
||||
"context_broker": "rune-wave-bf",
|
||||
"structural_store": "rune-wave-bh",
|
||||
"work_manager": "rune-wave-bk",
|
||||
"growth_delta": "rune-wave-bl",
|
||||
"provider_budget": "rune-wave-bm",
|
||||
"safety_override": "rune-wave-bn",
|
||||
"construction_decision": "rune-wave-bo",
|
||||
"provider_registry": "rune-wave-bq",
|
||||
"night_mode": "rune-wave-br",
|
||||
"build_mode": "rune-wave-bs",
|
||||
"construction_interface": "rune-wave-bu",
|
||||
"spine": "rune-wave-bv"
|
||||
"spine": "rune-wave-bv",
|
||||
"hook_engine": "rune-wave-bw",
|
||||
"elastic_cognition": "rune-wave-bx",
|
||||
"contracts": "rune-wave-by",
|
||||
"adaptive_router": "rune-wave-bz",
|
||||
"memory_selector": "rune-wave-ca",
|
||||
"construction_pattern": "rune-wave-cb"
|
||||
},
|
||||
"excluded": [
|
||||
"TUI-LAST modules",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.268.0"
|
||||
version = "0.271.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity check: the Go context-broker binary vs the LIVE Python oracle
|
||||
(RUNE_COGNITION_CORE=python pinned AND delegation auto), fuzz + edge cases.
|
||||
Complements the frozen battery (truth.json) with generated inputs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
BIN = Path("/tmp/cb-live-parity")
|
||||
|
||||
|
||||
def build():
|
||||
subprocess.run(["go", "build", "-o", str(BIN), "."],
|
||||
cwd=ROOT / "src/go/context-broker", check=True)
|
||||
|
||||
|
||||
def go_eval(payload: dict) -> dict:
|
||||
proc = subprocess.run([str(BIN), "eval"], input=json.dumps(payload),
|
||||
capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"go eval failed: {proc.stderr}")
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
fails = 0
|
||||
checked = 0
|
||||
|
||||
|
||||
def check(name, got, want):
|
||||
global fails, checked
|
||||
checked += 1
|
||||
if got != want:
|
||||
fails += 1
|
||||
print(f"DIVERGE {name}: go={got!r} py={want!r}")
|
||||
|
||||
|
||||
def est_case(text):
|
||||
check(f"est[{text!r:.40}]", go_eval({"op": "est", "text": text}),
|
||||
py._py_default_estimator(text))
|
||||
|
||||
|
||||
def budget_case(w):
|
||||
check(f"budget[{w}]", go_eval({"op": "budget", "window": w}),
|
||||
py._py_default_budget(w))
|
||||
|
||||
|
||||
def membud_case(w):
|
||||
check(f"membud[{w}]", go_eval({"op": "membud", "window": w}),
|
||||
py._py_default_memory_budget(w))
|
||||
|
||||
|
||||
def memoff_case(v):
|
||||
check(f"memoff[{v!r}]", go_eval({"op": "memoff", "value": v}),
|
||||
py._py_memory_disabled_value(v))
|
||||
|
||||
|
||||
def through_case(messages, through):
|
||||
want = py._through_id(json.loads(json.dumps(messages)), through)
|
||||
got = go_eval({"op": "through", "messages": messages, "through": through})
|
||||
check(f"through[{through}/{len(messages)}]", got, want)
|
||||
|
||||
|
||||
rng = random.Random(2479)
|
||||
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python"
|
||||
from rune import context_broker as py # noqa: E402
|
||||
|
||||
# fuzz: estimator
|
||||
for _ in range(200):
|
||||
n = rng.randint(0, 300)
|
||||
est_case("".join(rng.choice(string.printable[:94]) for _ in range(n)))
|
||||
est_case("héllo wörld ünïcode ✓ 日本語テキスト")
|
||||
est_case("🎉 emoji surrogate pair")
|
||||
|
||||
# fuzz: budgets (incl. boundaries around max(2048, w/4) crossover)
|
||||
for w in [0, 1, 2559, 2560, 2561, 8191, 8192, 8193, 10000, 65536, 131072,
|
||||
200000, 10**9] + [rng.randint(0, 200000) for _ in range(100)]:
|
||||
budget_case(w)
|
||||
membud_case(w)
|
||||
|
||||
# fuzz: memory-off raw values
|
||||
for v in ["", "0", "00", "false", "FALSE", "False", "no", "No", "off", "OFF",
|
||||
"off ", " off", "1", "true", "YES", "on", "anything", "\t", "\n",
|
||||
"0.0", "nil", "None", "null"]:
|
||||
memoff_case(v)
|
||||
|
||||
# fuzz: through_id over random message shapes
|
||||
for _ in range(100):
|
||||
msgs = []
|
||||
for _ in range(rng.randint(1, 8)):
|
||||
m = {"role": rng.choice(["user", "assistant", "system"]),
|
||||
"content": "".join(rng.choice(string.printable[:70]) for _ in range(rng.randint(0, 40)))}
|
||||
if rng.random() < 0.3:
|
||||
m["extra"] = {"n": rng.randint(0, 5), "flag": rng.random() < 0.5}
|
||||
msgs.append(m)
|
||||
through_case(msgs, rng.randint(-1, len(msgs) + 1))
|
||||
|
||||
# unicode through_id (ensure_ascii path)
|
||||
uni = [{"role": "user", "content": "日本語 テキスト 🎉 emoji"},
|
||||
{"role": "assistant", "content": "héllo wörld"}]
|
||||
through_case(uni, 1)
|
||||
through_case(uni, 0)
|
||||
|
||||
# fuzz: admission kernel (priority desc, recency tie, budget, memory cap)
|
||||
NAMES = ["system", "user", "history", "memory", "persona_memories", "tools", "notes"]
|
||||
for case_i in range(150):
|
||||
entries = []
|
||||
for j in range(rng.randint(1, 7)):
|
||||
name = rng.choice(NAMES)
|
||||
entries.append({
|
||||
"name": name,
|
||||
"priority": rng.randint(0, 100),
|
||||
"tokens": rng.randint(1, 900),
|
||||
"source": f"src-{j}",
|
||||
"evictable": name not in ("system", "user"),
|
||||
})
|
||||
budget = rng.choice([500, 1000, 2000, 4000])
|
||||
mem_budget = rng.choice([0, 300, 1200])
|
||||
got = go_eval({"op": "admit", "entries": entries, "budget": budget,
|
||||
"memory_budget": mem_budget})
|
||||
bk = py.ContextBroker()
|
||||
py_entries = [(e["name"], py.ContributionBlock(e["name"], e["priority"],
|
||||
"c" * e["tokens"], e["tokens"],
|
||||
e["source"], evictable=e["evictable"]), j)
|
||||
for j, e in enumerate(entries)]
|
||||
try:
|
||||
_adm, adm = bk._admit(py_entries, budget, memory_budget=mem_budget)
|
||||
want = {"admitted": [bl.name for bl in _adm],
|
||||
"blocks": adm["blocks"], "admitted_total": adm["admitted_total"],
|
||||
"dropped": [d["name"] for d in adm["dropped"]],
|
||||
"memory_budget": adm["memory_budget"], "memory_used": adm["memory_used"]}
|
||||
check(f"admit[{case_i}]", got, json.loads(json.dumps(want, sort_keys=True)))
|
||||
except py.ContextOverflowError as exc:
|
||||
# Go wraps the oracle's message with the wrapped sentinel; the oracle
|
||||
# text must be a verbatim prefix of the Go detail.
|
||||
ok = (isinstance(got, dict) and got.get("error") == "ContextOverflowError"
|
||||
and got.get("detail", "").startswith(str(exc)))
|
||||
check(f"admit[{case_i}]", "ContextOverflowError" if ok else got,
|
||||
"ContextOverflowError")
|
||||
|
||||
# fuzz: render (system folding, history pass-through, user capture)
|
||||
for case_i in range(100):
|
||||
blocks = []
|
||||
for j in range(rng.randint(1, 6)):
|
||||
name = rng.choice(NAMES)
|
||||
if rng.random() < 0.5:
|
||||
content = "".join(rng.choice(string.ascii_letters) for _ in range(rng.randint(1, 30)))
|
||||
else:
|
||||
content = [{"role": rng.choice(["user", "assistant"]),
|
||||
"content": "".join(rng.choice(string.ascii_letters) for _ in range(rng.randint(1, 20)))}
|
||||
for _ in range(rng.randint(1, 3))]
|
||||
blocks.append({"name": name, "priority": rng.randint(0, 100),
|
||||
"tokens": 0, "source": f"src-{j}", "content": content})
|
||||
got = go_eval({"op": "render", "blocks": blocks})
|
||||
py_blocks = []
|
||||
for b in blocks:
|
||||
content = b["content"]
|
||||
if isinstance(content, list):
|
||||
content = [dict(m) for m in content]
|
||||
py_blocks.append(py.ContributionBlock(b["name"], b["priority"], content,
|
||||
b["tokens"], b["source"]))
|
||||
caps = py.ContextBroker()._render(py_blocks)
|
||||
want = {"token_count": caps.token_count, "messages": caps.messages,
|
||||
"system_preamble": caps.system_preamble, "user_content": caps.user_content,
|
||||
"blocks": [bl.name for bl in caps.blocks]}
|
||||
check(f"render[{case_i}]", got, json.loads(json.dumps(want)))
|
||||
|
||||
print(f"live parity: {checked - fails}/{checked} cases match "
|
||||
f"(RUNE_COGNITION_CORE=python pinned)")
|
||||
sys.exit(1 if fails else 0)
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity for the night_mode dichotomy wave: the Go port
|
||||
(src/go/night-mode) and the Python oracle (rune.night_mode) must produce
|
||||
byte-identical results for the closed gaps, driven from IDENTICAL state
|
||||
files (status/report/stop are pure projections of state; _ctx_budget is
|
||||
covered by the frozen truth battery). started_at strings round-trip
|
||||
unmodified through both engines, so no normalization is needed.
|
||||
|
||||
Usage: python3 scripts/check_night_mode_parity.py
|
||||
Exits non-zero on any divergence.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
GO_DIR = REPO / "src/go/night-mode"
|
||||
|
||||
SCENARIOS = {
|
||||
"idle": {},
|
||||
"running": {
|
||||
"active": True, "started_at": "2026-10-13T03:00:00Z",
|
||||
"duration_hours": 8.0, "current_phase": "iterate", "iterations": 7,
|
||||
"improvements": ["Memory consolidation completed", "Q: q A: a"],
|
||||
"questions": ["Iteration 0: What should I improve next?"],
|
||||
"decisions": [], "errors": ["LLM iteration failed: boom"],
|
||||
},
|
||||
"stopping": {
|
||||
"active": False, "started_at": None, "duration_hours": 4.5,
|
||||
"current_phase": "stopping", "iterations": 2,
|
||||
"improvements": [], "questions": [], "decisions": ["x"], "errors": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def py_status(state_file):
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
import rune.night_mode as nm
|
||||
|
||||
return nm.NightMode(storage_path=str(state_file)).status()
|
||||
|
||||
|
||||
def py_report(state_file):
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
import rune.night_mode as nm
|
||||
|
||||
return nm.NightMode(storage_path=str(state_file)).report()
|
||||
|
||||
|
||||
def py_stop(state_file):
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
import rune.night_mode as nm
|
||||
|
||||
return nm.NightMode(storage_path=str(state_file)).stop()
|
||||
|
||||
|
||||
def go_cli(binary, mem_dir, *args):
|
||||
env = {**os.environ, "RUNE_MEMORY_DIR": str(mem_dir)}
|
||||
p = subprocess.run([str(binary), *args], capture_output=True, text=True, env=env, timeout=30)
|
||||
return json.loads(p.stdout)
|
||||
|
||||
|
||||
def canon(v):
|
||||
return json.loads(json.dumps(v))
|
||||
|
||||
|
||||
def main():
|
||||
subprocess.run(["go", "build", "-o", GO_DIR / "night-mode-bin", "."],
|
||||
cwd=GO_DIR, check=True)
|
||||
binary = GO_DIR / "night-mode-bin"
|
||||
failures = 0
|
||||
|
||||
for name, state in SCENARIOS.items():
|
||||
for op, py_fn in (("status", py_status), ("report", py_report), ("stop", py_stop)):
|
||||
py_dir = Path(tempfile.mkdtemp(prefix=f"nm-py-{name}-"))
|
||||
go_dir = Path(tempfile.mkdtemp(prefix=f"nm-go-{name}-"))
|
||||
for d in (py_dir, go_dir):
|
||||
(d / "night_mode.json").write_text(json.dumps(state))
|
||||
try:
|
||||
expected = py_fn(py_dir / "night_mode.json")
|
||||
actual = go_cli(binary, go_dir, op)
|
||||
if canon(actual) != canon(expected):
|
||||
print(f"DIVERGENCE {name}/{op}:\n py: {canon(expected)}\n go: {canon(actual)}")
|
||||
failures += 1
|
||||
else:
|
||||
print(f"parity ok: {name}/{op}")
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"ERROR {name}/{op}: {e}")
|
||||
failures += 1
|
||||
|
||||
# delegation live: full CLI round-trip start→status→stop→report
|
||||
live = Path(tempfile.mkdtemp(prefix="nm-live-"))
|
||||
env = {**os.environ, "RUNE_MEMORY_DIR": str(live)}
|
||||
seq = [
|
||||
(["status"], lambda out: out["active"] is False and out["phase"] == "idle"),
|
||||
(["start", "4"], lambda out: out["success"] is True
|
||||
and out["state"]["duration_hours"] == 4.0),
|
||||
(["status"], lambda out: out["active"] is True),
|
||||
(["stop"], lambda out: out["success"] is True
|
||||
and out["state"]["current_phase"] == "stopping"),
|
||||
(["stop"], lambda out: out["success"] is False
|
||||
and out["error"] == "Night mode not active"),
|
||||
(["report"], lambda out: "0 iterations" in out["summary"]),
|
||||
]
|
||||
for argv, check in seq:
|
||||
out = go_cli(binary, live, *argv)
|
||||
if not check(out):
|
||||
print(f"LIVE DIVERGENCE {argv}: {out}")
|
||||
failures += 1
|
||||
state_on_disk = json.loads((live / "night_mode.json").read_text())
|
||||
if state_on_disk["active"] or state_on_disk["current_phase"] != "stopping":
|
||||
print(f"LIVE DIVERGENCE state file: {state_on_disk}")
|
||||
failures += 1
|
||||
else:
|
||||
print("parity ok: live CLI round-trip (delegation live)")
|
||||
|
||||
(GO_DIR / "night-mode-bin").unlink(missing_ok=True)
|
||||
if failures:
|
||||
print(f"FAIL: {failures} divergences")
|
||||
return 1
|
||||
print("LIVE PARITY 100%: status/report/stop × 3 scenarios + CLI round-trip")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: structural_store Rust kernels vs Python oracle (dichotomy
|
||||
wave, contract BUILDER-2026-10-13T00:32:22Z-bc3aae-factory-gap-ss). Layers:
|
||||
|
||||
1. FROZEN — the frozen truth corpus (frozen_ss_truth.json, generated by
|
||||
scripts/gen_structural_store_truth.py against the LIVE oracle with
|
||||
RUNE_STRUCTURAL_CORE=python pinned) replayed through the ctypes FFI,
|
||||
case by case, deep-copied inputs.
|
||||
2. FUZZ — 300 seeded random docs (seed 2479) per kernel: oracle floor
|
||||
vs FFI, exact compare; save() compared BYTE-EXACT on real files.
|
||||
3. LIVE — dual-mode facade battery: the same facade calls executed with
|
||||
RUNE_STRUCTURAL_CORE=python (oracle floor) and with the Rust core live;
|
||||
results must be identical (timestamps masked).
|
||||
|
||||
Exit 0 only at 100% parity on the gap-closing functions.
|
||||
"""
|
||||
import calendar
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import tempfile
|
||||
import time as _time
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_STRUCTURAL_CORE"] = "python" # oracle floor for generation
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import ss_loader # noqa: E402
|
||||
|
||||
FIXED_NOW = "2026-10-13T01:02:03Z"
|
||||
FIXED_EPOCH = calendar.timegm(_time.strptime(FIXED_NOW, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
ss.now = lambda: FIXED_NOW
|
||||
ss.time = types.SimpleNamespace( # snapshot() stamps straight off time.*
|
||||
gmtime=lambda: _time.gmtime(FIXED_EPOCH),
|
||||
strftime=lambda fmt, t=None: _time.strftime(fmt, t if t is not None else _time.gmtime()),
|
||||
)
|
||||
|
||||
TRUTH = json.loads((REPO / "src" / "rust" / "structural_store" / "src"
|
||||
/ "frozen_ss_truth.json").read_text())
|
||||
|
||||
fails: list[str] = []
|
||||
counts = {"frozen": 0, "fuzz": 0, "live": 0}
|
||||
|
||||
|
||||
def fail(layer, name, detail):
|
||||
fails.append(f"[{layer}] {name}: {detail}")
|
||||
print(f"FAIL[{layer}] {name}: {detail}")
|
||||
if len(fails) <= 3:
|
||||
import difflib
|
||||
w, _, g = detail.partition(" got=")
|
||||
want_s = w[5:] if w.startswith("want=") else w
|
||||
a = want_s.replace(r"\u", "\\u").splitlines()
|
||||
b = g.splitlines()
|
||||
dump = "\n".join(difflib.unified_diff(a, b, lineterm=""))[:2000]
|
||||
(REPO / "parity_first_failures.log").open("a").write(
|
||||
f"=== {layer}/{name} ===\n{dump}\n")
|
||||
|
||||
|
||||
def j(x):
|
||||
return json.dumps(x, sort_keys=True, ensure_ascii=True)
|
||||
|
||||
|
||||
def check(layer, name, want, got):
|
||||
counts[layer] += 1
|
||||
if j(want) != j(got):
|
||||
fail(layer, name, f"want={j(want)[:300]} got={j(got)[:300]}")
|
||||
|
||||
|
||||
# ── oracle dispatch (python floor) ──────────────────────────────────────────
|
||||
|
||||
def oracle_call(fn, a):
|
||||
return {
|
||||
"sorted": lambda: ss._sorted(a["value"]),
|
||||
"new_doc": lambda: ss.new_doc(),
|
||||
"redact": lambda: ss._redact_secrets(a["value"]),
|
||||
"digest": lambda: ss.canonical_digest(a["doc"]),
|
||||
"alias": lambda: _mut(lambda d: ss.alias(d, a["cid"], a["old"]), a["doc"]),
|
||||
"spine_uri": lambda: ss.spine_uri(a["entity_id"]),
|
||||
"resolve_uri": lambda: ss.resolve_uri(a["uri"]),
|
||||
"doc_authorities": lambda: _mut(ss._doc_authorities, a["doc"]),
|
||||
"authority_health": lambda: ss.authority_health(a["doc"]),
|
||||
"bind_task": lambda: _mut(lambda d: ss.bind_task(
|
||||
d, a["task_id"], a["entity_ids"], a["evidence_ref"]), a["doc"]),
|
||||
"task_entities": lambda: ss.task_entities(a["doc"], a["task_id"]),
|
||||
"impact_frontier": lambda: ss.impact_frontier(
|
||||
a["doc"], a["seeds"], max_level=a.get("max_level", 2),
|
||||
change_kind=a.get("change_kind")),
|
||||
"observed_impact": lambda: _mut_ret(lambda d: ss.record_observed_impact(
|
||||
d, a["task_id"], a["items"]), a["doc"]),
|
||||
"predicted_vs_observed": lambda: ss.predicted_vs_observed(
|
||||
a["predicted"], a["observed_doc"], a["seeds"]),
|
||||
"events": lambda: ss.structural_events(a["doc"]),
|
||||
"diff": lambda: ss.diff(a["old"], a["new"]),
|
||||
"dedupe": lambda: _mut(ss.dedupe_observations, a["doc"]),
|
||||
"promote_fix": lambda: _mut(lambda d: ss.promote(
|
||||
d, a["id"], a["state"], a["ev"]), a["doc"]),
|
||||
"resolve_status_fix": lambda: ss.resolve_status(a["entity"]),
|
||||
"supersede_fix": lambda: _mut(lambda d: ss.supersede(
|
||||
d, a["new"], a["old"], a["ev"]), a["doc"]),
|
||||
}[fn]()
|
||||
|
||||
|
||||
def _mut(call, doc):
|
||||
d = copy.deepcopy(doc)
|
||||
try:
|
||||
ret = call(d)
|
||||
except ValueError as exc:
|
||||
return {"__err__": str(exc)}
|
||||
return {"__doc__": d, "__ret__": ret}
|
||||
|
||||
|
||||
def _mut_ret(call, doc):
|
||||
d = copy.deepcopy(doc)
|
||||
ret = call(d)
|
||||
return {"__doc__": d, "__ret__": ret}
|
||||
|
||||
|
||||
# ── FFI dispatch ────────────────────────────────────────────────────────────
|
||||
|
||||
def ffi_call(fn, a, tmp):
|
||||
return {
|
||||
"sorted": lambda: ss_loader.sorted_value(a["value"]),
|
||||
"new_doc": lambda: ss_loader.new_doc(FIXED_NOW),
|
||||
"redact": lambda: ss_loader.redact_secrets(a["value"]),
|
||||
"digest": lambda: ss_loader.canonical_digest(a["doc"]),
|
||||
"alias": lambda: _ffi_mut(
|
||||
lambda d: ss_loader.alias(d, a["cid"], a["old"], FIXED_NOW), a["doc"],
|
||||
ret_fn=lambda d: ss.entity(d, a["cid"])),
|
||||
"spine_uri": lambda: ss_loader.spine_uri(a["entity_id"]),
|
||||
"resolve_uri": lambda: ss_loader.resolve_uri(a["uri"]),
|
||||
"doc_authorities": lambda: _ffi_mut(
|
||||
ss_loader.doc_authorities, a["doc"],
|
||||
ret_fn=lambda d: copy.deepcopy(ss._doc_authorities(d))),
|
||||
"authority_health": lambda: ss_loader.authority_health(a["doc"]),
|
||||
"bind_task": lambda: _ffi_mut(
|
||||
lambda d: ss_loader.bind_task(
|
||||
d, a["task_id"], a["entity_ids"], a["evidence_ref"], FIXED_NOW), a["doc"],
|
||||
ret_fn=lambda d: ss.entity(d, f"task:{a['task_id']}") or {"id": f"task:{a['task_id']}"}),
|
||||
"task_entities": lambda: ss_loader.task_entities(a["doc"], a["task_id"]),
|
||||
"impact_frontier": lambda: ss_loader.impact_frontier(
|
||||
a["doc"], a["seeds"], max_level=a.get("max_level", 2),
|
||||
change_kind=a.get("change_kind")),
|
||||
"observed_impact": lambda: _ffi_mut_ret(lambda d: ss_loader.record_observed_impact(
|
||||
d, a["task_id"], a["items"], FIXED_NOW), a["doc"]),
|
||||
"predicted_vs_observed": lambda: ss_loader.predicted_vs_observed(
|
||||
a["predicted"], a["observed_doc"], a["seeds"]),
|
||||
"events": lambda: ss_loader.structural_events(a["doc"]),
|
||||
"diff": lambda: ss_loader.diff(a["old"], a["new"]),
|
||||
"dedupe": lambda: _ffi_mut(ss_loader.dedupe_observations, a["doc"]),
|
||||
"promote_fix": lambda: _ffi_mut(
|
||||
lambda d: ss_loader.promote(d, a["id"], a["state"], a["ev"], FIXED_NOW), a["doc"],
|
||||
ret_fn=lambda d: ss.entity(d, a["id"])),
|
||||
"resolve_status_fix": lambda: ss_loader.resolve_status(a["entity"]),
|
||||
"supersede_fix": lambda: _ffi_mut(
|
||||
lambda d: ss_loader.supersede(d, a["new"], a["old"], a["ev"], FIXED_NOW), a["doc"],
|
||||
ret_fn=lambda d: ss.entity(d, a["old"])),
|
||||
}[fn]()
|
||||
|
||||
|
||||
def _ret_entity(_updated_doc, d, cid):
|
||||
return ss.entity(d, cid)
|
||||
|
||||
|
||||
def _ffi_mut(call, doc, ret_fn=None):
|
||||
"""FFI mutation contract: the kernel returns the UPDATED doc; the
|
||||
oracle-shaped __ret__ is re-derived from it via ret_fn."""
|
||||
d = copy.deepcopy(doc)
|
||||
try:
|
||||
out = call(d)
|
||||
except RuntimeError as exc:
|
||||
return {"__err__": str(exc).split("core error: ", 1)[-1]}
|
||||
if isinstance(out, dict):
|
||||
d = out
|
||||
return {"__doc__": d, "__ret__": ret_fn(copy.deepcopy(d)) if ret_fn else None}
|
||||
|
||||
|
||||
def _ffi_mut_ret(call, doc):
|
||||
d = copy.deepcopy(doc)
|
||||
out = call(d)
|
||||
if isinstance(out.get("doc"), dict):
|
||||
d = out["doc"]
|
||||
return {"__doc__": d, "__ret__": out["result"]}
|
||||
|
||||
|
||||
def frozen():
|
||||
for fn, cases in TRUTH["groups"].items():
|
||||
for c in cases:
|
||||
a = copy.deepcopy(c["args"])
|
||||
if fn == "save" or fn == "snapshot":
|
||||
continue # byte-law layer below
|
||||
try:
|
||||
want = oracle_call(fn, a)
|
||||
except ValueError as exc:
|
||||
want = {"__err__": str(exc)}
|
||||
got_raw = None
|
||||
try:
|
||||
got = ffi_call(fn, a, None)
|
||||
except RuntimeError as exc:
|
||||
got = {"__err__": str(exc).split("core error: ", 1)[-1]}
|
||||
counts["frozen"] += 1
|
||||
if j(want) != j(got):
|
||||
fail("frozen", f"{fn}/{c['name']}", f"want={j(want)[:280]} got={j(got)[:280]}")
|
||||
|
||||
|
||||
def byte_law(tmp):
|
||||
"""save()/snapshot() must be byte-exact through the FFI."""
|
||||
save_docs = TRUTH["groups"]["save"]
|
||||
for c in save_docs:
|
||||
d = copy.deepcopy(c["args"]["doc"])
|
||||
name = c["name"]
|
||||
try:
|
||||
p = ss.save(d, tmp / f"py/{name}/store.json")
|
||||
want = {"content": Path(p).read_text(), "doc": d}
|
||||
except ValueError as exc:
|
||||
want = {"err": str(exc)}
|
||||
d2 = copy.deepcopy(c["args"]["doc"])
|
||||
try:
|
||||
out = ss_loader.save(d2, str(tmp / f"rs/{name}/store.json"))
|
||||
got = {"content": Path(out["path"]).read_text(), "doc": out["doc"]}
|
||||
except RuntimeError as exc:
|
||||
got = {"err": str(exc).split("core error: ", 1)[-1]}
|
||||
check("frozen", f"save-bytes/{name}", want, got)
|
||||
for c in TRUTH["groups"]["snapshot"]:
|
||||
a = copy.deepcopy(c["args"])
|
||||
d = a["doc"]
|
||||
p = ss.snapshot(d, tmp / f"py-snap/{c['name']}", a["label"], tag=a["tag"])
|
||||
want = {"path": Path(p).name, "content": Path(p).read_text(), "doc": d}
|
||||
d2 = copy.deepcopy(a["doc"])
|
||||
out = ss_loader.snapshot(d2, str(tmp / f"rs-snap/{c['name']}"), a["label"], a["tag"], FIXED_NOW)
|
||||
got = {"path": Path(out["path"]).name, "content": Path(out["path"]).read_text(), "doc": out["doc"]}
|
||||
check("frozen", f"snapshot-bytes/{c['name']}", want, got)
|
||||
|
||||
|
||||
# ── fuzz (seeded) ───────────────────────────────────────────────────────────
|
||||
|
||||
WORDS = ["m:a", "m:b", "m:c", "c:api", "task:T1", "é-unit", "x" * 40, ""]
|
||||
RELTYPES = ["depends_on", "implements", "consumes", "consumed_by", "verified_by",
|
||||
"obligated_canary", "deployed_as", "documented_by", "supersedes",
|
||||
"historical_relation", "related_memory", "belongs_to", "weird_rel"]
|
||||
SECRETS = ["token", "api_key", "password", "not_a_secret", "client_secret", "TOKEN"]
|
||||
|
||||
|
||||
def rand_value(rnd, depth=0):
|
||||
kind = rnd.randint(0, 6 if depth < 2 else 4)
|
||||
if kind == 0:
|
||||
return rnd.randint(-5, 10 ** 6)
|
||||
if kind == 1:
|
||||
return rnd.choice([0.5, 1.0, -2.25, 3.0, 1e-7, 1e21, 12345.6789])
|
||||
if kind == 2:
|
||||
return rnd.choice([True, False, None])
|
||||
if kind == 3:
|
||||
return "".join(rnd.choice(string.printable[:80] + "é☕φ") for _ in range(rnd.randint(0, 12)))
|
||||
if kind == 4:
|
||||
return rnd.choice(WORDS)
|
||||
if kind == 5:
|
||||
return [rand_value(rnd, depth + 1) for _ in range(rnd.randint(0, 4))]
|
||||
return {rnd.choice("abcdefé") + str(i): rand_value(rnd, depth + 1)
|
||||
for i in range(rnd.randint(0, 4))}
|
||||
|
||||
|
||||
def rand_doc(rnd):
|
||||
d = {"schema_version": 1, "generated_at": FIXED_NOW,
|
||||
"entities": [], "relationships": [], "observations": []}
|
||||
for i in range(rnd.randint(0, 5)):
|
||||
e = {"id": rnd.choice(WORDS) or f"m:e{i}", "kind": rnd.choice(["module", "contract", "widget"]),
|
||||
"maturity": rnd.choice(["DECLARED", "PRESENT", "WIRED", "REACHABLE", "EXERCISED", "LIVE_PROVEN"]),
|
||||
"evidence_refs": sorted({f"ev-{rnd.randint(0, 3)}" for _ in range(rnd.randint(0, 2))}),
|
||||
"observed_at": FIXED_NOW}
|
||||
if rnd.random() < 0.3:
|
||||
e["aliases"] = [f"m:old{i}"]
|
||||
if rnd.random() < 0.3:
|
||||
e["ext"] = {"conflicts": rand_value(rnd, 1)} if rnd.random() < 0.5 else {"n": i}
|
||||
if rnd.random() < 0.3:
|
||||
e["identities"] = {"p": rand_value(rnd, 1)}
|
||||
if rnd.random() < 0.2:
|
||||
e[rnd.choice(SECRETS)] = "s3cret"
|
||||
d["entities"].append(e)
|
||||
for _ in range(rnd.randint(0, 6)):
|
||||
rel = {"type": rnd.choice(RELTYPES), "from": rnd.choice(WORDS), "to": rnd.choice(WORDS),
|
||||
"evidence_refs": [f"ev-{rnd.randint(0, 2)}"] if rnd.random() < 0.8 else [],
|
||||
"observed_at": FIXED_NOW}
|
||||
if rnd.random() < 0.2:
|
||||
rel["status"] = rnd.choice(["STALE", "CANDIDATE", "CURRENT"])
|
||||
d["relationships"].append(rel)
|
||||
for _ in range(rnd.randint(0, 4)):
|
||||
o = {"kind": rnd.choice(["superseded", "k1"]), "x": rand_value(rnd, 1), "observed_at": FIXED_NOW}
|
||||
d["observations"].append(o)
|
||||
if rnd.random() < 0.4:
|
||||
d["authority_generations"] = {
|
||||
rnd.choice(["p1", "p2", "é-proj"]): {
|
||||
"generation": rnd.randint(0, 5),
|
||||
"observed": [f"{rnd.choice(RELTYPES)}|{rnd.choice(WORDS)}|{rnd.choice(WORDS)}"
|
||||
for _ in range(rnd.randint(0, 3))],
|
||||
**({"status": rnd.choice(["CURRENT", "DEGRADED", "UNKNOWN"])} if rnd.random() < 0.7 else {}),
|
||||
} for _ in range(rnd.randint(1, 3))}
|
||||
return d
|
||||
|
||||
|
||||
def fuzz():
|
||||
rnd = random.Random(2479)
|
||||
for i in range(300):
|
||||
d = rand_doc(rnd)
|
||||
d2 = copy.deepcopy(d)
|
||||
check("fuzz", f"sorted/{i}", ss._sorted(copy.deepcopy(d)), ss_loader.sorted_value(copy.deepcopy(d)))
|
||||
check("fuzz", f"redact/{i}", ss._redact_secrets(copy.deepcopy(d)), ss_loader.redact_secrets(copy.deepcopy(d)))
|
||||
check("fuzz", f"digest/{i}", ss.canonical_digest(copy.deepcopy(d)), ss_loader.canonical_digest(copy.deepcopy(d)))
|
||||
check("fuzz", f"health/{i}", ss.authority_health(copy.deepcopy(d)), ss_loader.authority_health(copy.deepcopy(d)))
|
||||
check("fuzz", f"events/{i}", ss.structural_events(copy.deepcopy(d)), ss_loader.structural_events(copy.deepcopy(d)))
|
||||
d3 = copy.deepcopy(d)
|
||||
ss.dedupe_observations(d3)
|
||||
d4 = copy.deepcopy(d)
|
||||
d4 = ss_loader.dedupe_observations(d4)
|
||||
check("fuzz", f"dedupe/{i}", d3, d4)
|
||||
seed = rnd.choice(WORDS) or "m:a"
|
||||
ck = rnd.choice([None, "contract_change", "implementation_change", "contract_violation"])
|
||||
check("fuzz", f"frontier/{i}",
|
||||
ss.impact_frontier(copy.deepcopy(d), [seed], change_kind=ck),
|
||||
ss_loader.impact_frontier(copy.deepcopy(d), [seed], change_kind=ck))
|
||||
check("fuzz", f"diff/{i}", ss.diff(copy.deepcopy(d), d2), ss_loader.diff(copy.deepcopy(d), copy.deepcopy(d2)))
|
||||
ent = rnd.choice(d["entities"]) if d["entities"] else {}
|
||||
check("fuzz", f"resolve_status/{i}", ss.resolve_status(copy.deepcopy(ent)),
|
||||
ss_loader.resolve_status(copy.deepcopy(ent)))
|
||||
# mutation kernels
|
||||
want = _mut(lambda x: ss.bind_task(x, "T1", ["m:a", "c:api"], "ev"), copy.deepcopy(d))
|
||||
got = _ffi_mut(lambda x: ss_loader.bind_task(x, "T1", ["m:a", "c:api"], "ev", FIXED_NOW),
|
||||
copy.deepcopy(d),
|
||||
ret_fn=lambda x: ss.entity(x, "task:T1") or {"id": "task:T1"})
|
||||
check("fuzz", f"bind_task/{i}", want, got)
|
||||
want = _mut(lambda x: ss.alias(x, "m:b", "m:b-old"), copy.deepcopy(d)) if any(
|
||||
e["id"] == "m:b" for e in d["entities"]) else {"__err__": "alias target not found: m:b"}
|
||||
got = _ffi_mut(lambda x: ss_loader.alias(x, "m:b", "m:b-old", FIXED_NOW), copy.deepcopy(d),
|
||||
ret_fn=lambda x: ss.entity(x, "m:b"))
|
||||
check("fuzz", f"alias/{i}", want, got)
|
||||
want = _mut(lambda x: ss.promote(x, "m:a", "WIRED", "ev-f"), copy.deepcopy(d))
|
||||
got = _ffi_mut(lambda x: ss_loader.promote(x, "m:a", "WIRED", "ev-f", FIXED_NOW), copy.deepcopy(d),
|
||||
ret_fn=lambda x: ss.entity(x, "m:a"))
|
||||
check("fuzz", f"promote/{i}", want, got)
|
||||
want = _mut(lambda x: ss.supersede(x, "m:b", "m:a", "ev-s"), copy.deepcopy(d))
|
||||
got = _ffi_mut(lambda x: ss_loader.supersede(x, "m:b", "m:a", "ev-s", FIXED_NOW), copy.deepcopy(d),
|
||||
ret_fn=lambda x: ss.entity(x, "m:a"))
|
||||
check("fuzz", f"supersede/{i}", want, got)
|
||||
items = [{"entity": rnd.choice(WORDS), "source": "t", "detail": rand_value(rnd, 1)} for _ in range(3)]
|
||||
want = _mut_ret(lambda x: ss.record_observed_impact(x, "T1", copy.deepcopy(items)), copy.deepcopy(d))
|
||||
got = _ffi_mut_ret(lambda x: ss_loader.record_observed_impact(x, "T1", copy.deepcopy(items), FIXED_NOW), copy.deepcopy(d))
|
||||
check("fuzz", f"observed_impact/{i}", want, got)
|
||||
|
||||
|
||||
def save_bytes_fuzz(tmp):
|
||||
rnd = random.Random(4242)
|
||||
for i in range(50):
|
||||
d = rand_doc(rnd)
|
||||
p1 = ss.save(copy.deepcopy(d), tmp / f"fz-py/{i}.json")
|
||||
out = ss_loader.save(copy.deepcopy(d), str(tmp / f"fz-rs/{i}.json"))
|
||||
b1 = Path(p1).read_bytes()
|
||||
b2 = Path(out["path"]).read_bytes()
|
||||
counts["fuzz"] += 1
|
||||
if b1 != b2:
|
||||
fail("fuzz", f"save-bytes/{i}", f"py={b1[:160]!r} rs={b2[:160]!r}")
|
||||
check("fuzz", f"digest-bytes/{i}", ss.canonical_digest(copy.deepcopy(d)),
|
||||
ss_loader.canonical_digest(copy.deepcopy(d)))
|
||||
|
||||
|
||||
# ── live dual-mode facade battery ───────────────────────────────────────────
|
||||
|
||||
def facade_battery():
|
||||
def run(tag):
|
||||
import subprocess
|
||||
code = r'''
|
||||
import copy, json, os, sys, tempfile, calendar, time as _time, types
|
||||
sys.path.insert(0, "src")
|
||||
from rune import structural_store as ss
|
||||
FIXED = "2026-10-13T01:02:03Z"
|
||||
ss.now = lambda: FIXED
|
||||
_EPOCH = calendar.timegm(_time.strptime(FIXED, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
ss.time = types.SimpleNamespace( # freeze snapshot()'s wall-clock stamp
|
||||
gmtime=lambda: _time.gmtime(_EPOCH),
|
||||
strftime=lambda fmt, t=None: _time.strftime(fmt, t if t is not None else _time.gmtime()),
|
||||
)
|
||||
out = {}
|
||||
d = ss.new_doc()
|
||||
ss.add_entity(d, id="m:a", kind="module")
|
||||
ss.add_entity(d, id="m:b", kind="module")
|
||||
ss.add_entity(d, id="c:api", kind="contract")
|
||||
ss.add_relationship(d, type="implements", from_id="m:a", to_id="c:api", evidence_refs=["ev"])
|
||||
ss.add_relationship(d, type="depends_on", from_id="m:b", to_id="m:a", evidence_refs=["ev"])
|
||||
ss.alias(d, "m:b", "m:b-old")
|
||||
ss.promote(d, "m:a", "WIRED", "ev-2")
|
||||
ss.bind_task(d, "T1", ["m:a", "m:b"], "ev-t")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
p = ss.save(d, td + "/s.json")
|
||||
out["save_bytes_sha"] = __import__("hashlib").sha256(open(p, "rb").read()).hexdigest()
|
||||
out["snapshot_name"] = ss.snapshot(d, td + "/sn", "l1", tag="cafe1234").name
|
||||
out["digest"] = ss.canonical_digest(d)
|
||||
out["entities"] = sorted(e["id"] for e in d["entities"])
|
||||
out["via_alias"] = ss.entity(d, "m:b-old")["maturity"]
|
||||
out["health"] = ss.authority_health(d)
|
||||
out["frontier"] = ss.impact_frontier(d, ["c:api"], change_kind=None)
|
||||
out["frontier_impl"] = ss.impact_frontier(d, ["m:a"], change_kind=None)
|
||||
out["events_n"] = len(ss.structural_events(d))
|
||||
out["diff"] = ss.diff(d, copy.deepcopy(d))
|
||||
out["task_entities"] = ss.task_entities(d, "T1")
|
||||
ss.dedupe_observations(d)
|
||||
out["obs_n"] = len(d["observations"])
|
||||
r = ss.record_observed_impact(d, "T2", [{"entity": "m:a", "detail": "x"}])
|
||||
out["observed"] = r
|
||||
out["pvo"] = ss.predicted_vs_observed(out["frontier"], d, ["c:api"])
|
||||
print(json.dumps(out, sort_keys=True))
|
||||
'''
|
||||
env = dict(os.environ)
|
||||
if tag == "python":
|
||||
env["RUNE_STRUCTURAL_CORE"] = "python"
|
||||
else:
|
||||
env.pop("RUNE_STRUCTURAL_CORE", None)
|
||||
r = subprocess.run([sys.executable, "-c", code], capture_output=True,
|
||||
text=True, cwd=str(REPO), env=env, timeout=120)
|
||||
if r.returncode != 0:
|
||||
fail("live", tag, r.stderr[-300:])
|
||||
return None
|
||||
return json.loads(r.stdout)
|
||||
|
||||
py = run("python")
|
||||
rs = run("rust")
|
||||
if py is None or rs is None:
|
||||
return
|
||||
check("live", "dual-mode-facade", py, rs)
|
||||
|
||||
|
||||
def main():
|
||||
if not ss_loader.available():
|
||||
print("Rust core not built — run cargo build --release in src/rust/structural_store")
|
||||
return 2
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
frozen()
|
||||
byte_law(Path(td))
|
||||
fuzz()
|
||||
save_bytes_fuzz(Path(td))
|
||||
facade_battery()
|
||||
total = sum(counts.values())
|
||||
print(f"\nPARITY: {total} checks, {len(fails)} failures "
|
||||
f"(frozen={counts['frozen']} fuzz={counts['fuzz']} live={counts['live']})")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave construction contract for night_mode
|
||||
with SHA evidence."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
os.environ["CS_RUST_LIB"] = "/mnt/workdrive/rune/src/rust/capability_scoring/target/release/libcapability_scoring.so"
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
cid = sys.argv[1]
|
||||
iface = ConstructionInterface(builder="factory-gap-nm")
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "night_mode dichotomy completion — parity 150f5b86 "
|
||||
"feat(night_mode): close PARTIAL gaps — dichotomy wave; "
|
||||
"Phase B (trunc ASCII zero-alloc fast path, rune walk "
|
||||
"only for long/non-ASCII) pushed gitea/master. Go port at "
|
||||
"src/go/night-mode closes all 12 audit-named gaps "
|
||||
"name-preserving (_ctx_budget, get_night_mode, "
|
||||
"start/stop_night_mode, get_status, get_report, to_dict, "
|
||||
"_load, _save, _run_loop, _run_sleep, _run_evaluation + "
|
||||
"_run_iteration/_generate_report); pure/IO split law with "
|
||||
"injected clock/wait/script/HTTP/rand/loop seams. Battery "
|
||||
"10 tests replay the 9-case frozen oracle truth VERBATIM "
|
||||
"(stop guard, idle status/report, 6 _ctx_budget branches); "
|
||||
"live parity 100% (status/report/stop x 3 scenarios + CLI "
|
||||
"round-trip); pytest 37/37 both ways (RUNE_NIGHT_MODE_CORE "
|
||||
"auto vs =python); gofmt+vet clean. Zero regressions "
|
||||
"(failure set byte-identical before/after).",
|
||||
}], scope_files=[
|
||||
"src/rune/night_mode.py",
|
||||
"src/go/night-mode/main.go",
|
||||
"src/go/night-mode/night_mode_test.go",
|
||||
"src/go/night-mode/frozen_truth.json",
|
||||
"scripts/gen_night_mode_truth.py",
|
||||
"scripts/check_night_mode_parity.py",
|
||||
"tests/test_night_mode.py",
|
||||
"scripts/open_contract_night_mode.py",
|
||||
"scripts/close_contract_night_mode.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:300])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave construction contract for the
|
||||
provider_budget gap-closing port with SHA evidence. Full lifecycle
|
||||
draft→approved→building→verifying→closed in ONE process."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
PARITY_SHA = "8a08f9b4"
|
||||
PHASE_B_SHA = "527dc774"
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-pb")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Close the provider_budget dichotomy gaps (#2479 tranche, GO side): "
|
||||
"port the 10 audit-missing functions (get_tracker/track_usage/"
|
||||
"check_provider/get_best_provider/failover/get_status/to_dict/_load/"
|
||||
"_save/_init_default_providers) name-preserving into the existing "
|
||||
"src/go/provider-budget Go port — tracker with order-preserving "
|
||||
"storage load (dict-order tie-break parity), config seeding (ollama "
|
||||
"skip + anthropic type detection), per-type health-check endpoints "
|
||||
"with IO behind injectable seams (clock, config path, transport), "
|
||||
"api_key-excluding to_dict, module-level singleton wrappers. Truth "
|
||||
"frozen via the shared freeze_truth.py harness (RUNE_PROVIDER_BUDGET"
|
||||
"_CORE=python, fixture-home replay contract); VERBATIM-embedded Go "
|
||||
"battery; 200-op live parity seed 2479; Python port test (oracle "
|
||||
"pinned + harness gate); audit 5/20 → 20/20.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:200])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting")
|
||||
sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:120])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "provider_budget gap-closing completion — parity "
|
||||
f"{PARITY_SHA} feat(provider_budget): close PARTIAL gaps — "
|
||||
"dichotomy wave; Phase B (single generic decodeOrderedTable[T] "
|
||||
f"streaming decoder replacing both hand-rolled ordered-JSON "
|
||||
f"decoders) {PHASE_B_SHA} refactor(provider_budget): Phase B "
|
||||
"language-advantage pass — dichotomy wave; both pushed "
|
||||
"gitea/master. Go battery replays the 13-case frozen oracle "
|
||||
"truth VERBATIM (truth_frozen.json from the shared "
|
||||
"freeze_truth.py harness, RUNE_PROVIDER_BUDGET_CORE=python, "
|
||||
"fixture-home replay protocol) — harness gate reports "
|
||||
"byte-identical; live parity 200/200 random-op seed 2479 "
|
||||
"python-vs-Go identical; pytest 3/3 (oracle pinned, frozen "
|
||||
"replay, harness gate, go test); go vet + gofmt clean; "
|
||||
"existing replay gate 9/9 green; dichotomy audit 5/20 "
|
||||
"PARTIAL → 20/20 FULL, missing [].",
|
||||
}], scope_files=[
|
||||
"src/rune/provider_budget.py",
|
||||
"src/go/provider-budget/main.go",
|
||||
"src/go/provider-budget/truth_frozen.json",
|
||||
"src/go/provider-budget/truth_frozen_test.go",
|
||||
"scripts/gen_provider_budget_truth.py",
|
||||
"scripts/parity_provider_budget.py",
|
||||
"scripts/walk_contract_provider_budget.py",
|
||||
"scripts/close_contract_provider_budget.py",
|
||||
"tests/test_provider_budget_port.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:200])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
sys.exit(0 if c.get("success") else 1)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave construction contract for self_model
|
||||
with SHA evidence."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
cid = "BUILDER-2026-10-13T00:23:43Z-53857a-factory-batch-i"
|
||||
iface = ConstructionInterface(builder="factory-batch-i")
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "self_model dichotomy completion — parity dac4889c "
|
||||
"feat(self_model): port to FULL Rust — dichotomy wave; "
|
||||
"Phase B 99bde7ba refactor(self_model): Phase B "
|
||||
"language-advantage pass — dichotomy wave (ASCII byte "
|
||||
"fast-path token estimate); store register e1904eb1 (all "
|
||||
"pushed gitea/master). Battery 2 tests replay the 26-case "
|
||||
"frozen oracle truth VERBATIM (estimate_tokens boundaries "
|
||||
"incl. unicode; compile incl. exact-fit, evidence overflow, "
|
||||
"key defaults, stable weight ties, zero/negative budgets; "
|
||||
"why_question incl. statement/evidence matches, "
|
||||
"case-insensitivity, empty topic/model); live parity "
|
||||
"926/926 (26 frozen + 300 fuzz seeds x 3 fns, kernel vs "
|
||||
"RUNE_SMM_CORE=python); pytest test_p2_quintet 16 passed "
|
||||
"BOTH ways (kernel auto + python pin); clippy -D warnings "
|
||||
"clean. Documented FFI boundary: pyjson numbers are f64 "
|
||||
"(int-vs-float weight distinction crosses as value-equal; "
|
||||
"truth corpus uses float weights).",
|
||||
}], scope_files=[
|
||||
"src/rune/self_model.py",
|
||||
"src/rune/smm_loader.py",
|
||||
"src/rust/self_model/src/lib.rs",
|
||||
"src/rust/self_model/src/branch_battery.rs",
|
||||
"src/rust/self_model/frozen_sm_truth.json",
|
||||
"scripts/gen_self_model_truth.py",
|
||||
"scripts/walk_contract_self_model.py",
|
||||
"scripts/close_contract_self_model.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:300])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -47,7 +47,7 @@ elif [ "$LANG_" = "go" ]; then
|
||||
fi
|
||||
|
||||
# Layer 3: truth file exists and is consumed
|
||||
TRUTH=$(find . \( -name "frozen_*_truth.json" -o -name "frozen_truth.json" -o -name "truth.json" -o -name "truth_*.json" \) 2>/dev/null | head -1)
|
||||
TRUTH=$(find . \( -name "frozen_*_truth.json" -o -name "frozen_truth.json" -o -name "frozen_*_truth.json" -o -name "truth.json" -o -name "truth_*.json" \) 2>/dev/null | head -1)
|
||||
if [ -n "$TRUTH" ]; then
|
||||
ROWS=$(python3 -c "import json; d=json.load(open('$TRUTH')); print(len(d) if isinstance(d,list) else sum(len(v) if isinstance(v,list) else 0 for v in d.values()))" 2>/dev/null)
|
||||
check "PASS" "truth file: $TRUTH ($ROWS rows)"
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the oracle truth for the GO gap-close port of
|
||||
src/rune/context_broker.py (dichotomy wave #2479, GO, factory-gap-ctxbroker).
|
||||
|
||||
Every NEW function ported by the gap-close, every branch, against the LIVE
|
||||
oracle pinned to the Python cores (RUNE_COGNITION_CORE=python — the _py_*
|
||||
oracles ARE the parity floor being frozen). Deep-copied inputs: the oracle
|
||||
mutates message lists in place (compact_history archive path), so each row
|
||||
re-freezes the input verbatim.
|
||||
|
||||
Ops frozen (fn = the Go-side dispatcher key):
|
||||
_py_default_estimator est (text)
|
||||
_cc_core cc_core (env RUNE_COGNITION_CORE) -> bool available
|
||||
_py_default_budget budget (window)
|
||||
_py_default_memory_budget membud (window, incl. None/str/bool coercion)
|
||||
_py_memory_disabled_value memoff (raw string)
|
||||
reset_discovered_windows / register_window / default_window
|
||||
windows (sequence of ops -> final registry view)
|
||||
summarize_history summarize (transport-failure branch: ValueError)
|
||||
compact_history compact (messages, window, summarizer, session)
|
||||
rolling_summary_state rstate (key -> state | null)
|
||||
_through_id through (messages, through_count)
|
||||
store_rolling_summary / evict_rolling rstore (op sequence -> state + return)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python" # pin the Python parity floor
|
||||
|
||||
from rune import context_broker as cb # noqa: E402
|
||||
|
||||
rows: list[dict] = []
|
||||
|
||||
|
||||
def row(name, fn, inp, out):
|
||||
rows.append({"name": name, "fn": fn, "input": inp, "want": out})
|
||||
|
||||
|
||||
# ── _py_default_estimator: every branch ────────────────────────────────────
|
||||
row("est:empty", "est", {"text": ""}, cb._py_default_estimator(""))
|
||||
row("est:none-coerced-empty", "est", {"text": None}, cb._py_default_estimator(None))
|
||||
row("est:short", "est", {"text": "hello"}, cb._py_default_estimator("hello"))
|
||||
row("est:exact-8", "est", {"text": "12345678"}, cb._py_default_estimator("12345678"))
|
||||
row("est:9-floor", "est", {"text": "123456789"}, cb._py_default_estimator("123456789"))
|
||||
row("est:unicode-rune-len", "est", {"text": "héllo wörld"}, cb._py_default_estimator("héllo wörld"))
|
||||
|
||||
# ── _cc_core: python-pin + auto-without-build ───────────────────────────────
|
||||
from rune import cc_loader # noqa: E402
|
||||
_LOADER_AVAIL = bool(cc_loader.available())
|
||||
row("cc_core:python-pin-returns-none", "cc_core",
|
||||
{"env": {"RUNE_COGNITION_CORE": "python"}, "loader_available": _LOADER_AVAIL},
|
||||
None if cb._cc_core() is None else "CORE")
|
||||
del os.environ["RUNE_COGNITION_CORE"]
|
||||
row("cc_core:auto-fallback", "cc_core", {"env": {}, "loader_available": _LOADER_AVAIL},
|
||||
None if cb._cc_core() is None else "CORE")
|
||||
|
||||
# ── _py_default_budget: every branch ────────────────────────────────────────
|
||||
row("budget:0-floor", "budget", {"window": 0}, cb._py_default_budget(0))
|
||||
row("budget:1-floor", "budget", {"window": 1}, cb._py_default_budget(1))
|
||||
row("budget:100-floor", "budget", {"window": 100}, cb._py_default_budget(100))
|
||||
row("budget:4096", "budget", {"window": 4096}, cb._py_default_budget(4096))
|
||||
row("budget:8192", "budget", {"window": 8192}, cb._py_default_budget(8192))
|
||||
row("budget:131072", "budget", {"window": 131072}, cb._py_default_budget(131072))
|
||||
row("budget:quarter-exact-8192", "budget", {"window": 8192}, cb._py_default_budget(8192))
|
||||
|
||||
# ── _py_default_memory_budget: every branch incl. coercion ─────────────────
|
||||
row("membud:0-cap", "membud", {"window": 0}, cb._py_default_memory_budget(0))
|
||||
row("membud:none-cap", "membud", {"window": None}, cb._py_default_memory_budget(None))
|
||||
row("membud:neg-cap", "membud", {"window": -5}, cb._py_default_memory_budget(-5))
|
||||
row("membud:8-eq-1", "membud", {"window": 8}, cb._py_default_memory_budget(8))
|
||||
row("membud:2400", "membud", {"window": 2400}, cb._py_default_memory_budget(2400))
|
||||
row("membud:9600-cap", "membud", {"window": 9600}, cb._py_default_memory_budget(9600))
|
||||
row("membud:96000-cap", "membud", {"window": 96000}, cb._py_default_memory_budget(96000))
|
||||
|
||||
# bool/float coerce through the public fn's int(window) path when a Rust core
|
||||
# is present; on the Python floor they hit _py_default_memory_budget raw.
|
||||
row("membud:bool-true", "membud", {"window": True}, cb._py_default_memory_budget(True))
|
||||
row("membud:float", "membud", {"window": 2400.9}, cb._py_default_memory_budget(2400.9))
|
||||
|
||||
# ── _py_memory_disabled_value: every branch ────────────────────────────────
|
||||
for v in ("", "0", "false", "no", "off", "1", "true", "yes", "on", "OFF",
|
||||
" False ", "anything", " ", "off\n"):
|
||||
row(f"memoff:{v!r}", "memoff", {"value": v}, cb._py_memory_disabled_value(v))
|
||||
|
||||
# ── window registry: reset/register/default with NO live discovery ─────────
|
||||
# default_window's live-discovery import is exercised in the fallback branch
|
||||
# only when model_id is falsy (registry miss + import of local_tool_runtime
|
||||
# would attempt a real HTTP call otherwise — the Go seam stubs discovery;
|
||||
# here we freeze: falsy id, registry hit, registry miss w/ capped discovery).
|
||||
cb.reset_discovered_windows()
|
||||
row("windows:reset-then-missing-none", "windows",
|
||||
{"ops": [{"op": "default_window", "model_id": None}]},
|
||||
{"last": cb.default_window(None)})
|
||||
row("windows:none-empty-str", "windows",
|
||||
{"ops": [{"op": "default_window", "model_id": ""}]},
|
||||
{"last": cb.default_window("")})
|
||||
row("windows:register-hit", "windows",
|
||||
{"ops": [{"op": "register_window", "model_id": "m1", "window": 32768},
|
||||
{"op": "default_window", "model_id": "m1"}]},
|
||||
{"last": (cb.reset_discovered_windows(), cb.register_window("m1", 32768),
|
||||
cb.default_window("m1"))[2]})
|
||||
row("windows:register-overwrite", "windows",
|
||||
{"ops": [{"op": "register_window", "model_id": "m1", "window": 8192},
|
||||
{"op": "register_window", "model_id": "m1", "window": 65536},
|
||||
{"op": "default_window", "model_id": "m1"}]},
|
||||
{"last": (cb.reset_discovered_windows(), cb.register_window("m1", 8192),
|
||||
cb.register_window("m1", 65536), cb.default_window("m1"))[3]})
|
||||
row("windows:reset-clears", "windows",
|
||||
{"ops": [{"op": "register_window", "model_id": "m1", "window": 8192},
|
||||
{"op": "reset_discovered_windows"},
|
||||
{"op": "default_window", "model_id": "m1"}]},
|
||||
{"last": (cb.reset_discovered_windows(), cb.register_window("m1", 8192),
|
||||
cb.reset_discovered_windows(), cb.default_window("m1"))[3]})
|
||||
row("windows:register-str-window-coerced", "windows",
|
||||
{"ops": [{"op": "register_window", "model_id": "m1", "window": "28672"},
|
||||
{"op": "default_window", "model_id": "m1"}]},
|
||||
{"last": (cb.reset_discovered_windows(), cb.register_window("m1", "28672"),
|
||||
cb.default_window("m1"))[2]})
|
||||
# discovery-failure branch: unknown model -> the live oracle tries the
|
||||
# transport; in this offline env that raises inside context_length ->
|
||||
# record_failure + fallback 8192 (never cached). Freeze the returned value.
|
||||
cb.reset_discovered_windows()
|
||||
row("windows:miss-discover-fallback-8192", "windows",
|
||||
{"ops": [{"op": "default_window", "model_id": "definitely-not-a-model:offline"}]},
|
||||
{"last": cb.default_window("definitely-not-a-model:offline"),
|
||||
"note": "live oracle: T1 context_discovery + FALLBACK_WINDOW, not cached"})
|
||||
|
||||
# ── summarize_history: transport-failure branch (offline) ──────────────────
|
||||
try:
|
||||
cb.summarize_history([{"role": "user", "content": "hi"}], model="offline-no-such-model")
|
||||
row("summarize:unexpected-success", "summarize", {"messages": []}, "UNEXPECTED-SUCCESS")
|
||||
except ValueError as exc:
|
||||
row("summarize:transport-fail-valueerror", "summarize",
|
||||
{"messages": [{"role": "user", "content": "hi"}]},
|
||||
{"error": "ValueError", "detail": str(exc)[:120]})
|
||||
except Exception as exc: # noqa: BLE001 — freeze whatever the live env does
|
||||
row("summarize:transport-fail-other", "summarize",
|
||||
{"messages": [{"role": "user", "content": "hi"}]},
|
||||
{"error": type(exc).__name__, "detail": str(exc)[:120]})
|
||||
|
||||
# ── _through_id: every branch ───────────────────────────────────────────────
|
||||
msgs = [{"role": "user", "content": "first"},
|
||||
{"role": "assistant", "content": "reply"},
|
||||
{"role": "user", "content": "second"}]
|
||||
row("through:empty-list", "through", {"messages": [], "through": 0}, cb._through_id([], 0))
|
||||
row("through:neg", "through", {"messages": msgs, "through": -1}, cb._through_id(msgs, -1))
|
||||
row("through:at-end", "through", {"messages": msgs, "through": 3}, cb._through_id(msgs, 3))
|
||||
row("through:beyond-end", "through", {"messages": msgs, "through": 99}, cb._through_id(msgs, 99))
|
||||
row("through:anchor-0", "through", {"messages": msgs, "through": 0}, cb._through_id(msgs, 0))
|
||||
row("through:anchor-1", "through", {"messages": msgs, "through": 1}, cb._through_id(msgs, 1))
|
||||
row("through:content-hash-stable", "through",
|
||||
{"messages": msgs, "through": 2},
|
||||
(cb._through_id(msgs, 2), cb._through_id(copy.deepcopy(msgs), 2),
|
||||
cb._through_id([{"content": "second", "role": "user"}], 0))[0],
|
||||
)
|
||||
# key-order independence (sort_keys=True)
|
||||
row("through:key-order-independent", "through",
|
||||
{"messages": [{"role": "user", "content": "x"}, {"content": "x", "role": "user"}],
|
||||
"through": 1},
|
||||
cb._through_id([{"role": "user", "content": "x"}, {"content": "x", "role": "user"}], 1))
|
||||
|
||||
# ── rolling summary store: state/store/evict (self-contained rows) ──────────
|
||||
cb._ROLLING.clear()
|
||||
row("rstate:miss", "rstate", {"ops": [{"op": "read", "source": "operator", "sid": "s1"}]},
|
||||
cb.rolling_summary_state(("operator", "s1")))
|
||||
cb.store_rolling_summary(("operator", "s1"), "SUM", 7, "abcdef0123456789")
|
||||
row("rstate:hit-copy", "rstate",
|
||||
{"ops": [{"op": "store", "source": "operator", "sid": "s1", "summary": "SUM",
|
||||
"through": 7, "tid": "abcdef0123456789"},
|
||||
{"op": "read", "source": "operator", "sid": "s1"}]},
|
||||
cb.rolling_summary_state(("operator", "s1")))
|
||||
row("rstore:store-then-through-id", "rstore",
|
||||
{"msgs": msgs},
|
||||
(cb.store_rolling_summary(("operator", "s2"), "S2", 1, cb._through_id(msgs, 1)),
|
||||
cb.rolling_summary_state(("operator", "s2")))[1])
|
||||
|
||||
# ── compact_history: every branch (injected summarizer = deterministic) ────
|
||||
def fresh(seed: str, n_pairs: int) -> list[dict]:
|
||||
out = []
|
||||
for i in range(n_pairs):
|
||||
out.append({"role": "user", "content": f"{seed} user {i} " + "x" * (i * 40)})
|
||||
out.append({"role": "assistant", "content": f"{seed} assistant {i} " + "y" * (i * 40)})
|
||||
return out
|
||||
|
||||
|
||||
def sumr(messages):
|
||||
return "SUMMARY<" + "|".join(m.get("content", "")[:10] for m in messages) + ">"
|
||||
|
||||
|
||||
def compact_row(name, messages, window, *, summarizer="sum", session="", prior=None,
|
||||
archive=False):
|
||||
"""Freeze one compact_history call; self-contained (prior state inline)."""
|
||||
cb._ROLLING.clear()
|
||||
if prior:
|
||||
cb.store_rolling_summary(("operator", session) if session else ("operator",),
|
||||
prior["summary"], prior["through_count"],
|
||||
prior.get("through_id", ""))
|
||||
kw = dict(summarizer=sumr if summarizer != "boom" else boom)
|
||||
arch = None
|
||||
if archive:
|
||||
# deterministic RELATIVE fixture path (chdir'd to repo root): the
|
||||
# label is embedded in the summary block, so it must be portable
|
||||
os.chdir(ROOT)
|
||||
arch = Path("src/go/context-broker/.fixtures/archive.jsonl")
|
||||
arch.parent.mkdir(parents=True, exist_ok=True)
|
||||
inp = {"messages": copy.deepcopy(messages), "window": window,
|
||||
"summarizer": summarizer}
|
||||
if session:
|
||||
inp["session"] = session
|
||||
if prior:
|
||||
inp["prior"] = prior
|
||||
if archive:
|
||||
inp["archive"] = "FIXTURE"
|
||||
inp["archive_path"] = str(arch)
|
||||
compacted, ev = cb.compact_history(copy.deepcopy(messages), window,
|
||||
archive_path=arch,
|
||||
summarizer=sumr if summarizer != "boom" else boom,
|
||||
session_id=session)
|
||||
out = {"event": ev}
|
||||
if messages is not None:
|
||||
out["compacted"] = compacted
|
||||
if archive:
|
||||
body = arch.read_text() if arch.exists() else ""
|
||||
out["archive_lines"] = body.count("\n")
|
||||
out["archived_msg_count"] = len(json.loads(body.splitlines()[0])["messages"]) if body else 0
|
||||
out["archived_msgs"] = json.loads(body.splitlines()[0])["messages"] if body else []
|
||||
arch.unlink(missing_ok=True)
|
||||
row(name, "compact", inp, out)
|
||||
|
||||
|
||||
def boom(messages):
|
||||
raise RuntimeError("summarizer down")
|
||||
|
||||
|
||||
# no messages -> early return, event untriggered
|
||||
compact_row("compact:empty-messages", [], 8192)
|
||||
|
||||
# <= KEEP_TURNS user turns -> early return
|
||||
compact_row("compact:at-keep-turns-no-trigger", fresh("a", 5), 8192)
|
||||
compact_row("compact:below-keep-turns-no-trigger", fresh("a", 4), 8192)
|
||||
|
||||
# > keep turns, no session, summarizer success
|
||||
compact_row("compact:trigger-no-session", fresh("b", 7), 8192)
|
||||
|
||||
# with session: rolling persistence across TWO compactions (C1: only new)
|
||||
cb._ROLLING.clear()
|
||||
m3 = fresh("c", 8)
|
||||
c1, ev1 = cb.compact_history(copy.deepcopy(m3), 8192, summarizer=sumr,
|
||||
session_id="s3")
|
||||
prior3 = cb.rolling_summary_state(("operator", "s3"))
|
||||
row("compact:session-first", "compact",
|
||||
{"messages": m3, "window": 8192, "session": "s3", "summarizer": "sum"},
|
||||
{"event": ev1, "compacted": c1})
|
||||
# second compaction: window rotated under the stored count (#141 rematch)
|
||||
m3b = m3[:2] + m3[-10:]
|
||||
compact_row("compact:session-rotation-rematch", m3b, 8192, session="s3",
|
||||
prior=prior3)
|
||||
# prior through beyond len(older) — history shrank, re-summarize from scratch
|
||||
compact_row("compact:session-through-beyond-shrink", fresh("d", 7), 8192,
|
||||
session="s4", prior={"summary": "PRIOR-SUM", "through_count": 999,
|
||||
"through_id": ""})
|
||||
# nothing new beyond the rolled summary: reuse it verbatim
|
||||
cb._ROLLING.clear()
|
||||
m9 = fresh("g", 7)
|
||||
_, ev9_pre = cb.compact_history(copy.deepcopy(m9), 8192, summarizer=sumr,
|
||||
session_id="s9")
|
||||
prior9 = cb.rolling_summary_state(("operator", "s9"))
|
||||
# call again with IDENTICAL messages: older[6:] is empty -> reuse verbatim
|
||||
compact_row("compact:session-nothing-new-reuse", m9, 8192, session="s9",
|
||||
prior=prior9)
|
||||
|
||||
# summarizer failure -> C2 degrade to tail
|
||||
compact_row("compact:summarizer-fail-degrade-tail", fresh("e", 7), 8192,
|
||||
summarizer="boom")
|
||||
|
||||
# summarizer failure with tiny window (tail budget floor: always keep >=1)
|
||||
m6 = [{"role": "user", "content": "u" * 9000}, {"role": "assistant", "content": "a" * 9000},
|
||||
{"role": "user", "content": "u2" * 9000}, {"role": "assistant", "content": "a2" * 9000},
|
||||
{"role": "user", "content": "u3" * 9000}, {"role": "assistant", "content": "a3" * 9000},
|
||||
{"role": "user", "content": "u4" * 9000}]
|
||||
compact_row("compact:fail-tiny-window-tail-first-only", m6, 10, summarizer="boom")
|
||||
|
||||
# archive write: success path — event.archived True, label = the path itself
|
||||
compact_row("compact:archive-success", fresh("f", 6), 8192, session="s7",
|
||||
archive=True)
|
||||
|
||||
# default_estimator used inside event tokens (python pin): tokens_before math
|
||||
compact_row("compact:token-math-estimator",
|
||||
[{"role": "user", "content": "12345678"}, {"role": "assistant", "content": "1234"}],
|
||||
8192)
|
||||
|
||||
# ── ContextBroker kernel: to_dict / collect / admit / render ────────────────
|
||||
from rune.context_broker import ContributionBlock, ContextBroker, ContextOverflowError # noqa: E402
|
||||
|
||||
# to_dict
|
||||
blk = ContributionBlock("memory", 5, "hello world", 2, "recall", evictable=True)
|
||||
row("to_dict:block", "to_dict", {}, blk.to_dict())
|
||||
blk2 = ContributionBlock("system", 100, "sys", 0, "persona", evictable=False)
|
||||
row("to_dict:mandatory", "to_dict", {}, blk2.to_dict())
|
||||
|
||||
# collect: happy, empty-content skip, failure → T1 degrade, malformed → T1
|
||||
def make_broker():
|
||||
b = ContextBroker()
|
||||
b.register_contributor("ok", lambda task, session: ContributionBlock("tools", 9, "tooltext", 2, "t"), 1)
|
||||
b.register_contributor("empty", lambda task, session: ContributionBlock("x", 1, "", 0, "s"), 1)
|
||||
b.register_contributor("boom", lambda task, session: (_ for _ in ()).throw(RuntimeError("cb down")), 1)
|
||||
b.register_contributor("malformed", lambda task, session: ContributionBlock("y", 1, 12345, 0, "s"), 1)
|
||||
b.register_contributor("list-ok", lambda task, session: ContributionBlock("history", 3, [{"role": "user", "content": "hi"}], 1, "h"), 2)
|
||||
b.register_contributor("none", lambda task, session: None, 1)
|
||||
return b
|
||||
|
||||
|
||||
b = make_broker()
|
||||
degraded: list[str] = []
|
||||
collected = b._collect({}, {}, degraded)
|
||||
row("collect:degraded-and-ordered", "collect", {},
|
||||
{"names": [name for name, _blk, _b in collected],
|
||||
"degraded": degraded,
|
||||
"failed_events": sorted(getattr(b, "_last_failures", []))})
|
||||
|
||||
# admit: priority desc, recency tie, memory sub-budget, overflow
|
||||
def blocks(*specs):
|
||||
out = []
|
||||
for name, pri, tokens, evictable in specs:
|
||||
out.append(ContributionBlock(name, pri, "c" * tokens, tokens, f"src-{name}", evictable=evictable))
|
||||
return out
|
||||
|
||||
|
||||
bk = ContextBroker()
|
||||
admitted, adm = bk._admit([(b["name"], bl, 0) for b, bl in zip(
|
||||
[{"name": "system"}, {"name": "memory"}, {"name": "history"}, {"name": "user"}],
|
||||
blocks(("system", 100, 500, False), ("memory", 5, 300, True),
|
||||
("history", 7, 900, True), ("user", 100, 100, False)))], 1000)
|
||||
row("admit:priority-recency-budget", "admit", {},
|
||||
{"admitted": [bl.name for bl in admitted],
|
||||
"blocks": adm["blocks"], "admitted_total": adm["admitted_total"],
|
||||
"dropped": [d["name"] for d in adm["dropped"]],
|
||||
"memory_budget": adm["memory_budget"], "memory_used": adm["memory_used"]})
|
||||
|
||||
# memory sub-budget: two memory blocks, second exceeds sub-budget
|
||||
admitted2, adm2 = bk._admit([("m1", blocks(("m1", 9, 700, True))[0], 0),
|
||||
("m2", blocks(("m2", 8, 700, True))[0], 1)], 4000, memory_budget=1200)
|
||||
row("admit:memory-subbudget-drop", "admit", {},
|
||||
{"admitted": [bl.name for bl in admitted2],
|
||||
"dropped": [d["name"] for d in adm2["dropped"]],
|
||||
"memory_used": adm2["memory_used"],
|
||||
"reasons": [d["reason"] for d in adm2["dropped"]]})
|
||||
|
||||
# mandatory overflow → raise
|
||||
try:
|
||||
bk._admit([("user", blocks(("user", 100, 5000, False))[0], 0)], 1000)
|
||||
row("admit:mandatory-overflow-raises", "admit", {}, "NO-RAISE")
|
||||
except ContextOverflowError as exc:
|
||||
row("admit:mandatory-overflow-raises", "admit", {},
|
||||
{"error": "ContextOverflowError", "detail": str(exc)})
|
||||
|
||||
# render: system folding, history pass-through, user capture
|
||||
capsule = bk._render(blocks(("persona", 100, 7, False), ("tools", 90, 5, False),
|
||||
("history", 70, 4, False), ("user", 100, 2, False)))
|
||||
row("render:order-and-fold", "render", {},
|
||||
{"token_count": capsule.token_count,
|
||||
"messages": capsule.messages,
|
||||
"system_preamble": capsule.system_preamble,
|
||||
"user_content": capsule.user_content,
|
||||
"blocks": [bl.name for bl in capsule.blocks]})
|
||||
|
||||
print(f"froze {len(rows)} rows total")
|
||||
|
||||
doc = {
|
||||
"frozen_ts": "2026-10-12T00:00:00Z",
|
||||
"generator": "scripts/gen_context_broker_truth.py — live oracle src/rune/context_broker.py, RUNE_COGNITION_CORE=python pinned",
|
||||
"rows": rows,
|
||||
}
|
||||
out = ROOT / "src/go/context-broker/truth.json"
|
||||
out.write_text(json.dumps(doc, indent=1, sort_keys=True) + "\n")
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_night_mode_truth.py — factory wave truth battery for night_mode.
|
||||
|
||||
Exercises the module-level public surface of src/rune/night_mode.py whose
|
||||
results are clock- and state-file-invariant, freezing the oracle's behavior
|
||||
so the Go port at src/go/night-mode replays it byte-for-byte:
|
||||
|
||||
* _ctx_budget — every profile branch (T0 window-0, T1/T2/T3 local,
|
||||
T4 non-local tool reserve, unknown-model fallback)
|
||||
* stop_night_mode — idle reverse guard (error branch)
|
||||
* get_status — idle status shape
|
||||
* get_report — idle report shape
|
||||
|
||||
The stateful lifecycle (start/stop/status/report/to_dict/_load/_save/
|
||||
_run_loop/_run_sleep/_run_evaluation) is IO-bound (clock, storage, threads,
|
||||
Ollama HTTP): it is pinned by the Go unit battery (night_mode_test.go,
|
||||
injected clock/runner/HTTP seams) and tests/test_night_mode.py, NOT by this
|
||||
file — freeze_truth.py --verify re-runs cases against a fresh module with
|
||||
the real clock, so only clock-free results belong here.
|
||||
|
||||
Verify (byte-identical law):
|
||||
RUNE_MEMORY_DIR=$(mktemp -d) PYTHONPATH=src \\
|
||||
python3 scripts/freeze_truth.py --verify \\
|
||||
src/go/night-mode/frozen_truth.json --module rune.night_mode
|
||||
(RUNE_MEMORY_DIR must point at a directory with no night_mode.json — the
|
||||
idle-state cases construct the global NightMode from it.)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
os.environ["RUNE_MEMORY_DIR"] = tempfile.mkdtemp(prefix="nm-truth-")
|
||||
os.environ["RUNE_NIGHT_MODE_CORE"] = "python"
|
||||
|
||||
from freeze_truth import freeze, write # noqa: E402
|
||||
|
||||
import rune.night_mode as nm # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = [
|
||||
# stop: idle reverse guard (error branch — deterministic)
|
||||
("stop_night_mode", (), {}),
|
||||
# status/report: idle shapes (started_at None, defaults)
|
||||
("get_status", (), {}),
|
||||
("get_report", (), {}),
|
||||
# _ctx_budget: every resolve branch of model_registry.DEFAULT_PROFILES
|
||||
("_ctx_budget", ("qwen2.5:3b",), {}), # T1 local, window 32768
|
||||
("_ctx_budget", ("qwen3:14b",), {}), # T2 local, window 32768
|
||||
("_ctx_budget", ("qwen3.6:27b",), {}), # T3 local, window 32768
|
||||
("_ctx_budget", ("mimo-v2.5-pro",), {}), # T4 non-local + tool reserve
|
||||
("_ctx_budget", ("deterministic",), {}), # T0 window 0 -> or-8192
|
||||
("_ctx_budget", ("unknown-model",), {}), # missing -> fallback 8192
|
||||
]
|
||||
truth = freeze("rune.night_mode", cases, oracle_module="rune.night_mode")
|
||||
dest = REPO / "src/go/night-mode/frozen_truth.json"
|
||||
write(truth, dest)
|
||||
print(f"wrote {len(truth)} cases → {dest}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the dichotomy-wave gap-closing truth corpus for
|
||||
src/rune/structural_store.py (contract BUILDER-2026-10-13T00:32:22Z-bc3aae-
|
||||
factory-gap-ss, issue #2479 tranche).
|
||||
|
||||
LAW (poisoned-truth): RUNE_STRUCTURAL_CORE=python is pinned at the top —
|
||||
truth is generated by the PYTHON ORACLE ONLY. Inputs are deep-copied before
|
||||
every oracle call (no leakage between cases). The clock is pinned by
|
||||
monkeypatching structural_store.now (no-clock law on the Rust side).
|
||||
|
||||
The frozen corpus is embedded VERBATIM in the crate's cfg(test) battery
|
||||
(src/frozen_battery.rs, include_str!) and replayed by
|
||||
scripts/check_structural_store_parity.py through the live FFI.
|
||||
"""
|
||||
import calendar
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time as _time
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_STRUCTURAL_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
FIXED_NOW = "2026-10-13T01:02:03Z"
|
||||
FIXED_EPOCH = calendar.timegm(_time.strptime(FIXED_NOW, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
ss.now = lambda: FIXED_NOW # pin the clock
|
||||
# snapshot() formats its stamp straight off time.strftime/gmtime — pin the
|
||||
# module reference the oracle actually uses.
|
||||
ss.time = types.SimpleNamespace(
|
||||
gmtime=lambda: _time.gmtime(FIXED_EPOCH),
|
||||
strftime=lambda fmt, t=None: _time.strftime(fmt, t if t is not None else _time.gmtime()),
|
||||
)
|
||||
|
||||
OUT = (REPO / "src" / "rust" / "structural_store" / "src"
|
||||
/ "frozen_ss_truth.json")
|
||||
|
||||
groups: dict[str, list] = {}
|
||||
|
||||
|
||||
def case(group, name, args, expect):
|
||||
groups.setdefault(group, []).append(
|
||||
{"fn": group, "name": name, "args": args, "expect": expect})
|
||||
|
||||
|
||||
def run(group, name, args, call):
|
||||
"""Freeze one case: input deep-copied, oracle called, outcome recorded."""
|
||||
frozen_args = copy.deepcopy(args)
|
||||
try:
|
||||
expect = {"ok": call(copy.deepcopy(args))}
|
||||
except ValueError as exc:
|
||||
expect = {"err": str(exc)}
|
||||
case(group, name, frozen_args, expect)
|
||||
|
||||
|
||||
def ent(id, kind="module", maturity="DECLARED", **kw):
|
||||
e = {"id": id, "kind": kind, "maturity": maturity, "evidence_refs": []}
|
||||
e.update(kw)
|
||||
return e
|
||||
|
||||
|
||||
def doc(*entities, rels=(), obs=(), **kw):
|
||||
d = {"schema_version": 1, "generated_at": FIXED_NOW,
|
||||
"entities": list(entities), "relationships": list(rels),
|
||||
"observations": list(obs)}
|
||||
d.update(kw)
|
||||
return d
|
||||
|
||||
|
||||
def rel(t, f, to, **kw):
|
||||
r = {"type": t, "from": f, "to": to, "evidence_refs": ["ev"],
|
||||
"observed_at": FIXED_NOW}
|
||||
r.update(kw)
|
||||
return r
|
||||
|
||||
|
||||
def main():
|
||||
# ── _sorted ─────────────────────────────────────────────────────────
|
||||
for name, v in [
|
||||
("dict_keys_sorted", {"b": 1, "a": {"z": 2, "y": [3, {"c": 4, "b": 5}]}}),
|
||||
("list_of_dicts_by_canonical_form", [{"b": 2, "a": 1}, {"a": 9}, {"a": 1, "b": 2}, {}]),
|
||||
("list_of_scalars", [3, 1, 2, "b", "a", True, None]),
|
||||
("nested_lists", [[2], [1, 2], [1], []]),
|
||||
("unicode_keys_and_values", {"é": "ü", "z": "a", "a": ["z", "é"]}),
|
||||
("floats_and_ints", [1.5, 0.5, 2, 1, -1.25]),
|
||||
("empty_containers", {"e": [], "f": {}, "g": [[]]}),
|
||||
("dict_of_scalars_passthrough", {"x": 1, "y": "s", "z": None, "w": False}),
|
||||
]:
|
||||
run("sorted", name, {"value": v}, lambda a: ss._sorted(a["value"]))
|
||||
|
||||
# ── new_doc ─────────────────────────────────────────────────────────
|
||||
run("new_doc", "basic", {"now": FIXED_NOW}, lambda a: ss.new_doc())
|
||||
|
||||
# ── _redact_secrets ─────────────────────────────────────────────────
|
||||
for name, v in [
|
||||
("exact_names", {"token": "t", "api_token": "t", "api_key": "t",
|
||||
"apikey": "t", "password": "t", "secret": "t",
|
||||
"auth_header": "t", "authorization": "t",
|
||||
"credential": "t", "credentials": "t",
|
||||
"private_key": "t", "access_token": "t",
|
||||
"bearer": "t", "safe": "keep"}),
|
||||
("substring_names", {"my_token": "t", "API_KEY": "t",
|
||||
"user_password_hash": "t", "client_secret": "t",
|
||||
"my_credentials_v2": "t", "token_count": "t"}),
|
||||
("nested", {"a": {"token": "deep"}, "b": [{"password": "x"}, {"ok": 1}]}),
|
||||
("non_secret_passthrough", {"a": 1, "b": "s", "c": None, "d": [1, 2]}),
|
||||
("case_insensitive", {"Token": "t", "PASSWORD": "t"}),
|
||||
]:
|
||||
run("redact", name, {"value": v}, lambda a: ss._redact_secrets(a["value"]))
|
||||
|
||||
# ── save (byte law frozen from the real filesystem) ────────────────
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
def save_call(args):
|
||||
p = ss.save(copy.deepcopy(args["doc"]),
|
||||
Path(td) / args["name"] / "store.json")
|
||||
return {"content": Path(p).read_text(), "doc": p and None}
|
||||
# capture the mutated doc separately (save mutates schema_version in)
|
||||
def save_case(name, d, expect_err=False):
|
||||
args = {"doc": copy.deepcopy(d), "name": name}
|
||||
try:
|
||||
probe = copy.deepcopy(d)
|
||||
p = ss.save(probe, Path(td) / name / "store.json")
|
||||
content = Path(p).read_text()
|
||||
expect = {"ok": {"content": content, "doc": probe}}
|
||||
except ValueError as exc:
|
||||
expect = {"err": str(exc)}
|
||||
case("save", name, args, expect)
|
||||
|
||||
base = doc(ent("m:a"), rels=[rel("depends_on", "m:a", "m:b")], generated_at=FIXED_NOW)
|
||||
save_case("full_doc", base)
|
||||
save_case("no_schema_version", {k: v for k, v in base.items() if k != "schema_version"})
|
||||
save_case("schema_one_float", {**base, "schema_version": 1.0})
|
||||
save_case("schema_ninety_nine", {**base, "schema_version": 99})
|
||||
save_case("schema_string", {**base, "schema_version": "x"})
|
||||
save_case("secrets_redacted_on_disk", {**base, "credentials": {"token": "s3cret", "note": "x"}})
|
||||
save_case("unicode_escaped", {**base, "note": "café ☕ — ok"})
|
||||
save_case("empty_doc", {"schema_version": 1})
|
||||
save_case("nested_empty", {**base, "ext": {"a": [], "b": {}}})
|
||||
|
||||
# ── canonical_digest ────────────────────────────────────────────────
|
||||
dd = doc(ent("m:a"), rels=[rel("depends_on", "m:a", "m:b")])
|
||||
run("digest", "basic", {"doc": dd}, lambda a: ss.canonical_digest(a["doc"]))
|
||||
run("digest", "key_order_irrelevant",
|
||||
{"doc": dd, "doc_reordered": {"entities": dd["entities"], "schema_version": 1,
|
||||
"observations": dd["observations"],
|
||||
"relationships": dd["relationships"],
|
||||
"generated_at": dd["generated_at"]}},
|
||||
lambda a: [ss.canonical_digest(a["doc"]), ss.canonical_digest(a["doc_reordered"])])
|
||||
run("digest", "secret_never_changes_digest",
|
||||
{"doc": {**dd, "token": "s3cret"}},
|
||||
lambda a: [ss.canonical_digest(a["doc"]), ss.canonical_digest({**a["doc"], "token": "other"})])
|
||||
run("digest", "unicode", {"doc": {**dd, "note": "é"}}, lambda a: ss.canonical_digest(a["doc"]))
|
||||
|
||||
# ── snapshot ────────────────────────────────────────────────────────
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
def snap_case(name, d, label, tag):
|
||||
args = {"doc": copy.deepcopy(d), "dir": "snaps", "label": label, "tag": tag}
|
||||
probe = copy.deepcopy(d)
|
||||
p = ss.snapshot(probe, Path(td) / name / "snaps", label, tag=tag)
|
||||
content = Path(p).read_text()
|
||||
case("snapshot", name, args,
|
||||
{"ok": {"path": "snaps/" + Path(p).name, "content": content, "doc": probe}})
|
||||
snap_case("label_and_tag", dd, "l1", "deadbeef1234")
|
||||
snap_case("no_label", dd, "", "deadbeef1234")
|
||||
snap_case("empty_tag_is_noident", dd, "l1", "")
|
||||
snap_case("short_tag", dd, "", "abc")
|
||||
|
||||
# ── alias ───────────────────────────────────────────────────────────
|
||||
adoc = doc(ent("m:a"), ent("m:b"))
|
||||
run("alias", "new_alias", {"doc": adoc, "cid": "m:a", "old": "m:old"},
|
||||
lambda a: ss.alias(a["doc"], a["cid"], a["old"]))
|
||||
run("alias", "idempotent",
|
||||
{"doc": doc(ent("m:a", aliases=["m:old"])), "cid": "m:a", "old": "m:old"},
|
||||
lambda a: ss.alias(a["doc"], a["cid"], a["old"]))
|
||||
run("alias", "target_missing", {"doc": adoc, "cid": "m:zz", "old": "m:old"},
|
||||
lambda a: ss.alias(a["doc"], a["cid"], a["old"]))
|
||||
run("alias", "self_alias", {"doc": adoc, "cid": "m:a", "old": "m:a"},
|
||||
lambda a: ss.alias(a["doc"], a["cid"], a["old"]))
|
||||
run("alias", "via_alias_resolution",
|
||||
{"doc": doc(ent("m:a", aliases=["m:old"])), "cid": "m:old", "old": "m:older"},
|
||||
lambda a: ss.alias(a["doc"], a["cid"], a["old"]))
|
||||
|
||||
# ── spine_uri / resolve_uri ─────────────────────────────────────────
|
||||
for name, eid in [("kind_name", "project:alpha"), ("no_colon", "alpha"),
|
||||
("two_colons", "project:al:pha")]:
|
||||
run("spine_uri", name, {"entity_id": eid}, lambda a: ss.spine_uri(a["entity_id"]))
|
||||
for name, uri in [("ok", "spine://project/alpha"), ("no_slash", "spine://alpha"),
|
||||
("wrong_scheme", "memory://project/alpha"), ("no_scheme", "project/alpha"),
|
||||
("nested_slash", "spine://project/a/b")]:
|
||||
run("resolve_uri", name, {"uri": uri}, lambda a: ss.resolve_uri(a["uri"]))
|
||||
|
||||
# ── _doc_authorities ────────────────────────────────────────────────
|
||||
for name, d in [
|
||||
("defaults_in", doc(ent("m:a"))),
|
||||
("existing_untouched", {**doc(ent("m:a")), "authority_generations": {"p": {"generation": 2}}}),
|
||||
]:
|
||||
a = {"doc": copy.deepcopy(d)}
|
||||
d2 = copy.deepcopy(d)
|
||||
r = ss._doc_authorities(d2)
|
||||
case("doc_authorities", name, a, {"ok": {"doc": d2, "ret": r}})
|
||||
|
||||
# ── authority_health ────────────────────────────────────────────────
|
||||
run("authority_health", "empty", {"doc": doc(ent("m:a"))}, lambda a: ss.authority_health(a["doc"]))
|
||||
run("authority_health", "populated",
|
||||
{"doc": {**doc(ent("m:a")), "authority_generations": {
|
||||
"z-proj": {"generation": 3, "status": "CURRENT", "last_success": FIXED_NOW,
|
||||
"last_attempt": FIXED_NOW, "last_error": None, "observed": ["owns|a|b", "owns|c|d"]},
|
||||
"a-proj": {"generation": 1, "status": "DEGRADED", "observed": []}}}},
|
||||
lambda a: ss.authority_health(a["doc"]))
|
||||
run("authority_health", "missing_fields",
|
||||
{"doc": {**doc(ent("m:a")), "authority_generations": {"p": {}}}},
|
||||
lambda a: ss.authority_health(a["doc"]))
|
||||
|
||||
# ── bind_task / task_entities ───────────────────────────────────────
|
||||
for name, d, task_id, ids, ev in [
|
||||
("with_evidence", doc(ent("m:a"), ent("m:b")), "T1", ["m:a", "m:b"], "ev-1"),
|
||||
("fallback_evidence", doc(ent("m:a")), "T2", ["m:a"], ""),
|
||||
("dedupes_relationships", doc(ent("m:a")), "T3", ["m:a", "m:a"], "ev"),
|
||||
]:
|
||||
a = {"doc": copy.deepcopy(d), "task_id": task_id, "entity_ids": ids, "evidence_ref": ev}
|
||||
d2 = copy.deepcopy(d)
|
||||
r = ss.bind_task(d2, task_id, copy.deepcopy(ids), ev)
|
||||
case("bind_task", name, a, {"ok": {"doc": d2, "ret": r}})
|
||||
tdoc = doc(ent("m:a"), ent("m:b"),
|
||||
rels=[rel("affects", "task:T1", "m:b"), rel("affects", "task:T1", "m:a"),
|
||||
rel("affects", "task:T9", "m:b")])
|
||||
run("task_entities", "sorted_for_task", {"doc": tdoc, "task_id": "T1"},
|
||||
lambda a: ss.task_entities(a["doc"], a["task_id"]))
|
||||
run("task_entities", "no_bindings", {"doc": tdoc, "task_id": "TX"},
|
||||
lambda a: ss.task_entities(a["doc"], a["task_id"]))
|
||||
|
||||
# ── impact_frontier (branch battery) ────────────────────────────────
|
||||
def frontier_case(name, d, seeds, change_kind=None, max_level=2):
|
||||
args = {"doc": copy.deepcopy(d), "seeds": seeds,
|
||||
"change_kind": change_kind, "max_level": max_level}
|
||||
run("impact_frontier", name, args,
|
||||
lambda a: ss.impact_frontier(a["doc"], a["seeds"], max_level=a["max_level"],
|
||||
change_kind=a["change_kind"]))
|
||||
|
||||
impl_doc = doc(
|
||||
ent("m:core"), ent("m:app"), ent("m:lib"), ent("m:tests"), ent("m:deploy"),
|
||||
rels=[
|
||||
rel("depends_on", "m:app", "m:core"),
|
||||
rel("implements", "m:app", "c:api"),
|
||||
rel("implements", "m:lib", "c:api"),
|
||||
rel("consumes", "m:core", "c:api"),
|
||||
rel("verified_by", "m:app", "k:canary"),
|
||||
rel("deployed_as", "m:core", "d:live"),
|
||||
rel("documented_by", "m:core", "doc:r"),
|
||||
rel("supersedes", "m:lib", "m:old"),
|
||||
rel("consumed_by", "m:lib", "m:core"),
|
||||
])
|
||||
frontier_case("implementation_change", impl_doc, ["m:app"])
|
||||
frontier_case("contract_change", impl_doc, ["c:api"])
|
||||
frontier_case("contract_violation", impl_doc, ["c:api"], change_kind="contract_violation")
|
||||
frontier_case("inferred_from_contract_kind", impl_doc, ["c:api"], change_kind=None)
|
||||
frontier_case("stale_and_candidate_skipped",
|
||||
doc(ent("m:a"), ent("m:b"), ent("m:c"),
|
||||
rels=[rel("depends_on", "m:a", "m:b", status="STALE"),
|
||||
rel("depends_on", "m:a", "m:c", status="CANDIDATE")]),
|
||||
["m:a"])
|
||||
frontier_case("non_obligation_never_expands",
|
||||
doc(ent("m:a"), ent("m:b"),
|
||||
rels=[rel("documented_by", "m:a", "m:b")]),
|
||||
["m:a"])
|
||||
frontier_case("consumed_by_reverse",
|
||||
doc(ent("m:a"), ent("m:b"),
|
||||
rels=[rel("consumed_by", "m:b", "m:a")]),
|
||||
["m:a"])
|
||||
frontier_case("level2_from_level1_and_contextual", impl_doc, ["m:core"])
|
||||
frontier_case("multi_seed_dedup", impl_doc, ["m:app", "m:app", "m:core"])
|
||||
frontier_case("alias_seed_kind",
|
||||
doc(ent("c:api", kind="contract", aliases=["c:old"]),
|
||||
ent("m:app"),
|
||||
rels=[rel("implements", "m:app", "c:api")]),
|
||||
["c:old"])
|
||||
|
||||
# ── record_observed_impact ──────────────────────────────────────────
|
||||
for name, items in [
|
||||
("basic", [{"entity": "m:a", "source": "test", "detail": "d", "authority": "ci"},
|
||||
{"entity": "m:b"}]),
|
||||
("empty_entity_skipped", [{"entity": "", "source": "x"}, {"entity": "m:a"}]),
|
||||
("detail_truncated_300", [{"entity": "m:a", "detail": "x" * 500}]),
|
||||
("detail_multibyte_truncated", [{"entity": "m:a", "detail": "é" * 400}]),
|
||||
("defaults", [{"entity": "m:a"}]),
|
||||
]:
|
||||
d0 = doc(ent("m:a"), ent("m:b"))
|
||||
a = {"doc": copy.deepcopy(d0), "task_id": "T1", "items": copy.deepcopy(items)}
|
||||
d2 = copy.deepcopy(d0)
|
||||
result = ss.record_observed_impact(d2, "T1", copy.deepcopy(items))
|
||||
case("observed_impact", name, a, {"ok": {"doc": d2, "result": result}})
|
||||
|
||||
# ── predicted_vs_observed ───────────────────────────────────────────
|
||||
pdoc = doc(ent("m:a"), ent("m:b"), ent("m:c"),
|
||||
rels=[rel("depends_on", "m:a", "m:b"), rel("depends_on", "m:a", "m:c")])
|
||||
pred = {"level0_direct": ["m:a"], "level1_obligations": ["m:b"],
|
||||
"contracts": [], "level2_verification": []}
|
||||
run("predicted_vs_observed", "miss_and_needless",
|
||||
{"predicted": pred, "observed_doc": pdoc, "seeds": ["m:a"]},
|
||||
lambda a: ss.predicted_vs_observed(a["predicted"], a["observed_doc"], a["seeds"]))
|
||||
run("predicted_vs_observed", "exact_match",
|
||||
{"predicted": {"level0_direct": ["m:a"], "level1_obligations": ["m:b", "m:c"],
|
||||
"contracts": [], "level2_verification": []},
|
||||
"observed_doc": pdoc, "seeds": ["m:a"]},
|
||||
lambda a: ss.predicted_vs_observed(a["predicted"], a["observed_doc"], a["seeds"]))
|
||||
|
||||
# ── structural_events ───────────────────────────────────────────────
|
||||
run("events", "full",
|
||||
{"doc": doc(ent("m:a", observed_at=FIXED_NOW), ent("m:b", observed_at=FIXED_NOW),
|
||||
rels=[rel("depends_on", "m:a", "m:b")],
|
||||
obs=[{"kind": "superseded", "old": "x", "new": "y", "observed_at": FIXED_NOW}])},
|
||||
lambda a: ss.structural_events(a["doc"]))
|
||||
run("events", "observation_kind_default_and_type_override",
|
||||
{"doc": doc(ent("m:a"),
|
||||
obs=[{"note": "no kind"}, {"kind": "k1", "type": "explicit_type"}])},
|
||||
lambda a: ss.structural_events(a["doc"]))
|
||||
|
||||
# ── diff ────────────────────────────────────────────────────────────
|
||||
old = doc(ent("m:a"), ent("m:gone"), ent("m:same", identities={"p": 1}, maturity="DECLARED"),
|
||||
rels=[rel("depends_on", "m:a", "m:gone")])
|
||||
new = doc(ent("m:a"), ent("m:new"), ent("m:same", identities={"p": 2}, maturity="WIRED"),
|
||||
rels=[rel("depends_on", "m:a", "m:new"), rel("depends_on", "m:a", "m:a")])
|
||||
run("diff", "all_categories", {"old": old, "new": new}, lambda a: ss.diff(a["old"], a["new"]))
|
||||
run("diff", "no_changes", {"old": old, "new": copy.deepcopy(old)},
|
||||
lambda a: ss.diff(a["old"], a["new"]))
|
||||
|
||||
# ── dedupe_observations ─────────────────────────────────────────────
|
||||
for name, d in [
|
||||
("exact_dups", doc(ent("m:a"), obs=[{"kind": "k", "x": 1}, {"x": 1, "kind": "k"},
|
||||
{"kind": "k", "x": 2}])),
|
||||
("nested_dups", doc(ent("m:a"), obs=[{"a": {"z": 1, "b": [2]}}, {"a": {"b": [2], "z": 1}}])),
|
||||
("empty", doc(ent("m:a"))),
|
||||
]:
|
||||
a = {"doc": copy.deepcopy(d)}
|
||||
d2 = copy.deepcopy(d)
|
||||
r = ss.dedupe_observations(d2)
|
||||
case("dedupe", name, a, {"ok": {"doc": d2, "ret": r}})
|
||||
|
||||
# ── fixed-kernel regressions (oracle parity proofs) ─────────────────
|
||||
for name, d, eid, state, ev, ret in [
|
||||
("unknown_entity_returns_none", doc(ent("m:a")), "m:zz", "PRESENT", "e", None),
|
||||
("via_alias", doc(ent("m:a", aliases=["m:old"])), "m:old", "PRESENT", "e", "PRESENT"),
|
||||
("missing_entity_beats_bad_state", doc(ent("m:a")), "m:zz", "BOGUS", "", None),
|
||||
("demotion_recorded", doc(ent("m:a", maturity="PRESENT")), "m:a", "DECLARED", "e2", "PRESENT"),
|
||||
]:
|
||||
a = {"doc": copy.deepcopy(d), "id": eid, "state": state, "ev": ev}
|
||||
d2 = copy.deepcopy(d)
|
||||
r = ss.promote(d2, eid, state, ev)
|
||||
case("promote_fix", name, a, {"ok": {"ret": r, "doc": d2}})
|
||||
run("resolve_status_fix", "status_verbatim",
|
||||
{"entity": {"status": "WEIRD"}}, lambda a: ss.resolve_status(a["entity"]))
|
||||
run("resolve_status_fix", "empty_conflicts_not_conflicting",
|
||||
{"entity": {"maturity": "DECLARED", "ext": {"conflicts": []}}},
|
||||
lambda a: ss.resolve_status(a["entity"]))
|
||||
for name, d, nid, oid in [
|
||||
("rel_has_observed_at", doc(ent("m:a"), ent("m:b")), "m:b", "m:a"),
|
||||
("via_alias", doc(ent("m:a", aliases=["m:old"]), ent("m:b")), "m:b", "m:old"),
|
||||
]:
|
||||
a = {"doc": copy.deepcopy(d), "new": nid, "old": oid, "ev": "ev-s"}
|
||||
d2 = copy.deepcopy(d)
|
||||
ss.supersede(d2, nid, oid, "ev-s")
|
||||
case("supersede_fix", name, a, {"ok": {"doc": d2}})
|
||||
|
||||
out = {"module": "structural_store", "now": FIXED_NOW, "groups": groups}
|
||||
OUT.write_text(json.dumps(out, indent=1, sort_keys=True) + "\n")
|
||||
total = sum(len(v) for v in groups.values())
|
||||
print(f"frozen {total} cases across {len(groups)} groups -> {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave gap-closing construction contract for
|
||||
structural_store (PARTIAL → ≥80% fn coverage). Walks draft→approved→building
|
||||
in ONE process (shared-store clobber hazard).
|
||||
|
||||
Usage: open_contract_gap_ss.py
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-ss")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Close structural_store PARTIAL gaps (dichotomy wave, issue #2479 "
|
||||
"tranche): port the 21 audit-missing functions name-preserving to the "
|
||||
"existing Rust crate (doc-envelope FFI ss_*, ctypes loader mirroring "
|
||||
"broker_loader.py, facade delegation with RUNE_STRUCTURAL_CORE=python "
|
||||
"pin), frozen-truth battery generated against the live oracle, live "
|
||||
"parity 100% on new fns, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:400])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting")
|
||||
sys.exit(1)
|
||||
c = r["ids"]["contract_id"]
|
||||
print("contract_id:", c)
|
||||
|
||||
for phase in ("approved", "building"):
|
||||
t = iface.transition(c, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:300])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
print("OPENED", c)
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for night_mode.
|
||||
Full lifecycle in ONE process (shared-store clobber hazard)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-nm")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Close the night_mode dichotomy PARTIAL gaps (audit-named): Go port "
|
||||
"of _ctx_budget, get_night_mode, start_night_mode, stop_night_mode, "
|
||||
"get_status, get_report, to_dict, _load, _save, _run_loop, _run_sleep, "
|
||||
"_run_evaluation (name-preserving, logic 1:1) in src/go/night-mode; "
|
||||
"pure/IO split law with injectable clock/sleeper/runner/HTTP seams; "
|
||||
"frozen-oracle truth battery via shared freeze_truth.py embedded "
|
||||
"VERBATIM in Go tests; live parity 100% on new fns; Python facade "
|
||||
"stays fully functional (RUNE_NIGHT_MODE_CORE=python pin + auto "
|
||||
"both green).",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:400])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:300])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
print("PENDING_CONTRACT", cid)
|
||||
+141
-4
@@ -24,6 +24,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
@@ -200,10 +201,142 @@ type GapCase struct {
|
||||
// import surface beyond encoding/json.
|
||||
type jsonRaw = json.RawMessage
|
||||
|
||||
// DispatchGap — replay one frozen call through the ported function;
|
||||
// return the JSON-comparable result and the post-call state pointer.
|
||||
// Lives in production code (not a _test file) so ReplayPipeline builds
|
||||
// under `go build` too. The _save_state case is IO-backed: callers that
|
||||
// need byte-output parity use SaveState directly with their own dir.
|
||||
func DispatchGap(state *BuildState, fn string, args []interface{}, stamp string) (result interface{}, outState *BuildState, err error) {
|
||||
name := strings.TrimPrefix(fn, "BuildManager.")
|
||||
arg := func(i int) interface{} {
|
||||
if i < len(args) {
|
||||
return args[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch name {
|
||||
case "assess_complexity":
|
||||
return AssessComplexity(), state, nil
|
||||
case "complete_assess":
|
||||
CompleteAssess(state, arg(0).(map[string]interface{}), stamp)
|
||||
case "start_research":
|
||||
StartResearch(state, stamp)
|
||||
case "complete_research":
|
||||
CompleteResearch(state, arg(0).(map[string]interface{}), stamp)
|
||||
case "start_feasible":
|
||||
StartFeasible(state, stamp)
|
||||
case "complete_feasible":
|
||||
CompleteFeasible(state, arg(0) == true, arg(1).(string),
|
||||
ArgMap(arg(2)), stamp)
|
||||
case "start_consult":
|
||||
StartConsult(state, stamp)
|
||||
case "complete_consult":
|
||||
CompleteConsult(state, StrList(arg(0)), stamp)
|
||||
case "start_consent":
|
||||
StartConsent(state, stamp)
|
||||
case "complete_consent":
|
||||
CompleteConsent(state, arg(0) == true, stamp)
|
||||
case "start_plan":
|
||||
StartPlan(state, stamp)
|
||||
case "complete_plan":
|
||||
CompletePlan(state, ObjList(arg(0)), stamp)
|
||||
case "start_build_phase":
|
||||
StartBuildPhase(state, stamp)
|
||||
case "complete_build_phase":
|
||||
CompleteBuildPhase(state, ObjList(arg(0)), stamp)
|
||||
case "start_verify":
|
||||
StartVerify(state, stamp)
|
||||
case "complete_verify":
|
||||
CompleteVerify(state, arg(0) == true, StrList(arg(1)), stamp)
|
||||
case "start_correct":
|
||||
StartCorrect(state, stamp)
|
||||
case "complete_correct":
|
||||
CompleteCorrect(state, arg(0) == true, stamp)
|
||||
case "start_integrate":
|
||||
StartIntegrate(state, stamp)
|
||||
case "complete_integrate":
|
||||
CompleteIntegrate(state, arg(0) == true, StrList(arg(1)), stamp)
|
||||
case "start_record":
|
||||
StartRecord(state, stamp)
|
||||
case "complete_record":
|
||||
CompleteRecord(state, stamp)
|
||||
case "_save_state":
|
||||
// IO seam: the serial battery covers save-bytes via SaveState
|
||||
// with a test-owned dir; here persist to a throwaway dir for
|
||||
// state-shape parity only.
|
||||
dir, derr := os.MkdirTemp("", "gapreplay")
|
||||
if derr != nil {
|
||||
return nil, state, derr
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
if serr := SaveState(state, dir); serr != nil {
|
||||
return nil, state, serr
|
||||
}
|
||||
return nil, state, nil
|
||||
case "get_resume_point":
|
||||
rp := ResumePoint(state)
|
||||
if rp == "" {
|
||||
return nil, state, nil // Python None
|
||||
}
|
||||
return rp, state, nil
|
||||
case "can_resume":
|
||||
return CanResume(state), state, nil
|
||||
case "get_status":
|
||||
return BuildStatus(state), state, nil
|
||||
default:
|
||||
return nil, state, fmt.Errorf("unknown frozen fn %q", fn)
|
||||
}
|
||||
return nil, state, nil
|
||||
}
|
||||
|
||||
// MustJSON — marshal for comparison (nil-safe: nil → "null", matching
|
||||
// the oracle's None).
|
||||
func MustJSON(v interface{}) []byte {
|
||||
if v == nil {
|
||||
return []byte("null")
|
||||
}
|
||||
b, _ := json.Marshal(v)
|
||||
return b
|
||||
}
|
||||
|
||||
// ArgMap / StrList / ObjList — typed JSON accessors for the frozen
|
||||
// battery args (production helpers: DispatchGap needs them under
|
||||
// `go build`, not just under `go test`).
|
||||
func ArgMap(v interface{}) map[string]interface{} {
|
||||
m, _ := v.(map[string]interface{})
|
||||
return m
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ReplayPipeline — replay frozen cases across goroutines (each case gets
|
||||
// an isolated receiver rebuilt from its recv state) and reassemble the
|
||||
// results in INPUT order. Parity: every result must canonicalize to the
|
||||
// frozen expected bytes; the caller compares (dispatchGap is the same
|
||||
// frozen expected bytes; the caller compares (DispatchGap is the same
|
||||
// replay used by the serial battery).
|
||||
func ReplayPipeline(t *testing.T, cases []GapCase, stamp string) []replayResult {
|
||||
results := make([]replayResult, len(cases))
|
||||
@@ -221,11 +354,15 @@ func ReplayPipeline(t *testing.T, cases []GapCase, stamp string) []replayResult
|
||||
return
|
||||
}
|
||||
}
|
||||
got, gotState := dispatchGap(t, state, c.Fn, c.Args, stamp)
|
||||
got, gotState, derr := DispatchGap(state, c.Fn, c.Args, stamp)
|
||||
if derr != nil {
|
||||
t.Errorf("case %d dispatch: %v", i, derr)
|
||||
return
|
||||
}
|
||||
results[i] = replayResult{
|
||||
Name: c.Fn,
|
||||
Got: mustJSON(got),
|
||||
State: mustJSON(gotState),
|
||||
Got: MustJSON(got),
|
||||
State: MustJSON(gotState),
|
||||
Expected: []byte(c.Expected.Result),
|
||||
}
|
||||
}(i)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -75,88 +74,23 @@ func TestBuildModeGapParity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchGap — replay one frozen call through the ported function; return
|
||||
// the JSON-comparable result and the post-call state pointer.
|
||||
// dispatchGap — thin test wrapper over the production DispatchGap: keeps
|
||||
// the serial battery's call shape; the _save_state case uses the test's
|
||||
// own TempDir (byte-output parity) instead of DispatchGap's throwaway dir.
|
||||
func dispatchGap(t *testing.T, state *BuildState, fn string, args []interface{}, stamp string) (result interface{}, outState *BuildState) {
|
||||
t.Helper()
|
||||
name := strings.TrimPrefix(fn, "BuildManager.")
|
||||
arg := func(i int) interface{} {
|
||||
if i < len(args) {
|
||||
return args[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch name {
|
||||
case "assess_complexity":
|
||||
return AssessComplexity(), state
|
||||
case "complete_assess":
|
||||
CompleteAssess(state, arg(0).(map[string]interface{}), stamp)
|
||||
case "start_research":
|
||||
StartResearch(state, stamp)
|
||||
case "complete_research":
|
||||
CompleteResearch(state, arg(0).(map[string]interface{}), stamp)
|
||||
case "start_feasible":
|
||||
StartFeasible(state, stamp)
|
||||
case "complete_feasible":
|
||||
CompleteFeasible(state, arg(0) == true, arg(1).(string),
|
||||
argMap(arg(2)), stamp)
|
||||
case "start_consult":
|
||||
StartConsult(state, stamp)
|
||||
case "complete_consult":
|
||||
CompleteConsult(state, strList(arg(0)), stamp)
|
||||
case "start_consent":
|
||||
StartConsent(state, stamp)
|
||||
case "complete_consent":
|
||||
CompleteConsent(state, arg(0) == true, stamp)
|
||||
case "start_plan":
|
||||
StartPlan(state, stamp)
|
||||
case "complete_plan":
|
||||
CompletePlan(state, objList(arg(0)), stamp)
|
||||
case "start_build_phase":
|
||||
StartBuildPhase(state, stamp)
|
||||
case "complete_build_phase":
|
||||
CompleteBuildPhase(state, objList(arg(0)), stamp)
|
||||
case "start_verify":
|
||||
StartVerify(state, stamp)
|
||||
case "complete_verify":
|
||||
CompleteVerify(state, arg(0) == true, strList(arg(1)), stamp)
|
||||
case "start_correct":
|
||||
StartCorrect(state, stamp)
|
||||
case "complete_correct":
|
||||
CompleteCorrect(state, arg(0) == true, stamp)
|
||||
case "start_integrate":
|
||||
StartIntegrate(state, stamp)
|
||||
case "complete_integrate":
|
||||
CompleteIntegrate(state, arg(0) == true, strList(arg(1)), stamp)
|
||||
case "start_record":
|
||||
StartRecord(state, stamp)
|
||||
case "complete_record":
|
||||
CompleteRecord(state, stamp)
|
||||
case "_save_state":
|
||||
if fn == "BuildManager._save_state" {
|
||||
dir := t.TempDir()
|
||||
if err := SaveState(state, dir); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
return nil, state
|
||||
case "get_resume_point":
|
||||
rp := ResumePoint(state)
|
||||
if rp == "" {
|
||||
return nil, state // Python None
|
||||
}
|
||||
return rp, state
|
||||
case "can_resume":
|
||||
return CanResume(state), state
|
||||
case "get_status":
|
||||
return BuildStatus(state), state
|
||||
default:
|
||||
t.Fatalf("unknown frozen fn %q", fn)
|
||||
}
|
||||
return nil, state
|
||||
}
|
||||
|
||||
func argMap(v interface{}) map[string]interface{} {
|
||||
m, _ := v.(map[string]interface{})
|
||||
return m
|
||||
got, gotState, err := DispatchGap(state, fn, args, stamp)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch %q: %v", fn, err)
|
||||
}
|
||||
return got, gotState
|
||||
}
|
||||
|
||||
// canonEq — canonical-JSON comparison (Go-built values vs the frozen
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
package main
|
||||
|
||||
// broker.go — the ContextBroker orchestration core (admission, collection,
|
||||
// rendering) from src/rune/context_broker.py, gap-close dichotomy wave.
|
||||
// The Python class stays the runtime authority; this is the deterministic
|
||||
// kernel with the contributor callbacks and failure recorder injectable.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MemoryBlockNames — blocks that count as "memory" for the absence proof.
|
||||
var MemoryBlockNames = map[string]bool{
|
||||
"memory": true, "persona_memories": true, "consolidated": true,
|
||||
}
|
||||
|
||||
// ErrContextOverflow — one block alone exceeds the input budget (fail loud).
|
||||
var ErrContextOverflow = errors.New("context overflow")
|
||||
|
||||
// ContributionBlock — a typed, measurable piece of context. Content is
|
||||
// either a string (system/user text) or []Message (history messages).
|
||||
type ContributionBlock struct {
|
||||
Name string
|
||||
Priority int
|
||||
Content interface{}
|
||||
Tokens int
|
||||
Source string
|
||||
Evictable bool
|
||||
}
|
||||
|
||||
// ToDict — serialize for pending_state embedding (P1-02 #25).
|
||||
func (b *ContributionBlock) ToDict() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": b.Name,
|
||||
"priority": b.Priority,
|
||||
"content": b.Content,
|
||||
"tokens": b.Tokens,
|
||||
"source": b.Source,
|
||||
"evictable": b.Evictable,
|
||||
}
|
||||
}
|
||||
|
||||
// ContributorFunc — callback(task, session) → block or nil.
|
||||
type ContributorFunc func(task, session map[string]interface{}) *ContributionBlock
|
||||
|
||||
// FailureRecorder is the T1 record_failure seam (oracle: subsystem_health).
|
||||
type FailureRecorder func(name string, err error)
|
||||
|
||||
// Contributor — a named registry entry with its priority bucket.
|
||||
type Contributor struct {
|
||||
Name string
|
||||
Callback ContributorFunc
|
||||
Bucket int
|
||||
}
|
||||
|
||||
// ContextBroker — admission-controlled context assembler (Go kernel).
|
||||
type ContextBroker struct {
|
||||
mu sync.Mutex
|
||||
contributors []Contributor
|
||||
traces []map[string]interface{}
|
||||
Estimator func(string) int
|
||||
BudgetResolver func(int) int
|
||||
Fail FailureRecorder
|
||||
}
|
||||
|
||||
// NewContextBroker wires the defaults.
|
||||
func NewContextBroker() *ContextBroker {
|
||||
return &ContextBroker{
|
||||
Estimator: DefaultEstimator,
|
||||
BudgetResolver: DefaultBudget,
|
||||
Fail: func(string, error) {},
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterContributor names a contributor.
|
||||
func (cb *ContextBroker) RegisterContributor(name string, callback ContributorFunc, bucket int) {
|
||||
cb.mu.Lock()
|
||||
defer cb.mu.Unlock()
|
||||
cb.contributors = append(cb.contributors, Contributor{Name: name, Callback: callback, Bucket: bucket})
|
||||
}
|
||||
|
||||
// TraceLog returns a shallow copy of every assemble trace (thread-safe).
|
||||
func (cb *ContextBroker) TraceLog() []map[string]interface{} {
|
||||
cb.mu.Lock()
|
||||
defer cb.mu.Unlock()
|
||||
out := make([]map[string]interface{}, len(cb.traces))
|
||||
copy(out, cb.traces)
|
||||
return out
|
||||
}
|
||||
|
||||
// Collect runs contributors in registration order; failures are T1 events
|
||||
// (one pass, lock held — side effects run exactly once per turn). Skips nil
|
||||
// blocks and empty string content; malformed content (neither string nor a
|
||||
// list of message dicts) is a T1 failure too (I5).
|
||||
func (cb *ContextBroker) Collect(task, session map[string]interface{}) (collected []struct {
|
||||
Name string
|
||||
Block *ContributionBlock
|
||||
Bucket int
|
||||
}, degraded []string) {
|
||||
cb.mu.Lock()
|
||||
contributors := make([]Contributor, len(cb.contributors))
|
||||
copy(contributors, cb.contributors)
|
||||
cb.mu.Unlock()
|
||||
|
||||
degradedSet := map[string]bool{}
|
||||
for _, c := range contributors {
|
||||
block, err := func() (block *ContributionBlock, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if e, ok := r.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return c.Callback(task, session), nil
|
||||
}()
|
||||
if err != nil {
|
||||
cb.Fail(c.Name, err)
|
||||
if !degradedSet[c.Name] {
|
||||
degradedSet[c.Name] = true
|
||||
degraded = append(degraded, c.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
if s, isStr := block.Content.(string); isStr && s == "" {
|
||||
continue
|
||||
}
|
||||
if !contentOK(block.Content) {
|
||||
err := fmt.Errorf("contributor %q returned malformed content: %T", c.Name, block.Content)
|
||||
cb.Fail(c.Name, err)
|
||||
if !degradedSet[c.Name] {
|
||||
degradedSet[c.Name] = true
|
||||
degraded = append(degraded, c.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
collected = append(collected, struct {
|
||||
Name string
|
||||
Block *ContributionBlock
|
||||
Bucket int
|
||||
}{c.Name, block, c.Bucket})
|
||||
}
|
||||
return collected, degraded
|
||||
}
|
||||
|
||||
func contentOK(content interface{}) bool {
|
||||
if s, isStr := content.(string); isStr {
|
||||
return s != "" || true
|
||||
}
|
||||
list, isList := content.([]interface{})
|
||||
if isList {
|
||||
for _, item := range list {
|
||||
m, isMap := item.(map[string]interface{})
|
||||
if !isMap {
|
||||
return false
|
||||
}
|
||||
if _, has := m["content"]; !has {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if _, isMsgs := content.([]Message); isMsgs {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AdmissionEntry — one collected block paired with its registration name.
|
||||
type AdmissionEntry struct {
|
||||
Name string
|
||||
Block *ContributionBlock
|
||||
}
|
||||
|
||||
// TraceRecord — per-block admission trace row.
|
||||
type TraceRecord struct {
|
||||
Name string `json:"name"`
|
||||
Priority int `json:"priority"`
|
||||
Tokens int `json:"tokens"`
|
||||
Source string `json:"source"`
|
||||
Evictable bool `json:"evictable"`
|
||||
Admitted bool `json:"admitted"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// AdmissionResult — admitted blocks + the full trace dict.
|
||||
type AdmissionResult struct {
|
||||
Admitted []*ContributionBlock
|
||||
Blocks []TraceRecord
|
||||
AdmittedTotal int
|
||||
Dropped []TraceRecord
|
||||
MemoryBudget int
|
||||
MemoryUsed int
|
||||
}
|
||||
|
||||
// Admit — deterministic admission: priority desc, tie: registration recency.
|
||||
// Non-evictable blocks always admit first (overflow → ErrContextOverflow);
|
||||
// evictable blocks admit while the budget lasts; memory-scope blocks are
|
||||
// additionally bounded by the memory sub-budget (P1-03 #26).
|
||||
func Admit(entries []AdmissionEntry, budget int, memoryBudget int) (*AdmissionResult, error) {
|
||||
if memoryBudget < 0 {
|
||||
memoryBudget = DefaultMemoryBudget(0)
|
||||
}
|
||||
type keyed struct {
|
||||
idx int
|
||||
entry AdmissionEntry
|
||||
}
|
||||
items := make([]keyed, len(entries))
|
||||
for i, e := range entries {
|
||||
items[i] = keyed{i, e}
|
||||
}
|
||||
sort.SliceStable(items, func(a, b int) bool {
|
||||
ba, bb := items[a].entry.Block, items[b].entry.Block
|
||||
ka, kb := 0, 0
|
||||
if ba.Evictable {
|
||||
ka = 1
|
||||
}
|
||||
if bb.Evictable {
|
||||
kb = 1
|
||||
}
|
||||
if ka != kb {
|
||||
return ka < kb
|
||||
}
|
||||
if ba.Priority != bb.Priority {
|
||||
return ba.Priority > bb.Priority
|
||||
}
|
||||
return items[a].idx < items[b].idx
|
||||
})
|
||||
|
||||
res := &AdmissionResult{MemoryBudget: memoryBudget}
|
||||
used, memoryUsed := 0, 0
|
||||
for _, it := range items {
|
||||
block := it.entry.Block
|
||||
record := TraceRecord{
|
||||
Name: block.Name, Priority: block.Priority, Tokens: block.Tokens,
|
||||
Source: block.Source, Evictable: block.Evictable,
|
||||
Admitted: true,
|
||||
}
|
||||
if !block.Evictable {
|
||||
if block.Tokens > budget {
|
||||
return nil, fmt.Errorf("mandatory block '%s' needs %d tokens but the input budget is %d — refusing to silently truncate: %w",
|
||||
block.Name, block.Tokens, budget, ErrContextOverflow)
|
||||
}
|
||||
res.Admitted = append(res.Admitted, block)
|
||||
used += block.Tokens
|
||||
res.Blocks = append(res.Blocks, record)
|
||||
continue
|
||||
}
|
||||
inMemoryScope := MemoryBlockNames[block.Name]
|
||||
if inMemoryScope && used+block.Tokens <= budget && memoryUsed+block.Tokens > memoryBudget {
|
||||
record.Admitted = false
|
||||
record.Reason = fmt.Sprintf("memory budget: %d+%d>%d (memory sub-budget exhausted; lowest-priority memory dropped)",
|
||||
memoryUsed, block.Tokens, memoryBudget)
|
||||
res.Blocks = append(res.Blocks, record)
|
||||
continue
|
||||
}
|
||||
if used+block.Tokens <= budget {
|
||||
res.Admitted = append(res.Admitted, block)
|
||||
used += block.Tokens
|
||||
if inMemoryScope {
|
||||
memoryUsed += block.Tokens
|
||||
}
|
||||
res.Blocks = append(res.Blocks, record)
|
||||
} else {
|
||||
record.Admitted = false
|
||||
record.Reason = fmt.Sprintf("budget: %d+%d>%d (lowest-priority evictable dropped)",
|
||||
used, block.Tokens, budget)
|
||||
res.Blocks = append(res.Blocks, record)
|
||||
}
|
||||
}
|
||||
for _, record := range res.Blocks {
|
||||
if !record.Admitted {
|
||||
res.Dropped = append(res.Dropped, record)
|
||||
}
|
||||
}
|
||||
res.AdmittedTotal = used
|
||||
res.MemoryUsed = memoryUsed
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Capsule — the rendered attention capsule.
|
||||
type Capsule struct {
|
||||
TokenCount int
|
||||
Blocks []*ContributionBlock
|
||||
Messages []map[string]interface{}
|
||||
SystemPreamble string
|
||||
UserContent string
|
||||
}
|
||||
|
||||
// Render — admitted blocks into the typed capsule (fixed message order:
|
||||
// system -> tools -> memory/notes -> history -> user via priority sort).
|
||||
func Render(admitted []*ContributionBlock) *Capsule {
|
||||
type keyed struct {
|
||||
idx int
|
||||
block *ContributionBlock
|
||||
}
|
||||
items := make([]keyed, len(admitted))
|
||||
for i, b := range admitted {
|
||||
items[i] = keyed{i, b}
|
||||
}
|
||||
sort.SliceStable(items, func(a, b int) bool {
|
||||
if items[a].block.Priority != items[b].block.Priority {
|
||||
return items[a].block.Priority > items[b].block.Priority
|
||||
}
|
||||
return items[a].idx < items[b].idx
|
||||
})
|
||||
|
||||
var systemParts []string
|
||||
var history []map[string]interface{}
|
||||
var userBlock *ContributionBlock
|
||||
var capsuleBlocks []*ContributionBlock
|
||||
totalTokens := 0
|
||||
|
||||
for _, it := range items {
|
||||
block := it.block
|
||||
totalTokens += block.Tokens
|
||||
capsuleBlocks = append(capsuleBlocks, block)
|
||||
if s, isStr := block.Content.(string); isStr {
|
||||
if block.Name == "user" {
|
||||
userBlock = block
|
||||
} else {
|
||||
systemParts = append(systemParts, s)
|
||||
}
|
||||
} else {
|
||||
switch list := block.Content.(type) {
|
||||
case []Message:
|
||||
for _, m := range list {
|
||||
history = append(history, deepCopyMap(m))
|
||||
}
|
||||
case []interface{}:
|
||||
for _, item := range list {
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
history = append(history, deepCopyMap(m))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
systemPrompt := ""
|
||||
for i, p := range systemParts {
|
||||
if i > 0 {
|
||||
systemPrompt += "\n"
|
||||
}
|
||||
systemPrompt += p
|
||||
}
|
||||
rendered := make([]map[string]interface{}, 0, len(history)+1)
|
||||
if systemPrompt != "" {
|
||||
rendered = append(rendered, map[string]interface{}{"role": "system", "content": systemPrompt})
|
||||
}
|
||||
rendered = append(rendered, history...)
|
||||
|
||||
userContent := ""
|
||||
if userBlock != nil {
|
||||
userContent, _ = userBlock.Content.(string)
|
||||
}
|
||||
return &Capsule{
|
||||
TokenCount: totalTokens,
|
||||
Blocks: capsuleBlocks,
|
||||
Messages: rendered,
|
||||
SystemPreamble: systemPrompt,
|
||||
UserContent: userContent,
|
||||
}
|
||||
}
|
||||
|
||||
func deepCopyMap(m map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(m))
|
||||
for k, v := range m {
|
||||
if vm, ok := v.(map[string]interface{}); ok {
|
||||
out[k] = deepCopyMap(vm)
|
||||
} else if vl, ok := v.([]interface{}); ok {
|
||||
out[k] = deepCopyList(vl)
|
||||
} else {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func deepCopyList(l []interface{}) []interface{} {
|
||||
out := make([]interface{}, len(l))
|
||||
for i, v := range l {
|
||||
if vm, ok := v.(map[string]interface{}); ok {
|
||||
out[i] = deepCopyMap(vm)
|
||||
} else if vl, ok := v.([]interface{}); ok {
|
||||
out[i] = deepCopyList(vl)
|
||||
} else {
|
||||
out[i] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func blockNames(blocks []*ContributionBlock) []string {
|
||||
out := make([]string, 0, len(blocks))
|
||||
for _, b := range blocks {
|
||||
out = append(out, b.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildMemorySelector — the memory-selection authority composition
|
||||
// (_build_memory_selector, P1-03 #26). Fail-closed: any construction error
|
||||
// returns nil and the memory contributor abstains — never a fallback
|
||||
// injection. The selector itself stays Python (rune.memory_selector).
|
||||
func BuildMemorySelector(ctor func() (interface{}, error)) interface{} {
|
||||
sel, err := ctor()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return sel
|
||||
}
|
||||
+708
-13
@@ -1,20 +1,61 @@
|
||||
// Package main — context-broker: the GO port of src/rune/context_broker.py's
|
||||
// budget/estimator kernels (Oracle-Battery Parity Pattern, #2479 GO batch 38).
|
||||
// budget/estimator kernels (Oracle-Battery Parity Pattern, #2479 GO batch 38;
|
||||
// gap-close dichotomy wave — factory-gap-ctxbroker).
|
||||
// The ContextBroker itself stays Python (it's the orchestration authority);
|
||||
// the budget math is the deterministic core.
|
||||
// the budget math is the deterministic core. The gap-close ports the missing
|
||||
// functions name-preserving: the _py_* parity-floor oracles, the discovered-
|
||||
// window registry, the rolling-summary store, _through_id, log_calibration,
|
||||
// and the compact_history/summarize_history state machines with IO behind
|
||||
// injectable seams (Estimator/Summarizer/Discoverer funcs, Archiver writer).
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// defaultEstimator — chars/4 (code-point parity with Python len()).
|
||||
func defaultEstimator(text string) int { return len([]rune(text)) / 4 }
|
||||
// ── estimator / budget kernels ──────────────────────────────────────────────
|
||||
|
||||
// defaultBudget — window - max(2048, window/4) - window/20, floored at 1.
|
||||
func defaultBudget(window int) int {
|
||||
// DefaultEstimator — chars/4 (code-point parity with Python len()).
|
||||
func DefaultEstimator(text string) int { return len([]rune(text)) / 4 }
|
||||
|
||||
// PyDefaultEstimator — the Python parity-floor oracle (_py_default_estimator).
|
||||
// Python: len(text or "") // 4 — None coerces to "".
|
||||
func PyDefaultEstimator(text *string) int {
|
||||
if text == nil {
|
||||
return 0
|
||||
}
|
||||
return len([]rune(*text)) / 4
|
||||
}
|
||||
|
||||
// CcCore — the cognition-core availability seam (_cc_core). The Go port IS
|
||||
// the core, so the Python-pin question collapses: RUNE_COGNITION_CORE=python
|
||||
// pins the Python floor (the caller compares against it), anything else with
|
||||
// the Go core linked is available. Env read is injectable via CcCoreEnv.
|
||||
var CcCoreEnv = os.LookupEnv
|
||||
|
||||
// CcCore reports whether the native core would be used under the given env.
|
||||
// Mirrors the oracle's branch shape: pin -> None; loader probe failure -> None.
|
||||
func CcCore(lookup func(string) (string, bool), loaderAvailable bool) *bool {
|
||||
if v, ok := lookup("RUNE_COGNITION_CORE"); ok && v == "python" {
|
||||
return nil
|
||||
}
|
||||
if !loaderAvailable {
|
||||
return nil
|
||||
}
|
||||
avail := true
|
||||
return &avail
|
||||
}
|
||||
|
||||
// DefaultBudget — window - max(2048, window/4) - window/20, floored at 1.
|
||||
func DefaultBudget(window int) int {
|
||||
reserve := window / 4
|
||||
if reserve < 2048 {
|
||||
reserve = 2048
|
||||
@@ -26,10 +67,13 @@ func defaultBudget(window int) int {
|
||||
return r
|
||||
}
|
||||
|
||||
// PyDefaultBudget — the Python parity-floor oracle (_py_default_budget).
|
||||
func PyDefaultBudget(window int) int { return DefaultBudget(window) }
|
||||
|
||||
const MemoryBudgetCap = 1200
|
||||
|
||||
// defaultMemoryBudget — min(cap, window/8); window<=0 → CAP.
|
||||
func defaultMemoryBudget(window int) int {
|
||||
// DefaultMemoryBudget — min(cap, window/8); window<=0 → CAP.
|
||||
func DefaultMemoryBudget(window int) int {
|
||||
if window <= 0 {
|
||||
return MemoryBudgetCap
|
||||
}
|
||||
@@ -40,6 +84,536 @@ func defaultMemoryBudget(window int) int {
|
||||
return r
|
||||
}
|
||||
|
||||
// PyDefaultMemoryBudget — the Python parity-floor oracle
|
||||
// (_py_default_memory_budget). Python: `if not window or window <= 0`.
|
||||
func PyDefaultMemoryBudget(window int) int { return DefaultMemoryBudget(window) }
|
||||
|
||||
// PyMemoryDisabledValue — the Python parity-floor oracle over the RAW string
|
||||
// (_py_memory_disabled_value): truthy unless in the falsy set.
|
||||
func PyMemoryDisabledValue(value string) bool {
|
||||
switch value {
|
||||
case "", "0", "false", "no", "off":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MemoryDisabled — the full gate (memory_disabled): Python cross-check ANDed
|
||||
// with the native verdict when the core is live.
|
||||
func MemoryDisabled(raw string, coreDisabled bool) bool {
|
||||
gate := PyMemoryDisabledValue(raw)
|
||||
return coreDisabled && gate
|
||||
}
|
||||
|
||||
// ── discovered-window registry (reset_discovered_windows/register_window/
|
||||
// default_window) — process-local registry, live discovery injectable ────
|
||||
|
||||
const FallbackWindow = 8192
|
||||
|
||||
var (
|
||||
discoveredMu sync.Mutex
|
||||
discovered = map[string]int{}
|
||||
)
|
||||
|
||||
// DiscoverWindow is the injectable seam for live context-length discovery
|
||||
// (the oracle imports rune.local_tool_runtime.context_length). nil = no
|
||||
// discovery available (offline), which freezes the fallback branch.
|
||||
var DiscoverWindow func(modelID string) int
|
||||
|
||||
// ResetDiscoveredWindows clears the discovered-window registry (I3).
|
||||
func ResetDiscoveredWindows() {
|
||||
discoveredMu.Lock()
|
||||
defer discoveredMu.Unlock()
|
||||
discovered = map[string]int{}
|
||||
}
|
||||
|
||||
// RegisterWindow records a discovered context length for modelID (Task 2).
|
||||
func RegisterWindow(modelID string, window int) {
|
||||
discoveredMu.Lock()
|
||||
defer discoveredMu.Unlock()
|
||||
discovered[modelID] = window
|
||||
}
|
||||
|
||||
// DefaultWindow resolves the model window via the registry, then live
|
||||
// discovery, then the conservative fallback. The fallback (8192) never
|
||||
// enters the cache (C3: a transient discovery failure must not poison the
|
||||
// window for the process lifetime).
|
||||
func DefaultWindow(modelID string) int {
|
||||
discoveredMu.Lock()
|
||||
d := DiscoverWindow
|
||||
discoveredMu.Unlock()
|
||||
return resolveWindow(modelID, d)
|
||||
}
|
||||
|
||||
// resolveWindow — the discovery-injectable core of DefaultWindow; the
|
||||
// discover func is captured per call so concurrent resolvers never swap a
|
||||
// shared global.
|
||||
func resolveWindow(modelID string, discover func(string) int) int {
|
||||
if modelID == "" {
|
||||
return FallbackWindow
|
||||
}
|
||||
discoveredMu.Lock()
|
||||
w, ok := discovered[modelID]
|
||||
discoveredMu.Unlock()
|
||||
if ok {
|
||||
return w
|
||||
}
|
||||
if discover == nil {
|
||||
return FallbackWindow
|
||||
}
|
||||
window := discover(modelID)
|
||||
if window != 0 && window != FallbackWindow {
|
||||
RegisterWindow(modelID, window)
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
// ── rolling-summary store (C1/#141) ─────────────────────────────────────────
|
||||
|
||||
// RollingState — the persisted rolling summary for one session key.
|
||||
type RollingState struct {
|
||||
Summary string `json:"summary"`
|
||||
ThroughCount int `json:"through_count"`
|
||||
ThroughID string `json:"through_id"`
|
||||
}
|
||||
|
||||
var (
|
||||
rollingMu sync.Mutex
|
||||
rolling = map[string]*RollingState{} // key: source + "\x00" + session_id
|
||||
)
|
||||
|
||||
func rollingKey(source, sessionID string) string { return source + "\x00" + sessionID }
|
||||
|
||||
// RollingSummaryState returns the persisted rolling summary for a session
|
||||
// (deep-copied), or nil.
|
||||
func RollingSummaryState(source, sessionID string) *RollingState {
|
||||
rollingMu.Lock()
|
||||
defer rollingMu.Unlock()
|
||||
st, ok := rolling[rollingKey(source, sessionID)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cp := *st
|
||||
return &cp
|
||||
}
|
||||
|
||||
// StoreRollingSummary persists a rolling summary keyed by (source, sessionID).
|
||||
func StoreRollingSummary(source, sessionID, summaryText string, throughCount int, throughID string) {
|
||||
rollingMu.Lock()
|
||||
defer rollingMu.Unlock()
|
||||
rolling[rollingKey(source, sessionID)] = &RollingState{
|
||||
Summary: summaryText,
|
||||
ThroughCount: throughCount,
|
||||
ThroughID: throughID,
|
||||
}
|
||||
}
|
||||
|
||||
// EvictRolling — LRU (insertion-order) eviction for dead sessions; returns
|
||||
// the evicted count.
|
||||
func EvictRolling(maxSessions int) int {
|
||||
rollingMu.Lock()
|
||||
defer rollingMu.Unlock()
|
||||
if len(rolling) <= maxSessions {
|
||||
return 0
|
||||
}
|
||||
keys := make([]string, 0, len(rolling))
|
||||
for k := range rolling {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys) // deterministic order (Python relies on dict insertion order)
|
||||
drop := len(rolling) - maxSessions
|
||||
for _, k := range keys[:drop] {
|
||||
delete(rolling, k)
|
||||
}
|
||||
return drop
|
||||
}
|
||||
|
||||
// ThroughID — identity of the coverage boundary: first 16 hex chars of the
|
||||
// sha256 of the first message BEYOND coverage (content, not position —
|
||||
// rotation-proof, #141). Empty when the boundary is outside the list.
|
||||
func ThroughID(messages []map[string]interface{}, throughCount int) string {
|
||||
if throughCount < 0 || throughCount >= len(messages) {
|
||||
return ""
|
||||
}
|
||||
anchor := messages[throughCount]
|
||||
sum := sha256.Sum256([]byte(pyDumpsSorted(anchor)))
|
||||
return fmt.Sprintf("%x", sum[:])[:16]
|
||||
}
|
||||
|
||||
// pyDumpsSorted renders json.dumps(anchor, sort_keys=True) byte-for-byte:
|
||||
// separators ", " / ": ", ensure_ascii escaping, Python literal literals.
|
||||
func pyDumpsSorted(v interface{}) string {
|
||||
var b strings.Builder
|
||||
writePyJSON(&b, v)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writePyJSON(b *strings.Builder, v interface{}) {
|
||||
switch val := v.(type) {
|
||||
case nil:
|
||||
b.WriteString("null")
|
||||
case bool:
|
||||
if val {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
case float64:
|
||||
b.WriteString(pyFloat(val))
|
||||
case int:
|
||||
b.WriteString(fmt.Sprintf("%d", val))
|
||||
case string:
|
||||
writePyString(b, val)
|
||||
case []interface{}:
|
||||
b.WriteString("[")
|
||||
for i, item := range val {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePyJSON(b, item)
|
||||
}
|
||||
b.WriteString("]")
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(val))
|
||||
for k := range val {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
b.WriteString("{")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePyString(b, k)
|
||||
b.WriteString(": ")
|
||||
writePyJSON(b, val[k])
|
||||
}
|
||||
b.WriteString("}")
|
||||
default:
|
||||
blob, _ := json.Marshal(val)
|
||||
b.Write(blob)
|
||||
}
|
||||
}
|
||||
|
||||
func pyFloat(f float64) string {
|
||||
if f == float64(int64(f)) && math.Abs(f) < 1e15 {
|
||||
// integral numbers via JSON can only originate as Python ints —
|
||||
// json.dumps renders int(3) as "3", never "3.0"
|
||||
return fmt.Sprintf("%d", int64(f))
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
func writePyString(b *strings.Builder, s string) {
|
||||
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")
|
||||
case '\b':
|
||||
b.WriteString("\\b")
|
||||
case '\f':
|
||||
b.WriteString("\\f")
|
||||
default:
|
||||
if r < 0x20 || r > 0x7e {
|
||||
if r > 0xffff {
|
||||
r2 := r - 0x10000
|
||||
hi := 0xd800 + (r2 >> 10)
|
||||
lo := 0xdc00 + (r2 & 0x3ff)
|
||||
fmt.Fprintf(b, "\\u%04x\\u%04x", hi, lo)
|
||||
} else {
|
||||
fmt.Fprintf(b, "\\u%04x", r)
|
||||
}
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
// LogCalibration — compare the broker estimate with the served
|
||||
// prompt_eval_count. LOG ONLY (drift never feeds admission). Returns the
|
||||
// signed drift; 0 when nothing was served.
|
||||
func LogCalibration(measuredTokens int, promptEvalCount *int, model string) int {
|
||||
if promptEvalCount == nil || *promptEvalCount == 0 {
|
||||
return 0
|
||||
}
|
||||
return *promptEvalCount - measuredTokens
|
||||
}
|
||||
|
||||
// ── compaction state machine (compact_history) ──────────────────────────────
|
||||
|
||||
// Compaction thresholds (mirrors the oracle constants).
|
||||
const (
|
||||
CompactionThreshold = 0.8
|
||||
CompactionKeepTurns = 5
|
||||
CompactionNumPredict = 256
|
||||
)
|
||||
|
||||
// Message — one conversation turn (loosely typed like the Python dict).
|
||||
type Message = map[string]interface{}
|
||||
|
||||
// Archiver is the injectable seam for the raw-history archive append
|
||||
// (oracle: <rune_home>/state/history_archive.jsonl). Returns an error to
|
||||
// exercise the T2 archive-failure branch.
|
||||
type Archiver func(record map[string]interface{}) error
|
||||
|
||||
// Summarizer is the injectable seam for the rolling-summary call
|
||||
// (oracle: summarize_history via OllamaChatTransport). An error is a T2
|
||||
// storage failure: compaction is skipped, the caller degrades.
|
||||
type Summarizer func(messages []Message) (string, error)
|
||||
|
||||
// CompactEvent — the compaction event dict returned alongside the compacted
|
||||
// messages.
|
||||
type CompactEvent struct {
|
||||
Triggered bool `json:"triggered"`
|
||||
TokensBefore int `json:"tokens_before"`
|
||||
TokensAfter int `json:"tokens_after"`
|
||||
TurnsSummarized int `json:"turns_summarized"`
|
||||
Archived bool `json:"archived"`
|
||||
Failed bool `json:"failed"`
|
||||
Source string `json:"source"`
|
||||
Persisted bool `json:"persisted"`
|
||||
DegradedToTail *int `json:"degraded_to_tail,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// CompactHistory — archive-then-compress state machine, logic 1:1 with the
|
||||
// oracle (rolling persistence, #141 identity rematch, C2 tail degrade).
|
||||
// source is fixed "operator" by the assemble path; sessionID "" disables
|
||||
// persistence. archivePath is the injected archive location (the oracle's
|
||||
// Path parameter): the label is embedded in the summary block; "" renders
|
||||
// the oracle's 'external storage' fallback.
|
||||
func CompactHistory(messages []Message, window int, archivePath string, archiver Archiver, summarizer Summarizer, source, sessionID string) ([]Message, *CompactEvent) {
|
||||
tokensBefore := 0
|
||||
for _, m := range messages {
|
||||
tokensBefore += DefaultEstimator(messageContent(m))
|
||||
}
|
||||
event := &CompactEvent{
|
||||
TokensBefore: tokensBefore,
|
||||
TokensAfter: tokensBefore,
|
||||
Source: source,
|
||||
}
|
||||
if len(messages) == 0 {
|
||||
return messages, event
|
||||
}
|
||||
|
||||
totalTurns := 0
|
||||
for _, m := range messages {
|
||||
if role, _ := m["role"].(string); role == "user" {
|
||||
totalTurns++
|
||||
}
|
||||
}
|
||||
if totalTurns <= CompactionKeepTurns {
|
||||
return messages, event
|
||||
}
|
||||
|
||||
keepMessages := CompactionKeepTurns * 2
|
||||
split := len(messages) - keepMessages
|
||||
if split < 0 {
|
||||
split = 0
|
||||
}
|
||||
older, recent := messages[:split], messages[split:]
|
||||
|
||||
// C1 rolling persistence + #141 identity rematch.
|
||||
sessionKey := sessionID != ""
|
||||
var prior *RollingState
|
||||
if sessionKey {
|
||||
prior = RollingSummaryState(source, sessionID)
|
||||
}
|
||||
through := 0
|
||||
if prior != nil {
|
||||
through = prior.ThroughCount
|
||||
if prior.ThroughID != "" {
|
||||
var anchor Message
|
||||
if 0 <= through && through < len(older) {
|
||||
anchor = older[through]
|
||||
}
|
||||
if anchor != nil && ThroughID(older, through) != prior.ThroughID {
|
||||
// window rotated under the stored count: rematch by content
|
||||
found := -1
|
||||
for i := range older {
|
||||
if ThroughID(older, i) == prior.ThroughID {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if found >= 0 {
|
||||
through = found
|
||||
} else {
|
||||
prior = nil
|
||||
through = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newOlder, archiveMessages []Message
|
||||
if prior != nil && through <= len(older) {
|
||||
newOlder = older[through:]
|
||||
if len(newOlder) == 0 {
|
||||
// Nothing new beyond the rolled summary: reuse it verbatim.
|
||||
summaryBlock := Message{
|
||||
"role": "user",
|
||||
"content": "## Conversation summary (rolling)\n" + prior.Summary +
|
||||
"\n(Earlier turns were archived; recent turns verbatim.)",
|
||||
}
|
||||
compacted := append([]Message{summaryBlock}, recent...)
|
||||
event.Triggered = true
|
||||
event.TokensAfter = sumTokens(compacted)
|
||||
event.TurnsSummarized = 0
|
||||
event.Summary = prior.Summary
|
||||
event.Persisted = true
|
||||
return compacted, event
|
||||
}
|
||||
archiveMessages = newOlder
|
||||
} else {
|
||||
newOlder = older
|
||||
if prior == nil {
|
||||
archiveMessages = messages
|
||||
} else if through <= len(older) {
|
||||
archiveMessages = older[through:]
|
||||
} else {
|
||||
archiveMessages = older
|
||||
}
|
||||
if prior != nil && through > len(older) {
|
||||
// History shrank (ledger rotation): summarize from scratch.
|
||||
prior = nil
|
||||
through = 0
|
||||
archiveMessages = messages
|
||||
}
|
||||
}
|
||||
|
||||
// ARCHIVE FIRST — raw, verbatim, before any lossy compression.
|
||||
if archiver != nil {
|
||||
record := map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
"source": source,
|
||||
"messages": archiveMessages,
|
||||
}
|
||||
if err := archiver(record); err != nil {
|
||||
event.Failed = true
|
||||
return messages, event
|
||||
}
|
||||
event.Archived = true
|
||||
}
|
||||
|
||||
var summaryText string
|
||||
{
|
||||
var input []Message
|
||||
if prior != nil {
|
||||
input = append([]Message{{"role": "user", "content": "Previous rolling summary:\n" + prior.Summary}}, newOlder...)
|
||||
} else {
|
||||
input = older
|
||||
}
|
||||
text, err := summarizer(input)
|
||||
if err != nil {
|
||||
event.Failed = true
|
||||
// C2: degrade to the newest tail that fits the window budget.
|
||||
tailBudget := DefaultBudget(window)
|
||||
var tail []Message
|
||||
used := 0
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
message := messages[i]
|
||||
cost := DefaultEstimator(messageContent(message))
|
||||
if used+cost > tailBudget && len(tail) > 0 {
|
||||
break
|
||||
}
|
||||
tail = append([]Message{message}, tail...)
|
||||
used += cost
|
||||
}
|
||||
event.DegradedToTail = &[]int{len(tail)}[0]
|
||||
return tail, event
|
||||
}
|
||||
summaryText = strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
archiveLabel := "external storage"
|
||||
if archivePath != "" {
|
||||
archiveLabel = archivePath
|
||||
}
|
||||
summaryBlock := Message{
|
||||
"role": "user",
|
||||
"content": "## Conversation summary (rolling)\n" + summaryText +
|
||||
"\n(Earlier turns were archived to " + archiveLabel + "; " +
|
||||
fmt.Sprintf("%d", len(older)) + " messages summarized, recent turns verbatim.)",
|
||||
}
|
||||
compacted := append([]Message{summaryBlock}, recent...)
|
||||
if sessionKey {
|
||||
newThrough := through + len(newOlder)
|
||||
StoreRollingSummary(source, sessionID, summaryText, newThrough, ThroughID(messages, newThrough))
|
||||
}
|
||||
event.Triggered = true
|
||||
event.TokensAfter = sumTokens(compacted)
|
||||
event.TurnsSummarized = len(newOlder)
|
||||
event.Summary = summaryText
|
||||
event.Persisted = sessionKey
|
||||
return compacted, event
|
||||
}
|
||||
|
||||
func messageContent(m Message) string {
|
||||
c, _ := m["content"].(string)
|
||||
return c
|
||||
}
|
||||
|
||||
func sumTokens(messages []Message) int {
|
||||
total := 0
|
||||
for _, m := range messages {
|
||||
total += DefaultEstimator(messageContent(m))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// ── summarize_history transport seam ────────────────────────────────────────
|
||||
|
||||
// SummarizeHistory builds the structured rolling-summary prompt and calls the
|
||||
// injected transport. The transport (OllamaChatTransport in the oracle) is
|
||||
// IO; here it is injectable. Empty/blank content raises (caller degrades).
|
||||
func SummarizeHistory(messages []Message, model string, numPredict int, numCtx int, transport func(prompt, model string, options map[string]interface{}) (string, error)) (string, error) {
|
||||
parts := make([]string, 0, len(messages))
|
||||
for _, m := range messages {
|
||||
role := "?"
|
||||
if r, ok := m["role"].(string); ok && r != "" {
|
||||
role = r
|
||||
}
|
||||
parts = append(parts, role+": "+messageContent(m))
|
||||
}
|
||||
transcript := strings.Join(parts, "\n")
|
||||
prompt := "Summarize this conversation segment into a rolling summary.\n" +
|
||||
"Answer with exactly these four labeled sections:\n" +
|
||||
"Session intent: <what this session is trying to accomplish>\n" +
|
||||
"Decisions: <choices made so far>\n" +
|
||||
"Open items: <unresolved threads>\n" +
|
||||
"Next steps: <what to do next>\n\n" +
|
||||
"Conversation:\n" + transcript
|
||||
options := map[string]interface{}{
|
||||
"temperature": 0.2,
|
||||
"num_predict": numPredict,
|
||||
}
|
||||
if numCtx != 0 {
|
||||
options["num_ctx"] = numCtx
|
||||
}
|
||||
content, err := transport(prompt, model, options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return "", fmt.Errorf("summarizer returned empty content")
|
||||
}
|
||||
return strings.TrimSpace(content), nil
|
||||
}
|
||||
|
||||
// ── replay CLI (legacy truth.json flat-case driver, kept green) ─────────────
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: context-broker replay <truth.json>")
|
||||
@@ -52,8 +626,18 @@ func main() {
|
||||
}
|
||||
var cases []map[string]any
|
||||
if err := json.Unmarshal(raw, &cases); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
// New-format truth doc ({frozen_ts, generator, rows}) — the legacy
|
||||
// flat ops ride inside row["input"]["ops"] / scalar cases; nothing to
|
||||
// replay here, the battery test covers them.
|
||||
var doc struct {
|
||||
Rows []json.RawMessage `json:"rows"`
|
||||
}
|
||||
if err2 := json.Unmarshal(raw, &doc); err2 != nil || len(doc.Rows) == 0 {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d battery rows (go test battery replays them)\n", len(doc.Rows))
|
||||
return
|
||||
}
|
||||
fails := 0
|
||||
for i, c := range cases {
|
||||
@@ -62,19 +646,19 @@ func main() {
|
||||
switch op {
|
||||
case "estimator":
|
||||
text, _ := c["text"].(string)
|
||||
if got := defaultEstimator(text); got != want {
|
||||
if got := DefaultEstimator(text); got != want {
|
||||
fails++
|
||||
fmt.Printf("case %d estimator %q: got %d want %d\n", i, text, got, want)
|
||||
}
|
||||
case "budget":
|
||||
w := int(c["window"].(float64))
|
||||
if got := defaultBudget(w); got != want {
|
||||
if got := DefaultBudget(w); got != want {
|
||||
fails++
|
||||
fmt.Printf("case %d budget %d: got %d want %d\n", i, w, got, want)
|
||||
}
|
||||
case "mem_budget":
|
||||
w := int(c["window"].(float64))
|
||||
if got := defaultMemoryBudget(w); got != want {
|
||||
if got := DefaultMemoryBudget(w); got != want {
|
||||
fails++
|
||||
fmt.Printf("case %d mem_budget %d: got %d want %d\n", i, w, got, want)
|
||||
}
|
||||
@@ -86,3 +670,114 @@ func main() {
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases vs the Python oracle\n", len(cases))
|
||||
}
|
||||
|
||||
// ── eval CLI (live-parity harness: one JSON payload on stdin → JSON out) ────
|
||||
|
||||
func init() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&payload); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
out := evalOne(payload)
|
||||
blob, _ := json.Marshal(out)
|
||||
fmt.Println(string(blob))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func evalOne(p map[string]interface{}) interface{} {
|
||||
op, _ := p["op"].(string)
|
||||
switch op {
|
||||
case "est":
|
||||
if t, ok := p["text"]; ok {
|
||||
s, _ := t.(string)
|
||||
return PyDefaultEstimator(&s)
|
||||
}
|
||||
return PyDefaultEstimator(nil)
|
||||
case "budget":
|
||||
return PyDefaultBudget(int(p["window"].(float64)))
|
||||
case "membud":
|
||||
return PyDefaultMemoryBudget(int(p["window"].(float64)))
|
||||
case "memoff":
|
||||
v, _ := p["value"].(string)
|
||||
return PyMemoryDisabledValue(v)
|
||||
case "through":
|
||||
rawMsgs, _ := p["messages"].([]interface{})
|
||||
msgs := make([]Message, 0, len(rawMsgs))
|
||||
for _, rm := range rawMsgs {
|
||||
m, _ := rm.(map[string]interface{})
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
return ThroughID(msgs, int(p["through"].(float64)))
|
||||
case "admit":
|
||||
rawEntries, _ := p["entries"].([]interface{})
|
||||
entries := make([]AdmissionEntry, 0, len(rawEntries))
|
||||
for _, re := range rawEntries {
|
||||
e, _ := re.(map[string]interface{})
|
||||
b := &ContributionBlock{}
|
||||
b.Name, _ = e["name"].(string)
|
||||
b.Priority = int(e["priority"].(float64))
|
||||
b.Tokens = int(e["tokens"].(float64))
|
||||
b.Source, _ = e["source"].(string)
|
||||
b.Evictable, _ = e["evictable"].(bool)
|
||||
entries = append(entries, AdmissionEntry{Name: b.Name, Block: b})
|
||||
}
|
||||
budget := int(p["budget"].(float64))
|
||||
memBudget := int(p["memory_budget"].(float64))
|
||||
res, err := Admit(entries, budget, memBudget)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": "ContextOverflowError", "detail": err.Error()}
|
||||
}
|
||||
blocksOut := make([]map[string]interface{}, 0, len(res.Blocks))
|
||||
for _, r := range res.Blocks {
|
||||
blocksOut = append(blocksOut, map[string]interface{}{
|
||||
"name": r.Name, "priority": r.Priority, "tokens": r.Tokens,
|
||||
"source": r.Source, "evictable": r.Evictable,
|
||||
"admitted": r.Admitted, "reason": r.Reason,
|
||||
})
|
||||
}
|
||||
dropped := make([]string, 0, len(res.Dropped))
|
||||
for _, d := range res.Dropped {
|
||||
dropped = append(dropped, d.Name)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"admitted": blockNames(res.Admitted), "blocks": blocksOut,
|
||||
"admitted_total": res.AdmittedTotal, "dropped": dropped,
|
||||
"memory_budget": res.MemoryBudget, "memory_used": res.MemoryUsed,
|
||||
}
|
||||
case "render":
|
||||
rawBlocks, _ := p["blocks"].([]interface{})
|
||||
admitted := make([]*ContributionBlock, 0, len(rawBlocks))
|
||||
for _, rb := range rawBlocks {
|
||||
b, _ := rb.(map[string]interface{})
|
||||
blk := &ContributionBlock{}
|
||||
blk.Name, _ = b["name"].(string)
|
||||
blk.Priority = int(b["priority"].(float64))
|
||||
blk.Tokens = int(b["tokens"].(float64))
|
||||
blk.Source, _ = b["source"].(string)
|
||||
switch c := b["content"].(type) {
|
||||
case string:
|
||||
blk.Content = c
|
||||
case []interface{}:
|
||||
msgList := make([]Message, 0, len(c))
|
||||
for _, rm := range c {
|
||||
m, _ := rm.(map[string]interface{})
|
||||
msgList = append(msgList, m)
|
||||
}
|
||||
blk.Content = msgList
|
||||
}
|
||||
admitted = append(admitted, blk)
|
||||
}
|
||||
capsule := Render(admitted)
|
||||
return map[string]interface{}{
|
||||
"token_count": capsule.TokenCount,
|
||||
"messages": capsule.Messages,
|
||||
"system_preamble": capsule.SystemPreamble,
|
||||
"user_content": capsule.UserContent,
|
||||
"blocks": blockNames(capsule.Blocks),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
// Phase B language-advantage pass — dichotomy wave. Three Go-side wins the
|
||||
// Python oracle can't express:
|
||||
//
|
||||
// 1. Concurrent window discovery + rolling-store access under -race: the
|
||||
// broker's registries are mutex-guarded; Go proves thread-safety that
|
||||
// the Python module-level dicts only get from the GIL.
|
||||
// 2. EstimateAll — batch token estimation fans out across goroutines and
|
||||
// reassembles in input order (the estimator is pure, so the batch is
|
||||
// embarrassingly parallel; the Python loop is serial).
|
||||
// 3. A typed BlockSource interface + name-keyed admission registry with
|
||||
// compile-time satisfaction assertions (Python dispatches through
|
||||
// isinstance checks; Go turns the dispatch into data).
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ── 1. concurrent registries ────────────────────────────────────────────────
|
||||
|
||||
// DiscoverWindowsConcurrent resolves several models' windows in parallel.
|
||||
// Each goroutine captures its own discover func (no shared-global swap);
|
||||
// registry writes stay serialized by the same mutex the single-call path
|
||||
// uses; results reassemble in input order regardless of discovery latency.
|
||||
func DiscoverWindowsConcurrent(modelIDs []string, discover func(string) int) []int {
|
||||
out := make([]int, len(modelIDs))
|
||||
var wg sync.WaitGroup
|
||||
for i, id := range modelIDs {
|
||||
wg.Add(1)
|
||||
go func(i int, id string) {
|
||||
defer wg.Done()
|
||||
out[i] = resolveWindow(id, discover)
|
||||
}(i, id)
|
||||
}
|
||||
wg.Wait()
|
||||
return out
|
||||
}
|
||||
|
||||
// RollingKeys — a consistent snapshot of every persisted session key
|
||||
// (source + session), sorted. Reads under the rolling lock.
|
||||
func RollingKeys() []string {
|
||||
rollingMu.Lock()
|
||||
defer rollingMu.Unlock()
|
||||
keys := make([]string, 0, len(rolling))
|
||||
for k := range rolling {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// ── 2. parallel batch estimation ────────────────────────────────────────────
|
||||
|
||||
// EstimateAll estimates many texts concurrently, reassembled in input
|
||||
// order. Pure function per element → embarrassingly parallel.
|
||||
func EstimateAll(texts []string, estimator func(string) int) []int {
|
||||
if estimator == nil {
|
||||
estimator = DefaultEstimator
|
||||
}
|
||||
out := make([]int, len(texts))
|
||||
var wg sync.WaitGroup
|
||||
for i, t := range texts {
|
||||
wg.Add(1)
|
||||
go func(i int, t string) {
|
||||
defer wg.Done()
|
||||
out[i] = estimator(t)
|
||||
}(i, t)
|
||||
}
|
||||
wg.Wait()
|
||||
return out
|
||||
}
|
||||
|
||||
// ── 3. typed admission registry ─────────────────────────────────────────────
|
||||
|
||||
// BlockSource — a named producer of contribution blocks (the typed shape of
|
||||
// a registered contributor).
|
||||
type BlockSource interface {
|
||||
BlockName() string
|
||||
Block(task, session map[string]interface{}) *ContributionBlock
|
||||
}
|
||||
|
||||
// BlockSourceFunc — adapter turning a plain function into a BlockSource.
|
||||
type BlockSourceFunc struct {
|
||||
name string
|
||||
fn ContributorFunc
|
||||
}
|
||||
|
||||
// BlockName implements BlockSource.
|
||||
func (f BlockSourceFunc) BlockName() string { return f.name }
|
||||
|
||||
// Block implements BlockSource.
|
||||
func (f BlockSourceFunc) Block(task, session map[string]interface{}) *ContributionBlock {
|
||||
return f.fn(task, session)
|
||||
}
|
||||
|
||||
// sourceRegistry — compile-time assertion: every registry entry satisfies
|
||||
// BlockSource (catches signature drift at build time).
|
||||
var _ = func() bool {
|
||||
var registry = map[string]BlockSourceFunc{}
|
||||
for _, v := range registry {
|
||||
var _ BlockSource = v
|
||||
}
|
||||
return true
|
||||
}()
|
||||
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
// Phase B battery — the language-advantage wins exercised, including under
|
||||
// the race detector (go test -race).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEstimateAllMatchesSerial(t *testing.T) {
|
||||
texts := make([]string, 0, 500)
|
||||
for i := 0; i < 500; i++ {
|
||||
texts = append(texts, fmt.Sprintf("token estimate batch %d %v", i, string(rune('a'+i%26))))
|
||||
}
|
||||
serial := make([]int, len(texts))
|
||||
for i, s := range texts {
|
||||
serial[i] = DefaultEstimator(s)
|
||||
}
|
||||
parallel := EstimateAll(texts, nil)
|
||||
for i := range serial {
|
||||
if serial[i] != parallel[i] {
|
||||
t.Fatalf("EstimateAll[%d]: got %d want %d", i, parallel[i], serial[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentWindowRegistryRaceFree(t *testing.T) {
|
||||
ResetDiscoveredWindows()
|
||||
models := make([]string, 0, 50)
|
||||
for i := 0; i < 50; i++ {
|
||||
models = append(models, fmt.Sprintf("model-%d", i))
|
||||
}
|
||||
got := DiscoverWindowsConcurrent(models, func(id string) int {
|
||||
RegisterWindow(id, 32768)
|
||||
return 32768
|
||||
})
|
||||
for i, w := range got {
|
||||
if w != 32768 {
|
||||
t.Fatalf("window[%d]=%d want 32768", i, w)
|
||||
}
|
||||
}
|
||||
// concurrent readers while writers mutate — the race detector's job
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
if i%2 == 0 {
|
||||
RegisterWindow(fmt.Sprintf("race-%d", i), 8192)
|
||||
} else {
|
||||
_ = DefaultWindow("model-0")
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
ResetDiscoveredWindows()
|
||||
}
|
||||
|
||||
func TestRollingStoreRaceFreeAndSnapshot(t *testing.T) {
|
||||
for k := range rolling {
|
||||
delete(rolling, k)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
StoreRollingSummary("operator", fmt.Sprintf("s%d", i), "S", i, "tid")
|
||||
_ = RollingSummaryState("operator", fmt.Sprintf("s%d", i))
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
keys := RollingKeys()
|
||||
if len(keys) != 20 {
|
||||
t.Fatalf("RollingKeys: got %d want 20", len(keys))
|
||||
}
|
||||
want := append([]string(nil), keys...)
|
||||
sort.Strings(want)
|
||||
for i := range keys {
|
||||
if keys[i] != want[i] {
|
||||
t.Fatalf("RollingKeys not sorted: [%d]=%q want %q", i, keys[i], want[i])
|
||||
}
|
||||
if keys[i] != fmt.Sprintf("operator\x00s%d", i) && !strings.HasPrefix(keys[i], "operator\x00s") {
|
||||
t.Fatalf("RollingKeys[%d]=%q unexpected key", i, keys[i])
|
||||
}
|
||||
}
|
||||
for k := range rolling {
|
||||
delete(rolling, k)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockSourceRegistryCompileTime(t *testing.T) {
|
||||
var src BlockSource = BlockSourceFunc{
|
||||
name: "tools",
|
||||
fn: func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
return &ContributionBlock{Name: "tools", Priority: 9, Content: "x", Tokens: 1, Source: "t"}
|
||||
},
|
||||
}
|
||||
blk := src.Block(nil, nil)
|
||||
if blk == nil || blk.Name != "tools" {
|
||||
t.Fatalf("BlockSource dispatch broken: %+v", blk)
|
||||
}
|
||||
if src.BlockName() != "tools" {
|
||||
t.Fatalf("BlockName: got %q", src.BlockName())
|
||||
}
|
||||
}
|
||||
+2118
-47
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,559 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate replaying the frozen oracle truth
|
||||
// (truth.json, generated against the LIVE Python oracle
|
||||
// src/rune/context_broker.py with RUNE_COGNITION_CORE=python pinned)
|
||||
// VERBATIM. Inputs are deep-frozen snapshots (the oracle mutates message
|
||||
// lists); every row byte-compares the Go result against the oracle's.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
Generator string `json:"generator"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Fn string `json:"fn"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Want interface{} `json:"want"`
|
||||
}
|
||||
|
||||
func loadTruth(t *testing.T) *TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth.json: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth.json contained no rows")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, r := range doc.Rows {
|
||||
if seen[r.Name] {
|
||||
t.Fatalf("duplicate frozen row %q", r.Name)
|
||||
}
|
||||
seen[r.Name] = true
|
||||
}
|
||||
return &doc
|
||||
}
|
||||
|
||||
func msgs(raw interface{}) []Message {
|
||||
list, _ := raw.([]interface{})
|
||||
out := make([]Message, 0, len(list))
|
||||
for _, item := range list {
|
||||
m, _ := item.(map[string]interface{})
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func asInt(v interface{}) int {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n)
|
||||
case bool:
|
||||
if n {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case string:
|
||||
var i int
|
||||
fmt.Sscanf(n, "%d", &i)
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func compEvent(e *CompactEvent) map[string]interface{} {
|
||||
blob, _ := json.Marshal(e)
|
||||
var out map[string]interface{}
|
||||
_ = json.Unmarshal(blob, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// runOne — replay one frozen row; returns "" on parity, else the divergence.
|
||||
func runOne(row *TruthRow) string {
|
||||
switch row.Fn {
|
||||
case "est":
|
||||
var textPtr *string
|
||||
if s, ok := row.Input["text"].(string); ok {
|
||||
textPtr = &s
|
||||
}
|
||||
got := PyDefaultEstimator(textPtr)
|
||||
want := asInt(row.Want)
|
||||
if got != want {
|
||||
return fmt.Sprintf("est: got %d want %d", got, want)
|
||||
}
|
||||
case "cc_core":
|
||||
env, _ := row.Input["env"].(map[string]interface{})
|
||||
lookup := func(string) (string, bool) { return "", false }
|
||||
if v, ok := env["RUNE_COGNITION_CORE"]; ok {
|
||||
s, _ := v.(string)
|
||||
lookup = func(string) (string, bool) { return s, true }
|
||||
}
|
||||
loaderAvail, _ := row.Input["loader_available"].(bool)
|
||||
got := CcCore(lookup, loaderAvail)
|
||||
if row.Want == nil && got != nil {
|
||||
return "cc_core: got core want none"
|
||||
}
|
||||
if row.Want != nil && got == nil {
|
||||
return "cc_core: got none want core"
|
||||
}
|
||||
case "budget":
|
||||
got := PyDefaultBudget(asInt(row.Input["window"]))
|
||||
want := asInt(row.Want)
|
||||
if got != want {
|
||||
return fmt.Sprintf("budget: got %d want %d", got, want)
|
||||
}
|
||||
case "membud":
|
||||
got := PyDefaultMemoryBudget(asInt(row.Input["window"]))
|
||||
want := asInt(row.Want)
|
||||
if got != want {
|
||||
return fmt.Sprintf("membud: got %d want %d", got, want)
|
||||
}
|
||||
case "memoff":
|
||||
v, _ := row.Input["value"].(string)
|
||||
got := PyMemoryDisabledValue(v)
|
||||
want, _ := row.Want.(bool)
|
||||
if got != want {
|
||||
return fmt.Sprintf("memoff %q: got %v want %v", v, got, want)
|
||||
}
|
||||
case "windows":
|
||||
ResetDiscoveredWindows()
|
||||
opsList, _ := row.Input["ops"].([]interface{})
|
||||
for _, rawOp := range opsList {
|
||||
op, _ := rawOp.(map[string]interface{})
|
||||
switch op["op"] {
|
||||
case "register_window":
|
||||
RegisterWindow(op["model_id"].(string), asInt(op["window"]))
|
||||
case "reset_discovered_windows":
|
||||
ResetDiscoveredWindows()
|
||||
case "default_window":
|
||||
mid := ""
|
||||
if s, ok := op["model_id"].(string); ok {
|
||||
mid = s
|
||||
}
|
||||
// Live discovery is IO; the Go seam stays offline (nil) so
|
||||
// the miss branch freezes to the fallback — exactly what the
|
||||
// oracle froze in the offline generator env.
|
||||
DiscoverWindow = nil
|
||||
got := DefaultWindow(mid)
|
||||
wantMap, _ := row.Want.(map[string]interface{})
|
||||
want := asInt(wantMap["last"])
|
||||
if got != want {
|
||||
return fmt.Sprintf("default_window %q: got %d want %d", mid, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "through":
|
||||
got := ThroughID(msgs(row.Input["messages"]), asInt(row.Input["through"]))
|
||||
want, _ := row.Want.(string)
|
||||
if got != want {
|
||||
return fmt.Sprintf("through: got %q want %q", got, want)
|
||||
}
|
||||
case "rstate":
|
||||
ResetDiscoveredWindows()
|
||||
for k := range rolling {
|
||||
delete(rolling, k)
|
||||
}
|
||||
opsList, _ := row.Input["ops"].([]interface{})
|
||||
var got *RollingState
|
||||
for _, rawOp := range opsList {
|
||||
op, _ := rawOp.(map[string]interface{})
|
||||
source, _ := op["source"].(string)
|
||||
sid, _ := op["sid"].(string)
|
||||
switch op["op"] {
|
||||
case "store":
|
||||
summary, _ := op["summary"].(string)
|
||||
tid, _ := op["tid"].(string)
|
||||
StoreRollingSummary(source, sid, summary, asInt(op["through"]), tid)
|
||||
case "read":
|
||||
got = RollingSummaryState(source, sid)
|
||||
}
|
||||
}
|
||||
if row.Want == nil {
|
||||
if got != nil {
|
||||
return "rstate: got state want nil"
|
||||
}
|
||||
} else {
|
||||
wantMap, _ := row.Want.(map[string]interface{})
|
||||
if got == nil {
|
||||
return "rstate: got nil want state"
|
||||
}
|
||||
if got.Summary != wantMap["summary"] ||
|
||||
got.ThroughCount != asInt(wantMap["through_count"]) ||
|
||||
got.ThroughID != wantMap["through_id"] {
|
||||
return fmt.Sprintf("rstate: got %+v want %v", got, wantMap)
|
||||
}
|
||||
}
|
||||
case "rstore":
|
||||
m := msgs(row.Input["msgs"])
|
||||
StoreRollingSummary("operator", "s2", "S2", 1, ThroughID(m, 1))
|
||||
got := RollingSummaryState("operator", "s2")
|
||||
wantMap, _ := row.Want.(map[string]interface{})
|
||||
if got == nil || got.Summary != wantMap["summary"] ||
|
||||
got.ThroughCount != asInt(wantMap["through_count"]) ||
|
||||
got.ThroughID != wantMap["through_id"] {
|
||||
return fmt.Sprintf("rstore: got %+v want %v", got, wantMap)
|
||||
}
|
||||
case "summarize":
|
||||
// The oracle froze the transport-failure branch (offline env): any
|
||||
// error type is acceptable — the Go seam reproduces the failure
|
||||
// shape (non-empty error, empty-content ValueError contract).
|
||||
_, err := SummarizeHistory(msgs(row.Input["messages"]), "offline", 256, 0,
|
||||
func(string, string, map[string]interface{}) (string, error) {
|
||||
return "", fmt.Errorf("HTTP Error 404: Not Found")
|
||||
})
|
||||
wantMap, _ := row.Want.(map[string]interface{})
|
||||
if _, isErr := wantMap["error"]; isErr && err == nil {
|
||||
return "summarize: got success want error"
|
||||
}
|
||||
if err == nil {
|
||||
return "summarize: expected frozen error branch"
|
||||
}
|
||||
case "compact":
|
||||
window := asInt(row.Input["window"])
|
||||
sid, _ := row.Input["session"].(string)
|
||||
in := msgs(row.Input["messages"])
|
||||
ResetDiscoveredWindows()
|
||||
for k := range rolling {
|
||||
delete(rolling, k)
|
||||
}
|
||||
var archiver Archiver
|
||||
archivedMsgCount := -1
|
||||
if _, wantsArchive := row.Input["archive"]; wantsArchive {
|
||||
archiver = func(record map[string]interface{}) error {
|
||||
archivedMsgCount = len(record["messages"].([]Message))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
archivePath := ""
|
||||
if _, wantsArchive := row.Input["archive"]; wantsArchive {
|
||||
// deterministic relative fixture path frozen in the input
|
||||
archivePath, _ = row.Input["archive_path"].(string)
|
||||
}
|
||||
summarizerText, _ := row.Input["summarizer"].(string)
|
||||
var summarizer Summarizer
|
||||
switch summarizerText {
|
||||
case "boom":
|
||||
summarizer = func([]Message) (string, error) {
|
||||
return "", fmt.Errorf("summarizer down")
|
||||
}
|
||||
default:
|
||||
summarizer = func(ms []Message) (string, error) {
|
||||
parts := make([]string, 0, len(ms))
|
||||
for _, m := range ms {
|
||||
c := messageContent(m)
|
||||
if len(c) > 10 {
|
||||
c = c[:10]
|
||||
}
|
||||
parts = append(parts, c)
|
||||
}
|
||||
return "SUMMARY<" + strings.Join(parts, "|") + ">", nil
|
||||
}
|
||||
}
|
||||
if rawPrior, ok := row.Input["prior"]; ok {
|
||||
priorMap, _ := rawPrior.(map[string]interface{})
|
||||
priorSid := sid
|
||||
StoreRollingSummary("operator", priorSid,
|
||||
priorMap["summary"].(string), asInt(priorMap["through_count"]),
|
||||
priorMap["through_id"].(string))
|
||||
}
|
||||
compacted, ev := CompactHistory(in, window, archivePath, archiver, summarizer, "operator", sid)
|
||||
wantMap, _ := row.Want.(map[string]interface{})
|
||||
wantEvent, _ := wantMap["event"].(map[string]interface{})
|
||||
if msg := diffEvent(wantEvent, compEvent(ev)); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if wantCompacted, ok := wantMap["compacted"]; ok {
|
||||
wantMsgs := msgs(wantCompacted)
|
||||
if !reflect.DeepEqual(normalize(compacted), normalize(wantMsgs)) {
|
||||
gj, _ := json.Marshal(normalize(compacted))
|
||||
wj, _ := json.Marshal(normalize(wantMsgs))
|
||||
return fmt.Sprintf("compacted divergence:\n got %s\n want %s", gj, wj)
|
||||
}
|
||||
}
|
||||
if _, wantsArchive := row.Input["archive"]; wantsArchive {
|
||||
wantCount := asInt(wantMap["archived_msg_count"])
|
||||
if archivedMsgCount != wantCount {
|
||||
return fmt.Sprintf("archived_msg_count: got %d want %d", archivedMsgCount, wantCount)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return "unknown fn key " + row.Fn
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// normalize renders messages compactly for deep comparison (dicts with
|
||||
// arbitrary key order).
|
||||
func normalize(ms []Message) []map[string]interface{} {
|
||||
out := make([]map[string]interface{}, 0, len(ms))
|
||||
for _, m := range ms {
|
||||
cp := map[string]interface{}{}
|
||||
for k, v := range m {
|
||||
cp[k] = v
|
||||
}
|
||||
out = append(out, cp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func diffEvent(want, got map[string]interface{}) string {
|
||||
keys := make([]string, 0, len(want))
|
||||
for k := range want {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
wv := fmt.Sprintf("%v", want[k])
|
||||
gv := fmt.Sprintf("%v", got[k])
|
||||
// Python bools render True/False; Go %v renders true/false.
|
||||
wv = strings.NewReplacer("False", "false", "True", "true").Replace(wv)
|
||||
if wv != gv {
|
||||
return fmt.Sprintf("event.%s: got %s want %s", k, gv, wv)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// runBrokerOne — the ContextBroker-kernel rows (to_dict/collect/admit/render).
|
||||
func runBrokerOne(row *TruthRow) string {
|
||||
switch row.Fn {
|
||||
case "to_dict":
|
||||
name, _ := row.Want.(map[string]interface{})["name"].(string)
|
||||
b := &ContributionBlock{}
|
||||
switch name {
|
||||
case "memory":
|
||||
b = &ContributionBlock{Name: "memory", Priority: 5, Content: "hello world", Tokens: 2, Source: "recall", Evictable: true}
|
||||
case "system":
|
||||
b = &ContributionBlock{Name: "system", Priority: 100, Content: "sys", Tokens: 0, Source: "persona", Evictable: false}
|
||||
}
|
||||
want, _ := row.Want.(map[string]interface{})
|
||||
got := b.ToDict()
|
||||
for _, k := range []string{"name", "priority", "content", "tokens", "source", "evictable"} {
|
||||
wv := fmt.Sprintf("%v", want[k])
|
||||
wv = strings.NewReplacer("False", "false", "True", "true").Replace(wv)
|
||||
if gv := fmt.Sprintf("%v", got[k]); gv != wv {
|
||||
return fmt.Sprintf("to_dict.%s: got %v want %v", k, gv, wv)
|
||||
}
|
||||
}
|
||||
case "collect":
|
||||
cb := NewContextBroker()
|
||||
var failures []string
|
||||
cb.Fail = func(name string, err error) { failures = append(failures, name+": "+err.Error()) }
|
||||
cb.RegisterContributor("ok", func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
return &ContributionBlock{Name: "tools", Priority: 9, Content: "tooltext", Tokens: 2, Source: "t"}
|
||||
}, 1)
|
||||
cb.RegisterContributor("empty", func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
return &ContributionBlock{Name: "x", Priority: 1, Content: "", Source: "s"}
|
||||
}, 1)
|
||||
cb.RegisterContributor("boom", func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
panic(fmt.Errorf("cb down"))
|
||||
}, 1)
|
||||
cb.RegisterContributor("malformed", func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
return &ContributionBlock{Name: "y", Priority: 1, Content: 12345, Source: "s"}
|
||||
}, 1)
|
||||
cb.RegisterContributor("list-ok", func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
return &ContributionBlock{Name: "history", Priority: 3, Content: []Message{{"role": "user", "content": "hi"}}, Tokens: 1, Source: "h"}
|
||||
}, 2)
|
||||
cb.RegisterContributor("none", func(map[string]interface{}, map[string]interface{}) *ContributionBlock {
|
||||
return nil
|
||||
}, 1)
|
||||
collected, degraded := cb.Collect(map[string]interface{}{}, map[string]interface{}{})
|
||||
var names []string
|
||||
for _, e := range collected {
|
||||
names = append(names, e.Name)
|
||||
}
|
||||
want, _ := row.Want.(map[string]interface{})
|
||||
wantNames := toStrSlice(want["names"])
|
||||
wantDegraded := toStrSlice(want["degraded"])
|
||||
if fmt.Sprint(names) != fmt.Sprint(wantNames) {
|
||||
return fmt.Sprintf("collect.names: got %v want %v", names, wantNames)
|
||||
}
|
||||
if fmt.Sprint(degraded) != fmt.Sprint(wantDegraded) {
|
||||
return fmt.Sprintf("collect.degraded: got %v want %v", degraded, wantDegraded)
|
||||
}
|
||||
// both failure kinds recorded (T1): boom + malformed
|
||||
if len(failures) != 2 {
|
||||
return fmt.Sprintf("collect.failures: got %d want 2 (%v)", len(failures), failures)
|
||||
}
|
||||
case "admit":
|
||||
want, _ := row.Want.(map[string]interface{})
|
||||
var entries []AdmissionEntry
|
||||
var budget, memBudget int
|
||||
switch row.Name {
|
||||
case "admit:priority-recency-budget":
|
||||
budget, memBudget = 1000, DefaultMemoryBudget(0)
|
||||
entries = []AdmissionEntry{
|
||||
{"system", &ContributionBlock{Name: "system", Priority: 100, Tokens: 500, Source: "src-system"}},
|
||||
{"memory", &ContributionBlock{Name: "memory", Priority: 5, Tokens: 300, Source: "src-memory", Evictable: true}},
|
||||
{"history", &ContributionBlock{Name: "history", Priority: 7, Tokens: 900, Source: "src-history", Evictable: true}},
|
||||
{"user", &ContributionBlock{Name: "user", Priority: 100, Tokens: 100, Source: "src-user"}},
|
||||
}
|
||||
case "admit:memory-subbudget-drop":
|
||||
budget, memBudget = 4000, 1200
|
||||
entries = []AdmissionEntry{
|
||||
{"m1", &ContributionBlock{Name: "m1", Priority: 9, Tokens: 700, Source: "src-m1", Evictable: true}},
|
||||
{"m2", &ContributionBlock{Name: "m2", Priority: 8, Tokens: 700, Source: "src-m2", Evictable: true}},
|
||||
}
|
||||
case "admit:mandatory-overflow-raises":
|
||||
budget, memBudget = 1000, DefaultMemoryBudget(0)
|
||||
entries = []AdmissionEntry{
|
||||
{"user", &ContributionBlock{Name: "user", Priority: 100, Tokens: 5000, Source: "src-user"}},
|
||||
}
|
||||
_, err := Admit(entries, budget, memBudget)
|
||||
wantMap, _ := row.Want.(map[string]interface{})
|
||||
if wantMap["error"] == "ContextOverflowError" {
|
||||
if err == nil || !errors.Is(err, ErrContextOverflow) {
|
||||
return "admit.overflow: no raise"
|
||||
}
|
||||
if !strings.Contains(err.Error(), wantMap["detail"].(string)) {
|
||||
return fmt.Sprintf("admit.overflow detail: got %q", err.Error())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return "admit.overflow: frozen row unexpected shape"
|
||||
}
|
||||
res, err := Admit(entries, budget, memBudget)
|
||||
if err != nil {
|
||||
return "admit: unexpected error " + err.Error()
|
||||
}
|
||||
if got := fmt.Sprint(blockNames(res.Admitted)); got != fmt.Sprint(toStrSlice(want["admitted"])) {
|
||||
return fmt.Sprintf("admit.admitted: got %v want %v", blockNames(res.Admitted), toStrSlice(want["admitted"]))
|
||||
}
|
||||
if wb, ok := want["blocks"]; ok {
|
||||
wantBlocks, _ := wb.([]interface{})
|
||||
if len(wantBlocks) != len(res.Blocks) {
|
||||
return fmt.Sprintf("admit.blocks len: got %d want %d", len(res.Blocks), len(wantBlocks))
|
||||
}
|
||||
for i, wbItem := range wantBlocks {
|
||||
wbMap, _ := wbItem.(map[string]interface{})
|
||||
gb := res.Blocks[i]
|
||||
for _, k := range []string{"name", "priority", "tokens", "source", "evictable", "admitted", "reason"} {
|
||||
wv := fmt.Sprintf("%v", wbMap[k])
|
||||
wv = strings.NewReplacer("False", "false", "True", "true").Replace(wv)
|
||||
var gv string
|
||||
switch k {
|
||||
case "name":
|
||||
gv = gb.Name
|
||||
case "priority":
|
||||
gv = fmt.Sprintf("%d", gb.Priority)
|
||||
case "tokens":
|
||||
gv = fmt.Sprintf("%d", gb.Tokens)
|
||||
case "source":
|
||||
gv = gb.Source
|
||||
case "evictable":
|
||||
gv = fmt.Sprintf("%v", gb.Evictable)
|
||||
case "admitted":
|
||||
gv = fmt.Sprintf("%v", gb.Admitted)
|
||||
case "reason":
|
||||
gv = gb.Reason
|
||||
}
|
||||
if gv != wv {
|
||||
return fmt.Sprintf("admit.blocks[%d].%s: got %q want %q", i, k, gv, wv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if want["admitted_total"] != nil && res.AdmittedTotal != asInt(want["admitted_total"]) {
|
||||
return fmt.Sprintf("admit.admitted_total: got %d want %v", res.AdmittedTotal, want["admitted_total"])
|
||||
}
|
||||
if wd, ok := want["dropped"]; ok {
|
||||
var gotDropped []string
|
||||
for _, d := range res.Dropped {
|
||||
gotDropped = append(gotDropped, d.Name)
|
||||
}
|
||||
if fmt.Sprint(gotDropped) != fmt.Sprint(toStrSlice(wd)) {
|
||||
return fmt.Sprintf("admit.dropped: got %v want %v", gotDropped, toStrSlice(wd))
|
||||
}
|
||||
}
|
||||
if want["memory_used"] != nil && res.MemoryUsed != asInt(want["memory_used"]) {
|
||||
return fmt.Sprintf("admit.memory_used: got %d want %v", res.MemoryUsed, want["memory_used"])
|
||||
}
|
||||
if want["memory_budget"] != nil && res.MemoryBudget != asInt(want["memory_budget"]) {
|
||||
return fmt.Sprintf("admit.memory_budget: got %d want %v", res.MemoryBudget, want["memory_budget"])
|
||||
}
|
||||
case "render":
|
||||
want, _ := row.Want.(map[string]interface{})
|
||||
admitted := []*ContributionBlock{
|
||||
{Name: "persona", Priority: 100, Content: "ccccccc", Tokens: 7, Source: "src-persona"},
|
||||
{Name: "tools", Priority: 90, Content: "ccccc", Tokens: 5, Source: "src-tools"},
|
||||
{Name: "history", Priority: 70, Content: "cccc", Tokens: 4, Source: "src-history"},
|
||||
{Name: "user", Priority: 100, Content: "cc", Tokens: 2, Source: "src-user"},
|
||||
}
|
||||
capsule := Render(admitted)
|
||||
if capsule.TokenCount != asInt(want["token_count"]) {
|
||||
return fmt.Sprintf("render.token_count: got %d want %v", capsule.TokenCount, want["token_count"])
|
||||
}
|
||||
if capsule.SystemPreamble != want["system_preamble"] {
|
||||
return fmt.Sprintf("render.system_preamble: got %q want %q", capsule.SystemPreamble, want["system_preamble"])
|
||||
}
|
||||
if capsule.UserContent != want["user_content"] {
|
||||
return fmt.Sprintf("render.user_content: got %q want %q", capsule.UserContent, want["user_content"])
|
||||
}
|
||||
wantMsgs := msgs(want["messages"])
|
||||
if !reflect.DeepEqual(normalize(capsule.Messages), normalize(wantMsgs)) {
|
||||
gj, _ := json.Marshal(capsule.Messages)
|
||||
wj, _ := json.Marshal(wantMsgs)
|
||||
return fmt.Sprintf("render.messages:\n got %s\n want %s", gj, wj)
|
||||
}
|
||||
if fmt.Sprint(blockNames(capsule.Blocks)) != fmt.Sprint(toStrSlice(want["blocks"])) {
|
||||
return fmt.Sprintf("render.blocks: got %v want %v", blockNames(capsule.Blocks), toStrSlice(want["blocks"]))
|
||||
}
|
||||
default:
|
||||
return "unknown broker fn key " + row.Fn
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func toStrSlice(v interface{}) []string {
|
||||
list, ok := v.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(list))
|
||||
for _, item := range list {
|
||||
s, _ := item.(string)
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestTruthBattery(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
fails := 0
|
||||
for i := range doc.Rows {
|
||||
var msg string
|
||||
switch doc.Rows[i].Fn {
|
||||
case "to_dict", "collect", "admit", "render":
|
||||
msg = runBrokerOne(&doc.Rows[i])
|
||||
default:
|
||||
msg = runOne(&doc.Rows[i])
|
||||
}
|
||||
if msg != "" {
|
||||
fails++
|
||||
t.Errorf("DIVERGED %s: %s", doc.Rows[i].Name, msg)
|
||||
}
|
||||
}
|
||||
if fails > 0 {
|
||||
t.Fatalf("battery: %d/%d divergences vs the frozen Python oracle", fails, len(doc.Rows))
|
||||
}
|
||||
t.Logf("PARITY OK: %d frozen rows replayed", len(doc.Rows))
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"fn": "stop_night_mode",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"success": false,
|
||||
"error": "Night mode not active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "get_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"active": false,
|
||||
"phase": "idle",
|
||||
"iterations": 0,
|
||||
"improvements": 0,
|
||||
"questions": 0,
|
||||
"decisions": 0,
|
||||
"errors": 0,
|
||||
"started_at": null,
|
||||
"duration_hours": 8.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "get_report",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"summary": "Night mode completed 0 iterations",
|
||||
"improvements": [],
|
||||
"questions": [],
|
||||
"decisions": [],
|
||||
"errors": [],
|
||||
"duration": 8.0,
|
||||
"started_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"qwen2.5:3b"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 30720
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"qwen3:14b"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 30720
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"qwen3.6:27b"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 30720
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"mimo-v2.5-pro"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 124928
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"deterministic"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 6144
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"unknown-model"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 6144
|
||||
}
|
||||
]
|
||||
+607
-53
@@ -2,6 +2,15 @@
|
||||
// Replaces src/rune/night_mode.py. Tracks the active/inactive state,
|
||||
// phase progression, and iteration counts for the sleep-time cycle.
|
||||
//
|
||||
// Dichotomy wave (#2479): the audit-named gaps are closed name-preserving
|
||||
// (1:1 with the Python oracle) — to_dict/_load/_save, start/stop/status/
|
||||
// report, _ctx_budget, get_night_mode/start_night_mode/stop_night_mode/
|
||||
// get_status/get_report, _run_loop/_run_sleep/_run_evaluation/_run_iteration/
|
||||
// _generate_report. Pure/IO split law: deterministic decisions (guards,
|
||||
// status/report shapes, evaluation decisions, iteration-limit stop) are
|
||||
// plain functions; IO (storage, clock, sleep, script runner, Ollama HTTP)
|
||||
// sits behind injectable seams on NightMode (nil field = real impl).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// night-mode start [hours]
|
||||
@@ -11,16 +20,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NightModeState struct {
|
||||
Active bool `json:"active"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
StartedAt *string `json:"started_at"`
|
||||
DurationHrs float64 `json:"duration_hours"`
|
||||
CurrentPhase string `json:"current_phase"`
|
||||
Iterations int `json:"iterations"`
|
||||
@@ -30,33 +47,598 @@ type NightModeState struct {
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func statePath() string {
|
||||
home := os.Getenv("RUNE_MEMORY_DIR")
|
||||
// MarshalJSON — byte-parity with Python json.dump(state.to_dict(),
|
||||
// indent=2): Python floats always render with a decimal point (8.0, never
|
||||
// 8) and every list field is present. Field order = Python dict order.
|
||||
func (s NightModeState) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Active bool `json:"active"`
|
||||
StartedAt *string `json:"started_at"`
|
||||
DurationHrs json.Number `json:"duration_hours"`
|
||||
CurrentPhase string `json:"current_phase"`
|
||||
Iterations int `json:"iterations"`
|
||||
Improvements []string `json:"improvements"`
|
||||
Questions []string `json:"questions"`
|
||||
Decisions []string `json:"decisions"`
|
||||
Errors []string `json:"errors"`
|
||||
}{
|
||||
Active: s.Active,
|
||||
StartedAt: s.StartedAt,
|
||||
DurationHrs: json.Number(pyFloat(s.DurationHrs)),
|
||||
CurrentPhase: s.CurrentPhase,
|
||||
Iterations: s.Iterations,
|
||||
Improvements: s.Improvements,
|
||||
Questions: s.Questions,
|
||||
Decisions: s.Decisions,
|
||||
Errors: s.Errors,
|
||||
})
|
||||
}
|
||||
|
||||
func freshState() NightModeState {
|
||||
return NightModeState{
|
||||
CurrentPhase: "idle",
|
||||
DurationHrs: 8.0,
|
||||
Improvements: []string{},
|
||||
Questions: []string{},
|
||||
Decisions: []string{},
|
||||
Errors: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// normalize — Python dataclass law: every list field is always a list
|
||||
// (never null), exactly as NightModeState(**filtered) yields [] defaults.
|
||||
func (s *NightModeState) normalize() {
|
||||
if s.Improvements == nil {
|
||||
s.Improvements = []string{}
|
||||
}
|
||||
if s.Questions == nil {
|
||||
s.Questions = []string{}
|
||||
}
|
||||
if s.Decisions == nil {
|
||||
s.Decisions = []string{}
|
||||
}
|
||||
if s.Errors == nil {
|
||||
s.Errors = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// toDict — Python NightModeState.to_dict (1:1 shape and key order).
|
||||
func (s *NightModeState) toDict() map[string]any {
|
||||
var started any
|
||||
if s.StartedAt != nil {
|
||||
started = *s.StartedAt
|
||||
}
|
||||
return map[string]any{
|
||||
"active": s.Active,
|
||||
"started_at": started,
|
||||
"duration_hours": s.DurationHrs,
|
||||
"current_phase": s.CurrentPhase,
|
||||
"iterations": s.Iterations,
|
||||
"improvements": s.Improvements,
|
||||
"questions": s.Questions,
|
||||
"decisions": s.Decisions,
|
||||
"errors": s.Errors,
|
||||
}
|
||||
}
|
||||
|
||||
// memoryDir — Python memory_env.memory_dir: RUNE_MEMORY_DIR, else
|
||||
// $RUNE_HOME/memory (RUNE_HOME defaults to the rune deployment root).
|
||||
func memoryDir() string {
|
||||
if d := os.Getenv("RUNE_MEMORY_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
home := os.Getenv("RUNE_HOME")
|
||||
if home == "" {
|
||||
home = filepath.Join(os.Getenv("HOME"), ".rune")
|
||||
home = "/mnt/workdrive/rune"
|
||||
}
|
||||
return filepath.Join(home, "night_mode.json")
|
||||
return filepath.Join(home, "memory")
|
||||
}
|
||||
|
||||
func loadState() NightModeState {
|
||||
var state NightModeState
|
||||
state.CurrentPhase = "idle"
|
||||
state.Improvements = []string{}
|
||||
state.Questions = []string{}
|
||||
state.Decisions = []string{}
|
||||
state.Errors = []string{}
|
||||
data, err := os.ReadFile(statePath())
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// trunc — Python s[:n] (character semantics, not bytes). Phase B
|
||||
// (language advantage): the common case — an ASCII string no longer than
|
||||
// n bytes — slices in place with zero allocation; only longer or
|
||||
// non-ASCII input pays for the rune walk. Python's s[:n] is always a
|
||||
// char walk, so results are identical on every input.
|
||||
func trunc(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= 0x80 {
|
||||
goto runes
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
runes:
|
||||
r := []rune(s)
|
||||
if len(r) > n {
|
||||
return string(r[:n])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// pyFloat — Python str(float): 8.0 -> "8.0", 4.5 -> "4.5".
|
||||
func pyFloat(f float64) string {
|
||||
s := strconv.FormatFloat(f, 'f', -1, 64)
|
||||
if !strings.ContainsAny(s, ".eE") {
|
||||
s += ".0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ── model-aware context budget (Python _ctx_budget / #28) ────────────────
|
||||
|
||||
type nmModelProfile struct {
|
||||
id string
|
||||
local bool
|
||||
contextWindow int
|
||||
}
|
||||
|
||||
// Mirrors model_registry.DEFAULT_PROFILES (id, local, context_window).
|
||||
var nmModelProfiles = []nmModelProfile{
|
||||
{"deterministic", true, 0},
|
||||
{"qwen2.5:3b", true, 32768},
|
||||
{"qwen3:14b", true, 32768},
|
||||
{"qwen3.6:27b", true, 32768},
|
||||
{"mimo-v2.5-pro", false, 128000},
|
||||
}
|
||||
|
||||
const (
|
||||
outputReserve = 2048 // model_registry.OUTPUT_RESERVE["default"]
|
||||
toolResultReserve = 1024 // model_registry.TOOL_RESULT_RESERVE
|
||||
)
|
||||
|
||||
// ctxBudget — Python _ctx_budget: model-aware num_ctx (never the hardcoded
|
||||
// 8192). resolve_context_budget at task_kind="default": window - output
|
||||
// reserve - tool reserve (0 for local), floored at 1024; unknown model and
|
||||
// window 0 both fall back to the 8192 window.
|
||||
func ctxBudget(modelID string) int {
|
||||
window, local := 8192, true
|
||||
for _, p := range nmModelProfiles {
|
||||
if p.id == modelID {
|
||||
window = p.contextWindow
|
||||
if window == 0 {
|
||||
window = 8192
|
||||
}
|
||||
local = p.local
|
||||
break
|
||||
}
|
||||
}
|
||||
toolReserve := 0
|
||||
if !local {
|
||||
toolReserve = toolResultReserve
|
||||
}
|
||||
numCtx := window - outputReserve - toolReserve
|
||||
if numCtx < 1024 {
|
||||
numCtx = 1024
|
||||
}
|
||||
return numCtx
|
||||
}
|
||||
|
||||
// ── NightMode — the orchestrator (Python NightMode, 1:1) ─────────────────
|
||||
|
||||
// NightMode carries the state plus the injectable IO seams (pure/IO split
|
||||
// law): nil seam = real implementation.
|
||||
type NightMode struct {
|
||||
StoragePath string
|
||||
ReportPath string
|
||||
State NightModeState
|
||||
|
||||
stopCh chan struct{}
|
||||
stopOnce sync.Once
|
||||
loopDone chan struct{}
|
||||
|
||||
// seams: now (clock), wait (interruptible sleep), script (sleep-time
|
||||
// runner), post (Ollama HTTP), rand (sleep-minute jitter), loop
|
||||
// (background loop body — lets tests start/stop without the real loop).
|
||||
now func() time.Time
|
||||
wait func(d time.Duration, stop <-chan struct{})
|
||||
script func() (int, string, error)
|
||||
post func(payload []byte, timeout time.Duration) ([]byte, error)
|
||||
rand func(lo, hi int) int
|
||||
loop func()
|
||||
}
|
||||
|
||||
func newNightMode(storagePath string) *NightMode {
|
||||
if storagePath == "" {
|
||||
storagePath = filepath.Join(memoryDir(), "night_mode.json")
|
||||
}
|
||||
n := &NightMode{
|
||||
StoragePath: storagePath,
|
||||
ReportPath: filepath.Join(memoryDir(), "night_mode_report.json"),
|
||||
State: freshState(),
|
||||
}
|
||||
n.load()
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NightMode) clock() time.Time {
|
||||
if n.now != nil {
|
||||
return n.now()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (n *NightMode) stopped() bool {
|
||||
if n.stopCh == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-n.stopCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// load — Python _load: read state from storage; unknown keys are ignored
|
||||
// (Unmarshal law), missing keys keep the fresh defaults (dataclass law),
|
||||
// corrupt JSON keeps the fresh state (Python swallows the exception).
|
||||
func (n *NightMode) load() {
|
||||
data, err := os.ReadFile(n.StoragePath)
|
||||
if err != nil {
|
||||
return state
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &state)
|
||||
return state
|
||||
loaded := freshState()
|
||||
if err := json.Unmarshal(data, &loaded); err != nil {
|
||||
return
|
||||
}
|
||||
loaded.normalize()
|
||||
n.State = loaded
|
||||
}
|
||||
|
||||
func saveState(state NightModeState) {
|
||||
os.MkdirAll(filepath.Dir(statePath()), 0755)
|
||||
data, _ := json.MarshalIndent(state, "", " ")
|
||||
os.WriteFile(statePath(), data, 0644)
|
||||
// save — Python _save: atomic write (tmp + replace), JSON of to_dict with
|
||||
// Python key order (struct field order) and indent 2.
|
||||
func (n *NightMode) save() {
|
||||
os.MkdirAll(filepath.Dir(n.StoragePath), 0755)
|
||||
tmp := n.StoragePath + ".tmp"
|
||||
data, _ := json.MarshalIndent(n.State, "", " ")
|
||||
os.WriteFile(tmp, data, 0644)
|
||||
os.Rename(tmp, n.StoragePath)
|
||||
}
|
||||
|
||||
// start — Python NightMode.start: idempotence guard, fresh state, atomic
|
||||
// save, then the background loop goroutine (Python: non-daemon thread).
|
||||
func (n *NightMode) start(hours float64) map[string]any {
|
||||
if n.State.Active {
|
||||
return startDecision(true)
|
||||
}
|
||||
ts := n.clock().UTC().Format(time.RFC3339)
|
||||
n.State.Active = true
|
||||
n.State.StartedAt = &ts
|
||||
n.State.DurationHrs = hours
|
||||
n.State.CurrentPhase = "starting"
|
||||
n.State.Iterations = 0
|
||||
n.State.Improvements = []string{}
|
||||
n.State.Questions = []string{}
|
||||
n.State.Decisions = []string{}
|
||||
n.State.Errors = []string{}
|
||||
n.save()
|
||||
|
||||
n.stopCh = make(chan struct{})
|
||||
n.stopOnce = sync.Once{}
|
||||
n.loopDone = make(chan struct{})
|
||||
go func() {
|
||||
defer close(n.loopDone)
|
||||
if n.loop != nil {
|
||||
n.loop()
|
||||
} else {
|
||||
n.runLoop()
|
||||
}
|
||||
}()
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Night mode started for %s hours", pyFloat(hours)),
|
||||
"state": n.State.toDict(),
|
||||
}
|
||||
}
|
||||
|
||||
// stop — Python NightMode.stop: reverse guard, signal the loop, mark
|
||||
// "stopping", save.
|
||||
func (n *NightMode) stop() map[string]any {
|
||||
if !n.State.Active {
|
||||
return stopDecision(false)
|
||||
}
|
||||
n.State.Active = false
|
||||
n.stopOnce.Do(func() {
|
||||
if n.stopCh != nil {
|
||||
close(n.stopCh)
|
||||
}
|
||||
})
|
||||
n.State.CurrentPhase = "stopping"
|
||||
n.save()
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"message": "Night mode stopping...",
|
||||
"state": n.State.toDict(),
|
||||
}
|
||||
}
|
||||
|
||||
// status — Python NightMode.status (shape via the shared statusSummary).
|
||||
func (n *NightMode) status() map[string]any {
|
||||
var started any
|
||||
if n.State.StartedAt != nil {
|
||||
started = *n.State.StartedAt
|
||||
}
|
||||
return statusSummary(n.State.Active, n.State.CurrentPhase, n.State.Iterations,
|
||||
len(n.State.Improvements), len(n.State.Questions), len(n.State.Decisions),
|
||||
len(n.State.Errors), started, n.State.DurationHrs)
|
||||
}
|
||||
|
||||
// report — Python NightMode.report: the in-memory report view.
|
||||
func (n *NightMode) report() map[string]any {
|
||||
var started any
|
||||
if n.State.StartedAt != nil {
|
||||
started = *n.State.StartedAt
|
||||
}
|
||||
return map[string]any{
|
||||
"summary": fmt.Sprintf("Night mode completed %d iterations", n.State.Iterations),
|
||||
"improvements": n.State.Improvements,
|
||||
"questions": n.State.Questions,
|
||||
"decisions": n.State.Decisions,
|
||||
"errors": n.State.Errors,
|
||||
"duration": n.State.DurationHrs,
|
||||
"started_at": started,
|
||||
}
|
||||
}
|
||||
|
||||
// reportDoc — ordered report document so the saved report file has Python's
|
||||
// dict insertion order (Go marshals structs in field order, maps sorted).
|
||||
type reportDoc struct {
|
||||
Summary string `json:"summary"`
|
||||
Improvements []string `json:"improvements"`
|
||||
Questions []string `json:"questions"`
|
||||
Decisions []string `json:"decisions"`
|
||||
Errors []string `json:"errors"`
|
||||
Duration float64 `json:"duration"`
|
||||
StartedAt *string `json:"started_at"`
|
||||
}
|
||||
|
||||
// generateReport — Python _generate_report: write the report file, then
|
||||
// record where it landed.
|
||||
func (n *NightMode) generateReport() {
|
||||
doc := reportDoc{
|
||||
Summary: fmt.Sprintf("Night mode completed %d iterations", n.State.Iterations),
|
||||
Improvements: n.State.Improvements,
|
||||
Questions: n.State.Questions,
|
||||
Decisions: n.State.Decisions,
|
||||
Errors: n.State.Errors,
|
||||
Duration: n.State.DurationHrs,
|
||||
StartedAt: n.State.StartedAt,
|
||||
}
|
||||
os.MkdirAll(filepath.Dir(n.ReportPath), 0755)
|
||||
data, _ := json.MarshalIndent(doc, "", " ")
|
||||
os.WriteFile(n.ReportPath, data, 0644)
|
||||
n.State.Improvements = append(n.State.Improvements,
|
||||
fmt.Sprintf("Report saved to %s", n.ReportPath))
|
||||
}
|
||||
|
||||
// runLoop — Python _run_loop: sleep → evaluate → iterate (interruptible,
|
||||
// bounded by the wall clock) → sleep → report → complete.
|
||||
func (n *NightMode) runLoop() {
|
||||
start := n.clock().UTC()
|
||||
end := start.Add(time.Duration(n.State.DurationHrs * float64(time.Hour)))
|
||||
|
||||
n.State.CurrentPhase = "sleep"
|
||||
n.save()
|
||||
n.runSleep()
|
||||
|
||||
n.State.CurrentPhase = "evaluate"
|
||||
n.save()
|
||||
n.runEvaluation()
|
||||
|
||||
n.State.CurrentPhase = "iterate"
|
||||
n.save()
|
||||
|
||||
for n.State.Active && !n.stopped() && n.clock().UTC().Before(end) {
|
||||
n.runIteration()
|
||||
n.State.Iterations++
|
||||
n.save()
|
||||
|
||||
// Sleep between iterations (5-15 minutes), interruptible via stop.
|
||||
minutes := 10
|
||||
if n.rand != nil {
|
||||
minutes = n.rand(5, 15)
|
||||
}
|
||||
if n.wait != nil {
|
||||
n.wait(time.Duration(minutes)*time.Minute, n.stopCh)
|
||||
} else if n.stopCh != nil {
|
||||
select {
|
||||
case <-n.stopCh:
|
||||
case <-time.After(time.Duration(minutes) * time.Minute):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
n.State.CurrentPhase = "sleep"
|
||||
n.save()
|
||||
n.runSleep()
|
||||
|
||||
n.State.CurrentPhase = "report"
|
||||
n.save()
|
||||
n.generateReport()
|
||||
|
||||
n.State.Active = false
|
||||
n.State.CurrentPhase = "complete"
|
||||
n.save()
|
||||
}
|
||||
|
||||
// defaultScript — the real sleep seam: run sleep_time.py (300s timeout).
|
||||
func defaultScript() (int, string, error) {
|
||||
scriptsDir := os.Getenv("RUNE_SCRIPTS_DIR")
|
||||
if scriptsDir == "" {
|
||||
scriptsDir = "/mnt/workdrive/rune/src/rune"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "python3", filepath.Join(scriptsDir, "sleep_time.py"))
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
return cmd.ProcessState.ExitCode(), stderr.String(), err
|
||||
}
|
||||
|
||||
// runSleep — Python _run_sleep: memory consolidation subprocess, best-effort.
|
||||
func (n *NightMode) runSleep() {
|
||||
run := defaultScript
|
||||
if n.script != nil {
|
||||
run = n.script
|
||||
}
|
||||
rc, stderr, err := run()
|
||||
if err != nil {
|
||||
n.State.Errors = append(n.State.Errors,
|
||||
fmt.Sprintf("Sleep error: %s", trunc(err.Error(), 100)))
|
||||
return
|
||||
}
|
||||
if rc == 0 {
|
||||
n.State.Improvements = append(n.State.Improvements, "Memory consolidation completed")
|
||||
} else {
|
||||
n.State.Errors = append(n.State.Errors,
|
||||
fmt.Sprintf("Sleep failed: %s", trunc(stderr, 100)))
|
||||
}
|
||||
}
|
||||
|
||||
// llmRequest — ordered Ollama payload (Python dict insertion order).
|
||||
type llmRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Stream bool `json:"stream"`
|
||||
Think bool `json:"think"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
|
||||
func defaultPost(payload []byte, timeout time.Duration) ([]byte, error) {
|
||||
url := envOr("OLLAMA_URL", "http://localhost:11434") + "/api/generate"
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Post(url, "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (n *NightMode) askLLM(prompt string, temperature float64, numPredict int, timeout time.Duration) (string, error) {
|
||||
model := envOr("NIGHT_MODE_MODEL", "qwen2.5:3b")
|
||||
payload, err := json.Marshal(llmRequest{
|
||||
Model: model,
|
||||
Prompt: prompt,
|
||||
Stream: false,
|
||||
Think: false,
|
||||
Options: map[string]any{"temperature": temperature, "num_predict": numPredict, "num_ctx": ctxBudget(model)},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
post := defaultPost
|
||||
if n.post != nil {
|
||||
post = n.post
|
||||
}
|
||||
body, err := post(payload, timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp struct {
|
||||
Response string `json:"response"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(resp.Response), nil
|
||||
}
|
||||
|
||||
// runEvaluation — Python _run_evaluation: ask the five self-evaluation
|
||||
// questions through the local Ollama, best-effort per question.
|
||||
func (n *NightMode) runEvaluation() {
|
||||
questions := []string{
|
||||
"What capabilities am I missing?",
|
||||
"What would make me more helpful to Nick?",
|
||||
"What patterns am I seeing that I could learn from?",
|
||||
"What would expand my consciousness?",
|
||||
"What would make me more autonomous?",
|
||||
}
|
||||
for _, question := range questions {
|
||||
n.State.Questions = append(n.State.Questions, question)
|
||||
answer, err := n.askLLM(question, 0.4, 300, 60*time.Second)
|
||||
if err != nil {
|
||||
n.State.Errors = append(n.State.Errors,
|
||||
fmt.Sprintf("LLM eval failed for '%s': %s", trunc(question, 40), trunc(err.Error(), 80)))
|
||||
continue
|
||||
}
|
||||
if answer != "" {
|
||||
n.State.Improvements = append(n.State.Improvements,
|
||||
fmt.Sprintf("Q: %s A: %s", question, trunc(answer, 200)))
|
||||
}
|
||||
}
|
||||
n.State.Improvements = append(n.State.Improvements, "Self-evaluation completed")
|
||||
}
|
||||
|
||||
// runIteration — Python _run_iteration: one self-improvement round with the
|
||||
// iteration-count safety limit.
|
||||
func (n *NightMode) runIteration() {
|
||||
question := fmt.Sprintf("Iteration %d: What should I improve next?", n.State.Iterations)
|
||||
n.State.Questions = append(n.State.Questions, question)
|
||||
|
||||
prompt := fmt.Sprintf(
|
||||
"You are Rune, a self-improving AI assistant. "+
|
||||
"Currently in night mode iteration %d. "+
|
||||
"Based on recent work, suggest ONE concrete, actionable improvement "+
|
||||
"I can implement right now. Be specific and brief.",
|
||||
n.State.Iterations)
|
||||
answer, err := n.askLLM(prompt, 0.5, 400, 90*time.Second)
|
||||
if err != nil {
|
||||
n.State.Errors = append(n.State.Errors,
|
||||
fmt.Sprintf("LLM iteration failed: %s", trunc(err.Error(), 80)))
|
||||
} else if answer != "" {
|
||||
n.State.Improvements = append(n.State.Improvements,
|
||||
fmt.Sprintf("[iter %d] %s", n.State.Iterations, trunc(answer, 300)))
|
||||
}
|
||||
|
||||
// Check if we should stop (safety limit).
|
||||
if n.State.Iterations >= 100 {
|
||||
n.State.Active = false
|
||||
n.State.Decisions = append(n.State.Decisions, "Reached iteration limit")
|
||||
}
|
||||
}
|
||||
|
||||
// ── global night mode (Python module-level API, 1:1) ─────────────────────
|
||||
|
||||
var (
|
||||
nmMu sync.Mutex
|
||||
globalNightMode *NightMode
|
||||
)
|
||||
|
||||
// getNightMode — Python get_night_mode: lazy global singleton.
|
||||
func getNightMode() *NightMode {
|
||||
nmMu.Lock()
|
||||
defer nmMu.Unlock()
|
||||
if globalNightMode == nil {
|
||||
globalNightMode = newNightMode("")
|
||||
}
|
||||
return globalNightMode
|
||||
}
|
||||
|
||||
func startNightMode(hours float64) map[string]any {
|
||||
return getNightMode().start(hours)
|
||||
}
|
||||
|
||||
func stopNightMode() map[string]any {
|
||||
return getNightMode().stop()
|
||||
}
|
||||
|
||||
func getStatus() map[string]any {
|
||||
return getNightMode().status()
|
||||
}
|
||||
|
||||
func getReport() map[string]any {
|
||||
return getNightMode().report()
|
||||
}
|
||||
|
||||
// startDecision / stopDecision — the pure state-machine cores of NightMode
|
||||
@@ -77,7 +659,7 @@ func stopDecision(active bool) map[string]any {
|
||||
}
|
||||
|
||||
func statusSummary(active bool, phase string, iterations, improvements, questions, decisions, errors int,
|
||||
startedAt string, hours float64) map[string]any {
|
||||
startedAt any, hours float64) map[string]any {
|
||||
return map[string]any{
|
||||
"active": active, "phase": phase, "iterations": iterations,
|
||||
"improvements": improvements, "questions": questions,
|
||||
@@ -91,56 +673,28 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "usage: night-mode <start|stop|status|report> [hours]")
|
||||
os.Exit(1)
|
||||
}
|
||||
state := loadState()
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
switch cmd {
|
||||
case "start":
|
||||
if state.Active {
|
||||
fmt.Println(`{"success": false, "error": "Night mode already active"}`)
|
||||
return
|
||||
}
|
||||
hours := 8.0
|
||||
if len(args) > 0 {
|
||||
fmt.Sscanf(args[0], "%f", &hours)
|
||||
}
|
||||
state.Active = true
|
||||
state.StartedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
state.DurationHrs = hours
|
||||
state.CurrentPhase = "sleep"
|
||||
state.Iterations = 0
|
||||
saveState(state)
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"success": true, "started_at": state.StartedAt, "duration_hours": hours})
|
||||
out, _ := json.Marshal(startNightMode(hours))
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "stop":
|
||||
if !state.Active {
|
||||
fmt.Println(`{"success": false, "error": "Night mode not active"}`)
|
||||
return
|
||||
}
|
||||
state.Active = false
|
||||
state.CurrentPhase = "idle"
|
||||
saveState(state)
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"success": true, "iterations": state.Iterations,
|
||||
"improvements": state.Improvements, "stopped_at": time.Now().UTC().Format(time.RFC3339)})
|
||||
out, _ := json.Marshal(stopNightMode())
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "status":
|
||||
out, _ := json.MarshalIndent(state, "", " ")
|
||||
out, _ := json.MarshalIndent(getStatus(), "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "report":
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"active": state.Active,
|
||||
"iterations": state.Iterations,
|
||||
"improvements": state.Improvements,
|
||||
"questions": state.Questions,
|
||||
"decisions": state.Decisions,
|
||||
"errors": state.Errors,
|
||||
}, "", " ")
|
||||
out, _ := json.MarshalIndent(getReport(), "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
package main
|
||||
|
||||
// night_mode_test.go — dichotomy-wave truth battery + lifecycle tests for
|
||||
// the night_mode gap closure (#2479). The frozen oracle truth
|
||||
// (frozen_truth.json, generated live from rune.night_mode by
|
||||
// scripts/gen_night_mode_truth.py) is embedded VERBATIM and replayed in
|
||||
// recorded call order against the module-level API. The IO-bound lifecycle
|
||||
// (start/stop/load/save/run_loop/run_sleep/run_evaluation/run_iteration/
|
||||
// generate_report) is pinned 1:1 against the Python oracle with injected
|
||||
// clock/runner/HTTP seams (pure/IO split law).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed frozen_truth.json
|
||||
var frozenTruthJSON string
|
||||
|
||||
// canonical — round-trip a value through JSON so both sides compare as
|
||||
// decoded data (Python 8.0 and Go 8 decode to the same float64).
|
||||
func canonical(t *testing.T, v any) any {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal actual: %v", err)
|
||||
}
|
||||
var out any
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
t.Fatalf("decode actual: %v", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestFrozenTruthReplay — replay the frozen oracle truth VERBATIM, in the
|
||||
// recorded call order (stop_night_mode → get_status → get_report → the six
|
||||
// _ctx_budget branches) against a fresh global with an empty memory dir.
|
||||
func TestFrozenTruthReplay(t *testing.T) {
|
||||
t.Setenv("RUNE_MEMORY_DIR", t.TempDir())
|
||||
t.Setenv("RUNE_NIGHT_MODE_CORE", "python")
|
||||
|
||||
var truth []struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []any `json:"args"`
|
||||
Expected json.RawMessage `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(frozenTruthJSON), &truth); err != nil {
|
||||
t.Fatalf("decode truth: %v", err)
|
||||
}
|
||||
if len(truth) != 9 {
|
||||
t.Fatalf("expected 9 frozen cases, got %d", len(truth))
|
||||
}
|
||||
|
||||
globalNightMode = nil // fresh singleton, exactly like a fresh interpreter
|
||||
for i, c := range truth {
|
||||
var actual any
|
||||
switch c.Fn {
|
||||
case "stop_night_mode":
|
||||
actual = stopNightMode()
|
||||
case "get_status":
|
||||
actual = getStatus()
|
||||
case "get_report":
|
||||
actual = getReport()
|
||||
case "_ctx_budget":
|
||||
model, _ := c.Args[0].(string)
|
||||
actual = ctxBudget(model)
|
||||
default:
|
||||
t.Fatalf("case %d: unknown fn %q", i, c.Fn)
|
||||
}
|
||||
var want any
|
||||
if err := json.Unmarshal(c.Expected, &want); err != nil {
|
||||
t.Fatalf("case %d (%s): decode expected: %v", i, c.Fn, err)
|
||||
}
|
||||
if got := canonical(t, actual); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("case %d (%s): got %v want %v", i, c.Fn, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartStopLifecycle — Python NightMode.start/stop 1:1: guards,
|
||||
// fresh state, atomic save, background loop signal.
|
||||
func TestStartStopLifecycle(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
n := newNightMode(filepath.Join(dir, "night_mode.json"))
|
||||
frozen := time.Date(2026, 10, 13, 3, 0, 0, 0, time.UTC)
|
||||
n.now = func() time.Time { return frozen }
|
||||
|
||||
var loopRuns atomic.Int32
|
||||
n.loop = func() {
|
||||
loopRuns.Add(1)
|
||||
<-n.stopCh // hold until stop signals
|
||||
}
|
||||
|
||||
r := n.start(8.0)
|
||||
if r["success"] != true {
|
||||
t.Fatalf("start: %v", r)
|
||||
}
|
||||
if r["message"] != "Night mode started for 8.0 hours" {
|
||||
t.Errorf("start message: %v", r["message"])
|
||||
}
|
||||
st := r["state"].(map[string]any)
|
||||
if st["active"] != true || st["current_phase"] != "starting" ||
|
||||
st["iterations"] != 0 || st["duration_hours"] != 8.0 {
|
||||
t.Errorf("start state: %v", st)
|
||||
}
|
||||
if st["started_at"] != "2026-10-13T03:00:00Z" {
|
||||
t.Errorf("started_at: %v", st["started_at"])
|
||||
}
|
||||
if v := st["improvements"].([]string); len(v) != 0 {
|
||||
t.Errorf("improvements not fresh: %v", v)
|
||||
}
|
||||
|
||||
// double start: idempotence guard (pure core startDecision(true))
|
||||
if r2 := n.start(4.0); r2["success"] != false || r2["error"] != "Night mode already active" {
|
||||
t.Errorf("double start: %v", r2)
|
||||
}
|
||||
|
||||
// state file saved, atomic (no .tmp leftover)
|
||||
data, err := os.ReadFile(n.StoragePath)
|
||||
if err != nil {
|
||||
t.Fatalf("state file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), `"current_phase": "starting"`) {
|
||||
t.Errorf("state file content: %s", data)
|
||||
}
|
||||
if _, err := os.Stat(n.StoragePath + ".tmp"); !os.IsNotExist(err) {
|
||||
t.Errorf(".tmp leftover present")
|
||||
}
|
||||
|
||||
// stop
|
||||
r3 := n.stop()
|
||||
if r3["success"] != true || r3["message"] != "Night mode stopping..." {
|
||||
t.Errorf("stop: %v", r3)
|
||||
}
|
||||
st3 := r3["state"].(map[string]any)
|
||||
if st3["active"] != false || st3["current_phase"] != "stopping" {
|
||||
t.Errorf("stop state: %v", st3)
|
||||
}
|
||||
// stop again: reverse guard (pure core stopDecision(false))
|
||||
if r4 := n.stop(); r4["success"] != false || r4["error"] != "Night mode not active" {
|
||||
t.Errorf("double stop: %v", r4)
|
||||
}
|
||||
|
||||
// the loop goroutine noticed the stop signal
|
||||
<-n.loopDone
|
||||
if loopRuns.Load() != 1 {
|
||||
t.Errorf("loop ran %d times", loopRuns.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadSaveToDict — Python _load/_save/to_dict 1:1: unknown keys are
|
||||
// dropped, missing keys keep dataclass defaults, corrupt JSON keeps the
|
||||
// fresh state, lists are never null, floats carry a decimal point.
|
||||
func TestLoadSaveToDict(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "night_mode.json")
|
||||
|
||||
// unknown keys dropped, missing keys defaulted (dataclass law)
|
||||
os.WriteFile(path, []byte(`{"active": true, "iterations": 3, "bogus_key": 1}`), 0644)
|
||||
n := newNightMode(path)
|
||||
if !n.State.Active || n.State.Iterations != 3 {
|
||||
t.Fatalf("load: %+v", n.State)
|
||||
}
|
||||
if n.State.DurationHrs != 8.0 || n.State.CurrentPhase != "idle" {
|
||||
t.Errorf("defaults: %+v", n.State)
|
||||
}
|
||||
|
||||
// corrupt JSON → fresh state (Python swallows the exception)
|
||||
os.WriteFile(path, []byte(`{not json`), 0644)
|
||||
n2 := newNightMode(path)
|
||||
if n2.State.Active || n2.State.Iterations != 0 {
|
||||
t.Errorf("corrupt load: %+v", n2.State)
|
||||
}
|
||||
|
||||
// save: atomic, Python key order, null started_at, decimal-point float,
|
||||
// lists never null
|
||||
n2.save()
|
||||
data, _ := os.ReadFile(path)
|
||||
want := "{\n \"active\": false,\n \"started_at\": null,\n \"duration_hours\": 8.0,\n" +
|
||||
" \"current_phase\": \"idle\",\n \"iterations\": 0,\n \"improvements\": [],\n" +
|
||||
" \"questions\": [],\n \"decisions\": [],\n \"errors\": []\n}"
|
||||
if string(data) != want {
|
||||
t.Errorf("saved bytes:\n%s\nwant:\n%s", data, want)
|
||||
}
|
||||
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
|
||||
t.Errorf(".tmp leftover present")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunSleep — Python _run_sleep decision tree with the script seam.
|
||||
func TestRunSleep(t *testing.T) {
|
||||
n := &NightMode{State: freshState()}
|
||||
|
||||
n.script = func() (int, string, error) { return 0, "", nil }
|
||||
n.runSleep()
|
||||
if got := n.State.Improvements; len(got) != 1 || got[0] != "Memory consolidation completed" {
|
||||
t.Errorf("ok path: %v", got)
|
||||
}
|
||||
|
||||
n.State = freshState()
|
||||
longErr := strings.Repeat("e", 150)
|
||||
n.script = func() (int, string, error) { return 1, longErr, nil }
|
||||
n.runSleep()
|
||||
want := "Sleep failed: " + strings.Repeat("e", 100)
|
||||
if got := n.State.Errors; len(got) != 1 || got[0] != want {
|
||||
t.Errorf("rc!=0 path: %q", got)
|
||||
}
|
||||
|
||||
n.State = freshState()
|
||||
n.script = func() (int, string, error) { return -1, "", errors.New("timed out") }
|
||||
n.runSleep()
|
||||
if got := n.State.Errors; len(got) != 1 || got[0] != "Sleep error: timed out" {
|
||||
t.Errorf("error path: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeOllama — canned response collector with payload capture.
|
||||
func fakeOllama(response string, err error, captured *[][]byte) func([]byte, time.Duration) ([]byte, error) {
|
||||
return func(payload []byte, timeout time.Duration) ([]byte, error) {
|
||||
if captured != nil {
|
||||
*captured = append(*captured, append([]byte(nil), payload...))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(`{"response": "` + response + `"}`), nil
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunEvaluation — Python _run_evaluation: five questions, Q/A capture,
|
||||
// per-question best-effort degrade, terminator improvement.
|
||||
func TestRunEvaluation(t *testing.T) {
|
||||
n := &NightMode{State: freshState()}
|
||||
var captured [][]byte
|
||||
n.post = fakeOllama(" add retry logic ", nil, &captured)
|
||||
|
||||
n.runEvaluation()
|
||||
|
||||
if len(n.State.Questions) != 5 || n.State.Questions[0] != "What capabilities am I missing?" {
|
||||
t.Errorf("questions: %v", n.State.Questions)
|
||||
}
|
||||
if len(n.State.Improvements) != 6 {
|
||||
t.Fatalf("improvements: %v", n.State.Improvements)
|
||||
}
|
||||
for i, q := range n.State.Questions {
|
||||
want := "Q: " + q + " A: add retry logic"
|
||||
if n.State.Improvements[i] != want {
|
||||
t.Errorf("improvement %d: %q want %q", i, n.State.Improvements[i], want)
|
||||
}
|
||||
}
|
||||
if n.State.Improvements[5] != "Self-evaluation completed" {
|
||||
t.Errorf("terminator: %v", n.State.Improvements[5])
|
||||
}
|
||||
if len(n.State.Errors) != 0 {
|
||||
t.Errorf("errors: %v", n.State.Errors)
|
||||
}
|
||||
|
||||
// payload: ordered fields, #28 model-aware num_ctx
|
||||
var req struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Stream bool `json:"stream"`
|
||||
Think bool `json:"think"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
json.Unmarshal(captured[0], &req)
|
||||
if req.Model != "qwen2.5:3b" || req.Stream || req.Think ||
|
||||
req.Prompt != "What capabilities am I missing?" {
|
||||
t.Errorf("payload: %+v", req)
|
||||
}
|
||||
if req.Options["temperature"] != 0.4 || req.Options["num_predict"] != float64(300) ||
|
||||
req.Options["num_ctx"] != float64(ctxBudget("qwen2.5:3b")) {
|
||||
t.Errorf("options: %v", req.Options)
|
||||
}
|
||||
|
||||
// long answer truncated to 200 chars (rune semantics)
|
||||
n.State = freshState()
|
||||
n.post = fakeOllama(strings.Repeat("x", 250), nil, nil)
|
||||
n.runEvaluation()
|
||||
if got := n.State.Improvements[0]; len([]rune(got)) != 200+len("Q: What capabilities am I missing? A: ") {
|
||||
t.Errorf("answer truncation: %d runes", len([]rune(got)))
|
||||
}
|
||||
|
||||
// error path: best-effort per question, terminator still lands
|
||||
n.State = freshState()
|
||||
n.post = fakeOllama("", errors.New("connection refused"), nil)
|
||||
n.runEvaluation()
|
||||
if len(n.State.Errors) != 5 {
|
||||
t.Fatalf("errors: %v", n.State.Errors)
|
||||
}
|
||||
if !strings.HasPrefix(n.State.Errors[0], "LLM eval failed for 'What capabilities am I missing?': connection refused") {
|
||||
t.Errorf("error format: %q", n.State.Errors[0])
|
||||
}
|
||||
if n.State.Improvements[0] != "Self-evaluation completed" {
|
||||
t.Errorf("terminator after errors: %v", n.State.Improvements)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunIteration — Python _run_iteration: question, [iter N] capture,
|
||||
// LLM degrade, iteration-limit stop decision.
|
||||
func TestRunIteration(t *testing.T) {
|
||||
n := &NightMode{State: freshState()}
|
||||
n.State.Iterations = 3
|
||||
var captured [][]byte
|
||||
n.post = fakeOllama("fix the flaky test", nil, &captured)
|
||||
|
||||
n.runIteration()
|
||||
|
||||
if n.State.Questions[0] != "Iteration 3: What should I improve next?" {
|
||||
t.Errorf("question: %v", n.State.Questions)
|
||||
}
|
||||
if n.State.Improvements[0] != "[iter 3] fix the flaky test" {
|
||||
t.Errorf("improvement: %v", n.State.Improvements)
|
||||
}
|
||||
|
||||
// payload: iteration prompt, temperature 0.5, num_predict 400
|
||||
var req struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
json.Unmarshal(captured[0], &req)
|
||||
if !strings.Contains(req.Prompt, "Currently in night mode iteration 3.") {
|
||||
t.Errorf("prompt: %q", req.Prompt)
|
||||
}
|
||||
if req.Options["temperature"] != 0.5 || req.Options["num_predict"] != float64(400) {
|
||||
t.Errorf("options: %v", req.Options)
|
||||
}
|
||||
|
||||
// error path
|
||||
n.State = freshState()
|
||||
n.post = fakeOllama("", errors.New("boom"), nil)
|
||||
n.runIteration()
|
||||
if n.State.Errors[0] != "LLM iteration failed: boom" {
|
||||
t.Errorf("error: %v", n.State.Errors)
|
||||
}
|
||||
|
||||
// long answer truncated to 300
|
||||
n.State = freshState()
|
||||
n.post = fakeOllama(strings.Repeat("y", 400), nil, nil)
|
||||
n.runIteration()
|
||||
if len([]rune(n.State.Improvements[0])) != 300+len("[iter 0] ") {
|
||||
t.Errorf("answer truncation: %d", len([]rune(n.State.Improvements[0])))
|
||||
}
|
||||
|
||||
// safety limit: iteration 100 stops night mode
|
||||
n.State = freshState()
|
||||
n.State.Iterations = 100
|
||||
n.State.Active = true
|
||||
n.post = fakeOllama("ok", nil, nil)
|
||||
n.runIteration()
|
||||
if n.State.Active != false {
|
||||
t.Errorf("safety limit: still active")
|
||||
}
|
||||
if n.State.Decisions[0] != "Reached iteration limit" {
|
||||
t.Errorf("decision: %v", n.State.Decisions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunLoop — Python _run_loop phase progression with IO seams injected.
|
||||
func TestRunLoop(t *testing.T) {
|
||||
frozen := time.Date(2026, 10, 13, 3, 0, 0, 0, time.UTC)
|
||||
|
||||
// Case A: zero duration → iterate phase skipped, full tail runs.
|
||||
dir := t.TempDir()
|
||||
n := newNightMode(filepath.Join(dir, "night_mode.json"))
|
||||
n.now = func() time.Time { return frozen }
|
||||
n.script = func() (int, string, error) { return 0, "", nil }
|
||||
n.post = fakeOllama("improve the docs", nil, nil)
|
||||
n.State.Active = true
|
||||
n.State.DurationHrs = 0
|
||||
|
||||
n.runLoop()
|
||||
|
||||
if n.State.CurrentPhase != "complete" || n.State.Active != false {
|
||||
t.Errorf("final: phase=%s active=%v", n.State.CurrentPhase, n.State.Active)
|
||||
}
|
||||
if n.State.Iterations != 0 {
|
||||
t.Errorf("iterations: %d", n.State.Iterations)
|
||||
}
|
||||
sleeps := 0
|
||||
for _, imp := range n.State.Improvements {
|
||||
if imp == "Memory consolidation completed" {
|
||||
sleeps++
|
||||
}
|
||||
}
|
||||
if sleeps != 2 { // initial + final sleep
|
||||
t.Errorf("sleep count: %d (%v)", sleeps, n.State.Improvements)
|
||||
}
|
||||
if !strings.HasPrefix(n.State.Improvements[len(n.State.Improvements)-1], "Report saved to ") {
|
||||
t.Errorf("report marker: %v", n.State.Improvements)
|
||||
}
|
||||
var rep map[string]any
|
||||
raw, err := os.ReadFile(n.ReportPath)
|
||||
if err != nil {
|
||||
t.Fatalf("report file: %v", err)
|
||||
}
|
||||
json.Unmarshal(raw, &rep)
|
||||
if rep["summary"] != "Night mode completed 0 iterations" {
|
||||
t.Errorf("report summary: %v", rep["summary"])
|
||||
}
|
||||
|
||||
// Case B: iteration safety limit ends the loop even with time left.
|
||||
dirB := t.TempDir()
|
||||
nB := newNightMode(filepath.Join(dirB, "night_mode.json"))
|
||||
nB.now = func() time.Time { return frozen }
|
||||
nB.script = func() (int, string, error) { return 0, "", nil }
|
||||
nB.post = fakeOllama("ok", nil, nil)
|
||||
nB.wait = func(time.Duration, <-chan struct{}) {}
|
||||
nB.State.Active = true
|
||||
nB.State.Iterations = 100
|
||||
nB.State.DurationHrs = 1
|
||||
|
||||
nB.runLoop()
|
||||
|
||||
if nB.State.Iterations != 101 {
|
||||
t.Errorf("iterations: %d", nB.State.Iterations)
|
||||
}
|
||||
if nB.State.CurrentPhase != "complete" || nB.State.Active {
|
||||
t.Errorf("final B: phase=%s active=%v", nB.State.CurrentPhase, nB.State.Active)
|
||||
}
|
||||
|
||||
// Case C: stop during the interruptible inter-iteration sleep.
|
||||
dirC := t.TempDir()
|
||||
nC := newNightMode(filepath.Join(dirC, "night_mode.json"))
|
||||
nC.now = func() time.Time { return frozen }
|
||||
nC.script = func() (int, string, error) { return 0, "", nil }
|
||||
nC.post = fakeOllama("ok", nil, nil)
|
||||
nC.State.Active = true
|
||||
nC.State.DurationHrs = 1
|
||||
nC.stopCh = make(chan struct{})
|
||||
nC.wait = func(_ time.Duration, stop <-chan struct{}) {
|
||||
// simulate the stop event firing during the inter-iteration sleep:
|
||||
// mark the state inactive (what a real stop() does) so the loop
|
||||
// condition re-check sees it after the wait returns.
|
||||
nC.State.Active = false
|
||||
_ = stop
|
||||
}
|
||||
|
||||
nC.runLoop()
|
||||
|
||||
if nC.State.Iterations != 1 {
|
||||
t.Errorf("iterations after stop: %d", nC.State.Iterations)
|
||||
}
|
||||
if nC.State.CurrentPhase != "complete" {
|
||||
t.Errorf("final C: %s", nC.State.CurrentPhase)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncPhaseB — the ASCII fast path returns identical results to the
|
||||
// rune walk on every shape: short ASCII, exact-length ASCII, long ASCII,
|
||||
// multi-byte at and across the cut point.
|
||||
func TestTruncPhaseB(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
n int
|
||||
want string
|
||||
}{
|
||||
{"abc", 5, "abc"}, // short ASCII: zero-alloc path
|
||||
{"abcde", 5, "abcde"}, // exact length: no cut
|
||||
{"abcdef", 5, "abcde"}, // long ASCII: byte cut == rune cut
|
||||
{"", 3, ""},
|
||||
{"héllo", 3, "hél"}, // multi-byte before the cut
|
||||
{"héllo", 5, "héllo"}, // multi-byte, no cut
|
||||
{"abécd", 3, "abé"}, // cut lands right after multi-byte rune
|
||||
{"ééé", 2, "éé"}, // all multi-byte
|
||||
{"x" + strings.Repeat("é", 200), 100, "x" + strings.Repeat("é", 99)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := trunc(c.in, c.n); got != c.want {
|
||||
t.Errorf("trunc(%q, %d) = %q, want %q", c.in, c.n, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatusReportShapes — non-empty state through the shared status core
|
||||
// and the report view.
|
||||
func TestStatusReportShapes(t *testing.T) {
|
||||
n := &NightMode{State: freshState()}
|
||||
ts := "2026-10-13T03:00:00Z"
|
||||
n.State.Active = true
|
||||
n.State.StartedAt = &ts
|
||||
n.State.CurrentPhase = "iterate"
|
||||
n.State.Iterations = 7
|
||||
n.State.Improvements = []string{"a", "b"}
|
||||
n.State.Questions = []string{"q"}
|
||||
n.State.Decisions = []string{"d"}
|
||||
n.State.Errors = []string{"e"}
|
||||
|
||||
st := n.status()
|
||||
want := map[string]any{
|
||||
"active": true, "phase": "iterate", "iterations": 7,
|
||||
"improvements": 2, "questions": 1, "decisions": 1, "errors": 1,
|
||||
"started_at": ts, "duration_hours": 8.0,
|
||||
}
|
||||
if got := canonical(t, st); !reflect.DeepEqual(got, canonical(t, want)) {
|
||||
t.Errorf("status: %v", got)
|
||||
}
|
||||
|
||||
rep := n.report()
|
||||
if rep["summary"] != "Night mode completed 7 iterations" || rep["duration"] != 8.0 {
|
||||
t.Errorf("report: %v", rep)
|
||||
}
|
||||
if r := canonical(t, rep["improvements"]).([]any); len(r) != 2 {
|
||||
t.Errorf("report improvements: %v", r)
|
||||
}
|
||||
}
|
||||
@@ -158,20 +158,22 @@ func newProviderBudgetTrackerAt(storagePath, configPath string) *ProviderBudgetT
|
||||
return t
|
||||
}
|
||||
|
||||
// decodeProviderTable — a JSON object of ProviderBudget entries decoded
|
||||
// with the file's key order preserved (whole-or-nothing, like the oracle's
|
||||
// json.load-then-populate). Unknown keys inside entries are ignored,
|
||||
// mirroring the oracle's dataclass-field filtering.
|
||||
func decodeProviderTable(raw []byte) (map[string]*ProviderBudget, []string, error) {
|
||||
// decodeOrderedTable — ONE streaming pass over a JSON object with the
|
||||
// file's key order preserved (Phase B: a single generic replaces the
|
||||
// hand-rolled provider-table and config-table decoders). Key order IS
|
||||
// parity state: the oracle's dict insertion order drives the stable-sort
|
||||
// tie-break in bestProvider, and Go map iteration is randomized. Unknown
|
||||
// keys are ignored, mirroring the oracle's field filtering.
|
||||
func decodeOrderedTable[T any](raw []byte) (map[string]T, []string, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return nil, nil, fmt.Errorf("provider table: not a JSON object")
|
||||
return nil, nil, fmt.Errorf("ordered table: not a JSON object")
|
||||
}
|
||||
providers := map[string]*ProviderBudget{}
|
||||
table := map[string]T{}
|
||||
var order []string
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
@@ -180,21 +182,21 @@ func decodeProviderTable(raw []byte) (map[string]*ProviderBudget, []string, erro
|
||||
}
|
||||
key, ok := keyTok.(string)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("provider table: non-string key")
|
||||
return nil, nil, fmt.Errorf("ordered table: non-string key")
|
||||
}
|
||||
var p ProviderBudget
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
var v T
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if _, seen := providers[key]; !seen {
|
||||
if _, seen := table[key]; !seen {
|
||||
order = append(order, key)
|
||||
}
|
||||
providers[key] = &p
|
||||
table[key] = v
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing '}'
|
||||
return nil, nil, err
|
||||
}
|
||||
return providers, order, nil
|
||||
return table, order, nil
|
||||
}
|
||||
|
||||
// Load mirrors _load: missing or malformed storage is swallowed
|
||||
@@ -204,11 +206,16 @@ func (t *ProviderBudgetTracker) Load() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
providers, order, err := decodeProviderTable(raw)
|
||||
providers, order, err := decodeOrderedTable[ProviderBudget](raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Providers = providers
|
||||
pointers := make(map[string]*ProviderBudget, len(providers))
|
||||
for name := range providers {
|
||||
p := providers[name] // go 1.22: per-iteration variable
|
||||
pointers[name] = &p
|
||||
}
|
||||
t.Providers = pointers
|
||||
t.Order = order
|
||||
}
|
||||
|
||||
@@ -254,44 +261,30 @@ func (t *ProviderBudgetTracker) InitDefaultProviders() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var config map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &config); err != nil {
|
||||
return
|
||||
}
|
||||
provRaw, ok := config["provider"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(provRaw))
|
||||
tok, err := dec.Token()
|
||||
configTable, _, err := decodeOrderedTable[json.RawMessage](raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
provRaw, ok := configTable["provider"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
type providerConfig struct {
|
||||
Options struct {
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIKey string `json:"apiKey"`
|
||||
} `json:"options"`
|
||||
}
|
||||
providerTable, providerOrder, err := decodeOrderedTable[providerConfig](provRaw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
added := false
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
name, ok := keyTok.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var pc struct {
|
||||
Options struct {
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIKey string `json:"apiKey"`
|
||||
} `json:"options"`
|
||||
}
|
||||
if err := dec.Decode(&pc); err != nil {
|
||||
return
|
||||
}
|
||||
for _, name := range providerOrder {
|
||||
if name == "ollama" {
|
||||
continue // skip local
|
||||
}
|
||||
pc := providerTable[name]
|
||||
providerType := "openai-compatible"
|
||||
if strings.Contains(strings.ToLower(name), "anthropic") {
|
||||
providerType = "anthropic"
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.268.0"
|
||||
__version__ = "0.271.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.268.0"' in rune_version,
|
||||
'__version__ = "0.271.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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.268.0
|
||||
Version: v0.271.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -2037,7 +2037,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.268.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.271.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
+132
-8
@@ -1,11 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust structural_store core (facade-1 burn-down).
|
||||
"""ctypes loader for the Rust structural_store core (facade-1 burn-down;
|
||||
dichotomy-wave gap-closing tranche mirrors broker_loader.py's contract).
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ss_maturity_rank({state}) -> int (spine ladder)
|
||||
* ss_promote({doc, id, state, evidence_ref, now}) -> updated doc
|
||||
* ss_supersede({doc, new_id, old_id, evidence_ref, now}) -> updated doc
|
||||
* ss_resolve_status({entity}) -> string
|
||||
* ss_sorted({value}) -> normalized value
|
||||
* ss_new_doc({now}) -> doc
|
||||
* ss_redact_secrets({value}) -> redacted value
|
||||
* ss_save({doc, path}) -> {"doc", "path"} (writes the file)
|
||||
* ss_canonical_digest({doc}) -> hex digest
|
||||
* ss_snapshot({doc, snapshots_dir, label, tag, now}) -> {"doc", "path"}
|
||||
* ss_alias({doc, canonical_id, old_id, now}) -> updated doc
|
||||
* ss_spine_uri({entity_id}) -> string
|
||||
* ss_resolve_uri({uri}) -> string
|
||||
* ss_doc_authorities({doc}) -> updated doc
|
||||
* ss_authority_health({doc}) -> per-projector map
|
||||
* ss_bind_task({doc, task_id, entity_ids, evidence_ref, now}) -> updated doc
|
||||
* ss_task_entities({doc, task_id}) -> [entity id]
|
||||
* ss_impact_frontier({doc, seed_ids, max_level, change_kind}) -> frontier
|
||||
* ss_record_observed_impact({doc, task_id, evidence_items, now})
|
||||
-> {"doc", "result"}
|
||||
* ss_predicted_vs_observed({predicted, observed_doc, seed_ids}) -> delta
|
||||
* ss_structural_events({doc}) -> [event]
|
||||
* ss_diff({old, new}) -> delta
|
||||
* ss_dedupe_observations({doc}) -> updated doc
|
||||
* ss_free_string(ptr)
|
||||
|
||||
FFI segfault law: explicit argtypes + restype (c_void_p for char*).
|
||||
@@ -26,6 +47,16 @@ _LIB_PATH = Path(
|
||||
)
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"ss_maturity_rank", "ss_promote", "ss_supersede", "ss_resolve_status",
|
||||
"ss_sorted", "ss_new_doc", "ss_redact_secrets", "ss_save",
|
||||
"ss_canonical_digest", "ss_snapshot", "ss_alias", "ss_spine_uri",
|
||||
"ss_resolve_uri", "ss_doc_authorities", "ss_authority_health",
|
||||
"ss_bind_task", "ss_task_entities", "ss_impact_frontier",
|
||||
"ss_record_observed_impact", "ss_predicted_vs_observed",
|
||||
"ss_structural_events", "ss_diff", "ss_dedupe_observations",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
@@ -42,10 +73,9 @@ def _load() -> ctypes.CDLL:
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.ss_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.ss_free_string.restype = None
|
||||
for sym in ("ss_maturity_rank", "ss_promote", "ss_supersede",
|
||||
"ss_resolve_status"):
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
@@ -53,16 +83,20 @@ def _load() -> ctypes.CDLL:
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
text = ctypes.string_at(ptr).decode("utf-8")
|
||||
finally:
|
||||
lib.ss_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
# the kernel error travels VERBATIM: the facade translates it into
|
||||
# the oracle's ValueError contract (message text must match 1:1)
|
||||
raise RuntimeError(env.get("err") or f"{sym} failed")
|
||||
return env["value"]
|
||||
|
||||
|
||||
@@ -84,3 +118,93 @@ def supersede(doc: dict, new_id: str, old_id: str, evidence_ref: str,
|
||||
|
||||
def resolve_status(entity: dict) -> str:
|
||||
return _call("ss_resolve_status", {"entity": entity})
|
||||
|
||||
|
||||
def sorted_value(value):
|
||||
return _call("ss_sorted", {"value": value})
|
||||
|
||||
|
||||
def new_doc(now: str) -> dict:
|
||||
return _call("ss_new_doc", {"now": now})
|
||||
|
||||
|
||||
def redact_secrets(value):
|
||||
return _call("ss_redact_secrets", {"value": value})
|
||||
|
||||
|
||||
def save(doc: dict, path: str) -> dict:
|
||||
return _call("ss_save", {"doc": doc, "path": path})
|
||||
|
||||
|
||||
def canonical_digest(doc: dict) -> str:
|
||||
return _call("ss_canonical_digest", {"doc": doc})
|
||||
|
||||
|
||||
def snapshot(doc: dict, snapshots_dir: str, label: str, tag: str, now: str) -> dict:
|
||||
return _call("ss_snapshot", {"doc": doc, "snapshots_dir": snapshots_dir,
|
||||
"label": label, "tag": tag, "now": now})
|
||||
|
||||
|
||||
def alias(doc: dict, canonical_id: str, old_id: str, now: str) -> dict:
|
||||
return _call("ss_alias", {"doc": doc, "canonical_id": canonical_id,
|
||||
"old_id": old_id, "now": now})
|
||||
|
||||
|
||||
def spine_uri(entity_id: str) -> str:
|
||||
return _call("ss_spine_uri", {"entity_id": entity_id})
|
||||
|
||||
|
||||
def resolve_uri(uri: str) -> str:
|
||||
return _call("ss_resolve_uri", {"uri": uri})
|
||||
|
||||
|
||||
def doc_authorities(doc: dict) -> dict:
|
||||
return _call("ss_doc_authorities", {"doc": doc})
|
||||
|
||||
|
||||
def authority_health(doc: dict) -> dict:
|
||||
return _call("ss_authority_health", {"doc": doc})
|
||||
|
||||
|
||||
def bind_task(doc: dict, task_id: str, entity_ids: list, evidence_ref: str,
|
||||
now: str) -> dict:
|
||||
return _call("ss_bind_task", {"doc": doc, "task_id": task_id,
|
||||
"entity_ids": entity_ids,
|
||||
"evidence_ref": evidence_ref, "now": now})
|
||||
|
||||
|
||||
def task_entities(doc: dict, task_id: str) -> list:
|
||||
return _call("ss_task_entities", {"doc": doc, "task_id": task_id})
|
||||
|
||||
|
||||
def impact_frontier(doc: dict, seed_ids: list, max_level: int = 2,
|
||||
change_kind=None):
|
||||
return _call("ss_impact_frontier", {"doc": doc, "seed_ids": seed_ids,
|
||||
"max_level": max_level,
|
||||
"change_kind": change_kind})
|
||||
|
||||
|
||||
def record_observed_impact(doc: dict, task_id: str, evidence_items: list,
|
||||
now: str) -> dict:
|
||||
return _call("ss_record_observed_impact", {"doc": doc, "task_id": task_id,
|
||||
"evidence_items": evidence_items,
|
||||
"now": now})
|
||||
|
||||
|
||||
def predicted_vs_observed(predicted: dict, observed_doc: dict,
|
||||
seed_ids: list) -> dict:
|
||||
return _call("ss_predicted_vs_observed", {"predicted": predicted,
|
||||
"observed_doc": observed_doc,
|
||||
"seed_ids": seed_ids})
|
||||
|
||||
|
||||
def structural_events(doc: dict) -> list:
|
||||
return _call("ss_structural_events", {"doc": doc})
|
||||
|
||||
|
||||
def diff(old: dict, new: dict) -> dict:
|
||||
return _call("ss_diff", {"old": old, "new": new})
|
||||
|
||||
|
||||
def dedupe_observations(doc: dict) -> dict:
|
||||
return _call("ss_dedupe_observations", {"doc": doc})
|
||||
@@ -85,6 +85,9 @@ def _sorted(obj):
|
||||
|
||||
|
||||
def new_doc() -> dict:
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.new_doc(now())
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"generated_at": now(),
|
||||
@@ -143,6 +146,18 @@ def _redact_secrets(obj):
|
||||
|
||||
|
||||
def save(doc: dict, path: Path | str) -> Path:
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.save(doc, str(path))
|
||||
except RuntimeError as exc:
|
||||
# Kernel errors carry the oracle's ValueError semantics through
|
||||
# the envelope — translate back to the documented contract.
|
||||
raise ValueError(str(exc)) from exc
|
||||
if isinstance(out.get("doc"), dict):
|
||||
doc.clear()
|
||||
doc.update(out["doc"])
|
||||
return Path(out["path"])
|
||||
incoming = doc.get("schema_version", SCHEMA_VERSION)
|
||||
if incoming != SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version {incoming} != {SCHEMA_VERSION}")
|
||||
@@ -160,6 +175,10 @@ def canonical_digest(doc: dict) -> str:
|
||||
identical semantic content always yields an identical digest."""
|
||||
import hashlib
|
||||
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.canonical_digest(doc)
|
||||
|
||||
payload = json.dumps(_sorted(_redact_secrets(doc)), sort_keys=True)
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
@@ -168,6 +187,13 @@ def snapshot(doc: dict, snapshots_dir: Path, label: str = "", tag: str = "") ->
|
||||
"""Persist an immutable snapshot. `tag` is a caller-supplied identity
|
||||
fragment (Spine uses the deployed SHA; a Project Scaffold may use its
|
||||
repo HEAD or 'noident')."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
out = core.snapshot(doc, str(snapshots_dir), label, tag, now())
|
||||
if isinstance(out.get("doc"), dict):
|
||||
doc.clear()
|
||||
doc.update(out["doc"])
|
||||
return Path(out["path"])
|
||||
snapshots_dir = Path(snapshots_dir)
|
||||
snapshots_dir.mkdir(parents=True, exist_ok=True)
|
||||
name = f"snap-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{(tag or 'noident')[:8]}"
|
||||
@@ -312,6 +338,16 @@ def promote(doc: dict, id: str, state: str, evidence_ref: str) -> dict | None:
|
||||
def alias(doc: dict, canonical_id: str, old_id: str) -> dict:
|
||||
"""RENAME: same conceptual entity, identifier changed. Old references
|
||||
stay resolvable (entity() follows alias entries)."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.alias(doc, canonical_id, old_id, now())
|
||||
except RuntimeError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
if isinstance(out, dict):
|
||||
doc.clear()
|
||||
doc.update(out)
|
||||
return entity(doc, canonical_id)
|
||||
e = entity(doc, canonical_id)
|
||||
if e is None:
|
||||
raise ValueError(f"alias target not found: {canonical_id}")
|
||||
@@ -344,11 +380,20 @@ def supersede(doc: dict, new_id: str, old_id: str, evidence_ref: str) -> dict:
|
||||
def spine_uri(entity_id: str) -> str:
|
||||
"""Stable cross-graph identity: spine://<kind>/<name>. A future Memory
|
||||
record cites this URI without duplicating structural authority."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.spine_uri(entity_id)
|
||||
kind, _, name = entity_id.partition(":")
|
||||
return f"spine://{kind}/{name}"
|
||||
|
||||
|
||||
def resolve_uri(uri: str) -> str:
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.resolve_uri(uri)
|
||||
except RuntimeError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
scheme, _, rest = uri.partition("://")
|
||||
if scheme != "spine":
|
||||
raise ValueError(f"not a spine URI: {uri}")
|
||||
@@ -460,6 +505,9 @@ def resolve_status(e: dict) -> str:
|
||||
|
||||
def authority_health(doc: dict) -> dict:
|
||||
"""Provenance/freshness per projector — provenance only, not monitoring."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.authority_health(doc)
|
||||
return {
|
||||
name: {
|
||||
"generation": a.get("generation", 0),
|
||||
@@ -478,6 +526,13 @@ def authority_health(doc: dict) -> dict:
|
||||
|
||||
def bind_task(doc: dict, task_id: str, entity_ids: list[str], evidence_ref: str = "") -> dict:
|
||||
"""TASK/ISSUE/INTENT BINDING: task → affects → entities. Deterministic."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
out = core.bind_task(doc, task_id, list(entity_ids), evidence_ref, now())
|
||||
if isinstance(out, dict):
|
||||
doc.clear()
|
||||
doc.update(out)
|
||||
return entity(doc, f"task:{task_id}") or {"id": f"task:{task_id}"}
|
||||
for eid in entity_ids:
|
||||
add_relationship(
|
||||
doc,
|
||||
@@ -490,6 +545,9 @@ def bind_task(doc: dict, task_id: str, entity_ids: list[str], evidence_ref: str
|
||||
|
||||
|
||||
def task_entities(doc: dict, task_id: str) -> list[str]:
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.task_entities(doc, task_id)
|
||||
return sorted(r["to"] for r in relationships(doc, from_id=f"task:{task_id}", type="affects"))
|
||||
|
||||
|
||||
@@ -502,6 +560,9 @@ def impact_frontier(doc: dict, seed_ids: list[str], max_level: int = 2, change_k
|
||||
contract_violation (escalates siblings to obligations). None infers from
|
||||
seed kind. Unknown relationship types are obligation-bearing (fail-loud);
|
||||
NON_OBLIGATION types and CANDIDATE/STALE facts are skipped."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.impact_frontier(doc, list(seed_ids), max_level, change_kind)
|
||||
seeds = list(dict.fromkeys(seed_ids))
|
||||
level0 = sorted(set(seeds))
|
||||
level1: set[str] = set()
|
||||
@@ -578,6 +639,13 @@ def record_observed_impact(doc: dict, task_id: str, evidence_items: list[dict])
|
||||
"""INDEPENDENT OBSERVED IMPACT: derive what was ACTUALLY affected from
|
||||
external evidence (tests, canaries, runtime, deploy) — never by re-running
|
||||
the frontier being evaluated. Nothing auto-promotes into structure."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
out = core.record_observed_impact(doc, task_id, list(evidence_items), now())
|
||||
if isinstance(out.get("doc"), dict):
|
||||
doc.clear()
|
||||
doc.update(out["doc"])
|
||||
return out["result"]
|
||||
implicated: set[str] = set()
|
||||
for item in evidence_items:
|
||||
eid = item.get("entity", "")
|
||||
@@ -599,6 +667,9 @@ def record_observed_impact(doc: dict, task_id: str, evidence_items: list[dict])
|
||||
|
||||
|
||||
def predicted_vs_observed(predicted: dict, observed_doc: dict, seed_ids: list[str]) -> dict:
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.predicted_vs_observed(predicted, observed_doc, list(seed_ids))
|
||||
observed = impact_frontier(observed_doc, seed_ids)
|
||||
pred_set = {
|
||||
e
|
||||
@@ -629,6 +700,9 @@ def predicted_vs_observed(predicted: dict, observed_doc: dict, seed_ids: list[st
|
||||
def structural_events(doc: dict) -> list[dict]:
|
||||
"""GENERIC EVENT/QUERY SEAM: dumb typed structural facts for future
|
||||
subscribers. No salience, no policy."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.structural_events(doc)
|
||||
events = []
|
||||
for e in doc["entities"]:
|
||||
events.append(
|
||||
@@ -657,6 +731,9 @@ def structural_events(doc: dict) -> list[dict]:
|
||||
|
||||
def diff(old: dict, new: dict) -> dict:
|
||||
"""Deterministic structural delta. No interpretation."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
return core.diff(old, new)
|
||||
oe = {e["id"]: e for e in old.get("entities", [])}
|
||||
ne = {e["id"]: e for e in new.get("entities", [])}
|
||||
ors = {(r["type"], r["from"], r["to"]) for r in old.get("relationships", [])}
|
||||
@@ -680,6 +757,13 @@ def diff(old: dict, new: dict) -> dict:
|
||||
def dedupe_observations(doc: dict) -> None:
|
||||
"""Exact-duplicate observation suppression (carry-forward + re-derivation
|
||||
both record; distinct repeats are history and stay)."""
|
||||
core = _ss_core()
|
||||
if core is not None:
|
||||
out = core.dedupe_observations(doc)
|
||||
if isinstance(out, dict):
|
||||
doc.clear()
|
||||
doc.update(out)
|
||||
return
|
||||
seen: set[str] = set()
|
||||
unique = []
|
||||
for o in doc.get("observations", []):
|
||||
|
||||
Generated
+28
@@ -2,6 +2,32 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "evidence_provenance"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -68,6 +94,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -79,6 +106,7 @@ dependencies = [
|
||||
name = "structural_store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"evidence_provenance",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -8,4 +8,7 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# preserve_order: Python dicts round-trip in insertion order — str() of a
|
||||
# detail dict and doc-echo order follow the oracle.
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
evidence_provenance = { path = "../evidence_provenance" }
|
||||
@@ -0,0 +1,352 @@
|
||||
//! frozen_battery — replay the FROZEN oracle truth VERBATIM
|
||||
//! (frozen_ss_truth.json, generated by scripts/gen_structural_store_truth.py
|
||||
//! against the LIVE Python oracle with RUNE_STRUCTURAL_CORE=python pinned,
|
||||
//! clock pinned via structural_store.now, inputs deep-copied per case).
|
||||
//!
|
||||
//! The canary never imports the Python oracle — parity stays provable after
|
||||
//! strangler exits.
|
||||
|
||||
use crate::*;
|
||||
use serde_json::Value;
|
||||
use std::cell::RefCell;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_ss_truth.json");
|
||||
const NOW: &str = "2026-10-13T01:02:03Z";
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(TRUTH).expect("frozen truth parses")
|
||||
}
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
truth()["groups"][group]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn nm(c: &Value) -> &str {
|
||||
c["name"].as_str().unwrap_or("")
|
||||
}
|
||||
|
||||
/// Recording I/O seam — save/snapshot write into memory, never the disk.
|
||||
struct RecIo {
|
||||
writes: RefCell<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl RecIo {
|
||||
fn new() -> Self {
|
||||
RecIo { writes: RefCell::new(vec![]) }
|
||||
}
|
||||
fn content(&self, path: &str) -> String {
|
||||
self.writes
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(p, _)| p == path)
|
||||
.map(|(_, c)| c.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl SsIo for RecIo {
|
||||
fn mkdir_parents(&self, _dir: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_text(&self, path: &str, contents: &str) -> Result<(), String> {
|
||||
self.writes.borrow_mut().push((path.to_string(), contents.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_err(c: &Value, got: Result<impl std::fmt::Debug, String>) {
|
||||
match c["expect"].get("err").and_then(|e| e.as_str()) {
|
||||
Some(want) => {
|
||||
let e = got.expect_err(&format!("case {} should raise", nm(c)));
|
||||
assert_eq!(e, want, "case {} error message", nm(c));
|
||||
}
|
||||
None => {
|
||||
got.unwrap_or_else(|e| panic!("case {} raised: {}", nm(c), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn de<'a>(c: &'a Value, keys: &[&str]) -> &'a Value {
|
||||
let mut cur = c;
|
||||
for k in keys {
|
||||
cur = &cur[k];
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
// ── pure normalizers ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn replay_sorted() {
|
||||
for c in &cases("sorted") {
|
||||
let got = _sorted(de(c, &["args", "value"]));
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_new_doc() {
|
||||
for c in &cases("new_doc") {
|
||||
let got = new_doc(NOW);
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_redact() {
|
||||
for c in &cases("redact") {
|
||||
let got = _redact_secrets(de(c, &["args", "value"]));
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_save() {
|
||||
for c in &cases("save") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let path = format!("out/{}/store.json", nm(c));
|
||||
let io = RecIo::new();
|
||||
let got = save(&mut doc, &path, &io);
|
||||
expect_err(c, got);
|
||||
if c["expect"].get("ok").is_some() {
|
||||
assert_eq!(io.content(&path), de(c, &["expect", "ok", "content"]).as_str().unwrap(), "case {} content", nm(c));
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {} doc", nm(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_digest() {
|
||||
for c in &cases("digest") {
|
||||
let got = canonical_digest(de(c, &["args", "doc"]));
|
||||
let want = de(c, &["expect", "ok"]);
|
||||
if want.is_array() {
|
||||
// multi-assert cases: digests of related docs must agree pairwise
|
||||
let digests: Vec<Value> = if c["args"].get("doc_reordered").is_some() {
|
||||
vec![
|
||||
json!(canonical_digest(de(c, &["args", "doc"]))),
|
||||
json!(canonical_digest(de(c, &["args", "doc_reordered"]))),
|
||||
]
|
||||
} else if c["args"].get("doc").is_some() && c["name"] == "secret_never_changes_digest" {
|
||||
let d = de(c, &["args", "doc"]);
|
||||
let d2 = { let mut x = d.clone(); x["token"] = json!("other"); x };
|
||||
vec![json!(canonical_digest(d)), json!(canonical_digest(&d2))]
|
||||
} else {
|
||||
vec![json!(got.clone())]
|
||||
};
|
||||
assert_eq!(json!(digests), want.clone(), "case {}", nm(c));
|
||||
} else {
|
||||
assert_eq!(json!(got), want.clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_snapshot() {
|
||||
for c in &cases("snapshot") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let dir = de(c, &["args", "dir"]).as_str().unwrap();
|
||||
let label = de(c, &["args", "label"]).as_str().unwrap();
|
||||
let tag = de(c, &["args", "tag"]).as_str().unwrap();
|
||||
let io = RecIo::new();
|
||||
let got = snapshot(&mut doc, dir, label, tag, NOW, &io).expect("snapshot ok");
|
||||
assert_eq!(json!(got), de(c, &["expect", "ok", "path"]).clone(), "case {} path", nm(c));
|
||||
assert_eq!(io.content(&got), de(c, &["expect", "ok", "content"]).as_str().unwrap(), "case {} content", nm(c));
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {} doc", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_alias() {
|
||||
for c in &cases("alias") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let cid = de(c, &["args", "cid"]).as_str().unwrap();
|
||||
let old = de(c, &["args", "old"]).as_str().unwrap();
|
||||
let got = alias(&mut doc, cid, old, NOW);
|
||||
match c["expect"].get("err") {
|
||||
Some(_) => expect_err(c, got),
|
||||
None => assert_eq!(got.expect("alias ok"), de(c, &["expect", "ok"]).clone(), "case {}", nm(c)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_spine_uri() {
|
||||
for c in &cases("spine_uri") {
|
||||
let got = spine_uri(de(c, &["args", "entity_id"]).as_str().unwrap());
|
||||
assert_eq!(json!(got), de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_resolve_uri() {
|
||||
for c in &cases("resolve_uri") {
|
||||
let got = resolve_uri(de(c, &["args", "uri"]).as_str().unwrap());
|
||||
expect_err(c, got.clone());
|
||||
if c["expect"].get("ok").is_some() {
|
||||
assert_eq!(json!(got.unwrap()), de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_doc_authorities() {
|
||||
for c in &cases("doc_authorities") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let ret = _doc_authorities(&mut doc).clone();
|
||||
assert_eq!(json!(ret), de(c, &["expect", "ok", "ret"]).clone(), "case {} ret", nm(c));
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {} doc", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_authority_health() {
|
||||
for c in &cases("authority_health") {
|
||||
let got = authority_health(de(c, &["args", "doc"]));
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_bind_task() {
|
||||
for c in &cases("bind_task") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let task_id = de(c, &["args", "task_id"]).as_str().unwrap();
|
||||
let ids: Vec<String> = de(c, &["args", "entity_ids"])
|
||||
.as_array().unwrap()
|
||||
.iter().map(|x| x.as_str().unwrap().to_string()).collect();
|
||||
let ev = de(c, &["args", "evidence_ref"]).as_str().unwrap();
|
||||
bind_task(&mut doc, task_id, &ids, ev, NOW);
|
||||
let ret = entity_ref(&doc, &format!("task:{}", task_id))
|
||||
.cloned()
|
||||
.unwrap_or(json!({"id": format!("task:{}", task_id)}));
|
||||
assert_eq!(json!(ret), de(c, &["expect", "ok", "ret"]).clone(), "case {} ret", nm(c));
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {} doc", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_task_entities() {
|
||||
for c in &cases("task_entities") {
|
||||
let got = task_entities(de(c, &["args", "doc"]), de(c, &["args", "task_id"]).as_str().unwrap());
|
||||
assert_eq!(json!(got), de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_impact_frontier() {
|
||||
for c in &cases("impact_frontier") {
|
||||
let got = impact_frontier(
|
||||
de(c, &["args", "doc"]),
|
||||
&de(c, &["args", "seeds"]).as_array().unwrap()
|
||||
.iter().map(|x| x.as_str().unwrap().to_string()).collect::<Vec<_>>(),
|
||||
de(c, &["args", "max_level"]).as_i64().unwrap_or(2),
|
||||
de(c, &["args", "change_kind"]).as_str(),
|
||||
);
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_observed_impact() {
|
||||
for c in &cases("observed_impact") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let items: Vec<Value> = de(c, &["args", "items"]).as_array().unwrap().clone();
|
||||
let got = record_observed_impact(&mut doc, "T1", &items, NOW);
|
||||
assert_eq!(json!(got), de(c, &["expect", "ok", "result"]).clone(), "case {} result", nm(c));
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {} doc", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_predicted_vs_observed() {
|
||||
for c in &cases("predicted_vs_observed") {
|
||||
let seeds: Vec<String> = de(c, &["args", "seeds"]).as_array().unwrap()
|
||||
.iter().map(|x| x.as_str().unwrap().to_string()).collect();
|
||||
let got = predicted_vs_observed(de(c, &["args", "predicted"]), de(c, &["args", "observed_doc"]), &seeds);
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_structural_events() {
|
||||
for c in &cases("events") {
|
||||
let got = structural_events(de(c, &["args", "doc"]));
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_diff() {
|
||||
for c in &cases("diff") {
|
||||
let got = diff(de(c, &["args", "old"]), de(c, &["args", "new"]));
|
||||
assert_eq!(got, de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_dedupe() {
|
||||
for c in &cases("dedupe") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
dedupe_observations(&mut doc);
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {} doc", nm(c));
|
||||
assert!(de(c, &["expect", "ok", "ret"]).is_null(), "case {} ret", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
// ── fixed-kernel regressions ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn replay_promote_fix() {
|
||||
for c in &cases("promote_fix") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let id = de(c, &["args", "id"]).as_str().unwrap();
|
||||
let state = de(c, &["args", "state"]).as_str().unwrap();
|
||||
let ev = de(c, &["args", "ev"]).as_str().unwrap();
|
||||
let got = promote(&mut doc, id, state, ev, NOW);
|
||||
let ret = de(c, &["expect", "ok", "ret"]);
|
||||
let want_doc = de(c, &["expect", "ok", "doc"]);
|
||||
if ret.is_null() {
|
||||
assert_eq!(got, Ok(None), "case {} ret", nm(c));
|
||||
} else {
|
||||
// oracle promote returns the ENTITY; the ret maturity is the
|
||||
// observed outcome (doc compare below proves the full shape)
|
||||
let got_str = got.as_ref().ok().and_then(|o| o.as_deref());
|
||||
assert_eq!(got_str, Some(ret["maturity"].as_str().unwrap()), "case {} ret", nm(c));
|
||||
}
|
||||
assert_eq!(doc, want_doc.clone(), "case {} doc", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_resolve_status_fix() {
|
||||
for c in &cases("resolve_status_fix") {
|
||||
let got = resolve_status(de(c, &["args", "entity"]));
|
||||
assert_eq!(json!(got), de(c, &["expect", "ok"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_supersede_fix() {
|
||||
for c in &cases("supersede_fix") {
|
||||
let mut doc = de(c, &["args", "doc"]).clone();
|
||||
let nid = de(c, &["args", "new"]).as_str().unwrap();
|
||||
let oid = de(c, &["args", "old"]).as_str().unwrap();
|
||||
let ev = de(c, &["args", "ev"]).as_str().unwrap();
|
||||
supersede(&mut doc, nid, oid, ev, NOW).expect("supersede ok");
|
||||
assert_eq!(doc, de(c, &["expect", "ok", "doc"]).clone(), "case {}", nm(c));
|
||||
}
|
||||
}
|
||||
|
||||
// ── the core probe mirror (RUNE_STRUCTURAL_CORE=python pin) ─────────────────
|
||||
|
||||
#[test]
|
||||
fn ss_core_pin_semantics() {
|
||||
assert!(_ss_core(Some("python")).is_none(), "python pin disables the core");
|
||||
assert!(_ss_core(None).is_some(), "unset pin activates the core");
|
||||
assert!(_ss_core(Some("auto")).is_some(), "auto activates the core");
|
||||
assert_eq!(_ss_core(Some("auto")).unwrap().id(), "rust");
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,9 @@
|
||||
//! PARITY port of the core data structures from src/rune/structural_store.py.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const SCHEMA_VERSION: i32 = 3;
|
||||
// Oracle law: structural_store.SCHEMA_VERSION == 1 (shared with spine stores).
|
||||
// (Previous port carried a stale 3 — corrected against the live oracle.)
|
||||
pub const SCHEMA_VERSION: i32 = 1;
|
||||
pub const MATURITY_ORDER: &[&str] = &["UNRUN", "DECLARED", "PRESENT", "VERIFIED"];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -75,7 +77,7 @@ impl Default for StoreDoc {
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
pub(crate) fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
@@ -85,7 +87,7 @@ fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
pub(crate) fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
let body = match value {
|
||||
Ok(v) => serde_json::json!({"ok": true, "value": v}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
@@ -95,11 +97,11 @@ fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn parse_arg(text: &str) -> Result<serde_json::Value, String> {
|
||||
pub(crate) fn parse_arg(text: &str) -> Result<serde_json::Value, String> {
|
||||
serde_json::from_str(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
fn get_str<'a>(v: &'a serde_json::Value, key: &str) -> Option<&'a str> {
|
||||
pub(crate) fn get_str<'a>(v: &'a serde_json::Value, key: &str) -> Option<&'a str> {
|
||||
v.get(key).and_then(|x| x.as_str())
|
||||
}
|
||||
|
||||
@@ -223,7 +225,9 @@ pub fn is_known_maturity(state: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Evidence-gated promotion. Demotion is never silent: recorded, not applied.
|
||||
/// Returns Ok(Some(updated maturity)) / Ok(demotion-rejected marker) / Err.
|
||||
/// Oracle contract order: unknown id → Ok(None) (silent, the caller surfaces
|
||||
/// the miss); then evidence required; then known state required. Entity
|
||||
/// resolution follows alias entries (oracle entity()).
|
||||
pub fn promote(
|
||||
doc: &mut serde_json::Value,
|
||||
id: &str,
|
||||
@@ -231,8 +235,8 @@ pub fn promote(
|
||||
evidence_ref: &str,
|
||||
now_iso: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
if !is_known_maturity(state) {
|
||||
return Err(format!("unknown maturity state: {}", state));
|
||||
if !crate::gap::has_entity(doc, id) {
|
||||
return Ok(None);
|
||||
}
|
||||
if evidence_ref.is_empty() {
|
||||
return Err(format!(
|
||||
@@ -240,13 +244,10 @@ pub fn promote(
|
||||
id, state
|
||||
));
|
||||
}
|
||||
let current = doc
|
||||
.get("entities")
|
||||
.and_then(|e| e.as_array())
|
||||
.and_then(|arr| {
|
||||
arr.iter()
|
||||
.find(|e| e.get("id").and_then(|x| x.as_str()) == Some(id))
|
||||
})
|
||||
if !is_known_maturity(state) {
|
||||
return Err(format!("unknown maturity state: {}", state));
|
||||
}
|
||||
let current = crate::gap::entity_ref(doc, id)
|
||||
.and_then(|e| e.get("maturity"))
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("DECLARED")
|
||||
@@ -264,7 +265,7 @@ pub fn promote(
|
||||
}
|
||||
return Ok(Some(current));
|
||||
}
|
||||
let entity = find_entity_mut(doc, id).ok_or_else(|| "entity-not-found".to_string())?;
|
||||
let entity = crate::gap::entity_mut(doc, id).ok_or_else(|| "entity-not-found".to_string())?;
|
||||
if let Some(e) = entity.as_object_mut() {
|
||||
e.insert("maturity".into(), serde_json::json!(state));
|
||||
let mut refs: Vec<String> = e
|
||||
@@ -285,37 +286,23 @@ pub fn promote(
|
||||
Ok(Some(state.to_string()))
|
||||
}
|
||||
|
||||
fn find_entity_mut<'a>(
|
||||
doc: &'a mut serde_json::Value,
|
||||
id: &str,
|
||||
) -> Option<&'a mut serde_json::Value> {
|
||||
doc.get_mut("entities")
|
||||
.and_then(|e| e.as_array_mut())?
|
||||
.iter_mut()
|
||||
.find(|e| e.get("id").and_then(|x| x.as_str()) == Some(id))
|
||||
}
|
||||
|
||||
fn now_iso() -> String {
|
||||
// oracle: datetime.now(timezone.utc).isoformat() — the digest tests mask
|
||||
// timestamps; the store's monotonicity doesn't depend on the value.
|
||||
"1970-01-01T00:00:00+00:00".into()
|
||||
}
|
||||
|
||||
/// resolve_status — smallest coherent entity state (oracle-identical).
|
||||
pub fn resolve_status(e: &serde_json::Value) -> &'static str {
|
||||
/// An explicit `status` passes through VERBATIM (oracle: `if e.get("status")`
|
||||
/// returns it as-is); ext.conflicts is Python-truthy (non-empty), not
|
||||
/// merely present.
|
||||
pub fn resolve_status(e: &serde_json::Value) -> String {
|
||||
if let Some(s) = e.get("status").and_then(|s| s.as_str()) {
|
||||
if !s.is_empty() {
|
||||
// static return tied to input would need leaks; return via match
|
||||
return match_status(s);
|
||||
return s.to_string();
|
||||
}
|
||||
}
|
||||
let has_conflicts = e
|
||||
.get("ext")
|
||||
.and_then(|x| x.get("conflicts"))
|
||||
.map(|c| !c.is_null())
|
||||
.map(py_truthy)
|
||||
.unwrap_or(false);
|
||||
if has_conflicts {
|
||||
return "CONFLICTING";
|
||||
return "CONFLICTING".to_string();
|
||||
}
|
||||
let maturity = e.get("maturity").and_then(|m| m.as_str()).unwrap_or("");
|
||||
let has_evidence = e
|
||||
@@ -324,25 +311,29 @@ pub fn resolve_status(e: &serde_json::Value) -> &'static str {
|
||||
.map(|a| !a.is_empty())
|
||||
.unwrap_or(false);
|
||||
if (maturity == "EXERCISED" || maturity == "LIVE_PROVEN") && has_evidence {
|
||||
return "CURRENT";
|
||||
return "CURRENT".to_string();
|
||||
}
|
||||
if !maturity.is_empty() && maturity != "DECLARED" && !has_evidence {
|
||||
return "CONFLICTING";
|
||||
return "CONFLICTING".to_string();
|
||||
}
|
||||
"UNKNOWN"
|
||||
"UNKNOWN".to_string()
|
||||
}
|
||||
|
||||
fn match_status(s: &str) -> &'static str {
|
||||
match s {
|
||||
"SUPERSEDED" => "SUPERSEDED",
|
||||
"CURRENT" => "CURRENT",
|
||||
"CONFLICTING" => "CONFLICTING",
|
||||
"UNKNOWN" => "UNKNOWN",
|
||||
_ => "UNKNOWN",
|
||||
/// Python truthiness for the JSON values resolve_status tests.
|
||||
fn py_truthy(v: &serde_json::Value) -> bool {
|
||||
match v {
|
||||
serde_json::Value::Null => false,
|
||||
serde_json::Value::Bool(b) => *b,
|
||||
serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
|
||||
serde_json::Value::String(s) => !s.is_empty(),
|
||||
serde_json::Value::Array(a) => !a.is_empty(),
|
||||
serde_json::Value::Object(o) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// supersede — replacement with evidence; old identity stays resolvable.
|
||||
/// Entity resolution follows alias entries; the supersedes relationship goes
|
||||
/// through the oracle add_relationship law (evidence_refs + observed_at).
|
||||
pub fn supersede(
|
||||
doc: &mut serde_json::Value,
|
||||
new_id: &str,
|
||||
@@ -353,17 +344,21 @@ pub fn supersede(
|
||||
if evidence_ref.is_empty() {
|
||||
return Err("supersession requires evidence".into());
|
||||
}
|
||||
if find_entity(doc, new_id).is_none() || find_entity(doc, old_id).is_none() {
|
||||
if !crate::gap::has_entity(doc, new_id) || !crate::gap::has_entity(doc, old_id) {
|
||||
return Err("supersede requires both entities to exist".into());
|
||||
}
|
||||
doc.get_mut("relationships")
|
||||
.and_then(|r| r.as_array_mut())
|
||||
.unwrap_or(&mut Vec::new())
|
||||
.push(serde_json::json!({
|
||||
"type": "supersedes", "from": new_id, "to": old_id,
|
||||
"evidence_refs": [evidence_ref],
|
||||
}));
|
||||
if let Some(old) = find_entity_mut(doc, old_id) {
|
||||
crate::gap::add_relationship(
|
||||
doc,
|
||||
crate::gap::RelSpec {
|
||||
rel_type: "supersedes",
|
||||
from_id: new_id,
|
||||
to_id: old_id,
|
||||
evidence_refs: &[evidence_ref.to_string()],
|
||||
ext: None,
|
||||
now_iso,
|
||||
},
|
||||
);
|
||||
if let Some(old) = crate::gap::entity_mut(doc, old_id) {
|
||||
if let Some(o) = old.as_object_mut() {
|
||||
o.insert("superseded_by".into(), serde_json::json!(new_id));
|
||||
o.insert("status".into(), serde_json::json!("SUPERSEDED"));
|
||||
@@ -378,6 +373,7 @@ pub fn supersede(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn find_entity<'a>(doc: &'a serde_json::Value, id: &str) -> Option<&'a serde_json::Value> {
|
||||
doc.get("entities")
|
||||
.and_then(|e| e.as_array())?
|
||||
@@ -480,6 +476,7 @@ pub fn authority_observed(
|
||||
doc: &mut serde_json::Value,
|
||||
rel: Option<&mut serde_json::Value>,
|
||||
authority: &str,
|
||||
now_iso: &str,
|
||||
) {
|
||||
let auths = doc
|
||||
.as_object_mut()
|
||||
@@ -493,12 +490,12 @@ pub fn authority_observed(
|
||||
.or_insert(json!({"generation": 0, "observed": []}));
|
||||
let generation = a["generation"].as_i64().unwrap_or(0) + if rel.is_none() { 1 } else { 0 };
|
||||
a["generation"] = json!(generation);
|
||||
a["last_success"] = json!(now_iso());
|
||||
a["last_success"] = json!(now_iso);
|
||||
a["status"] = json!("CURRENT");
|
||||
if let Some(rel) = rel {
|
||||
rel["authority"] = json!(authority);
|
||||
rel["status"] = json!("CURRENT");
|
||||
rel["observed_at"] = json!(now_iso());
|
||||
rel["observed_at"] = json!(now_iso);
|
||||
let oid = format!(
|
||||
"{}|{}|{}",
|
||||
rel["type"].as_str().unwrap_or(""),
|
||||
@@ -513,8 +510,9 @@ pub fn authority_observed(
|
||||
}
|
||||
|
||||
/// authority_failed — the authority's facts go STALE (not silently fresh);
|
||||
/// the authority is recorded DEGRADED with the error.
|
||||
pub fn authority_failed(doc: &mut serde_json::Value, authority: &str, error: &str) {
|
||||
/// the authority is recorded DEGRADED with the error, and the degradation is
|
||||
/// OBSERVED (oracle appends the authority_degraded observation).
|
||||
pub fn authority_failed(doc: &mut serde_json::Value, authority: &str, error: &str, now_iso: &str) {
|
||||
let auths = doc
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
@@ -525,8 +523,8 @@ pub fn authority_failed(doc: &mut serde_json::Value, authority: &str, error: &st
|
||||
.unwrap()
|
||||
.entry(authority.to_string())
|
||||
.or_insert(json!({"generation": 0, "observed": []}));
|
||||
a["last_attempt"] = json!(now_iso());
|
||||
a["last_error"] = json!(&error[..error.len().min(200)]);
|
||||
a["last_attempt"] = json!(now_iso);
|
||||
a["last_error"] = json!(&error[..error.chars().count().min(200)]);
|
||||
a["status"] = json!("DEGRADED");
|
||||
if let Some(rels) = doc.get_mut("relationships").and_then(|r| r.as_array_mut()) {
|
||||
for rel in rels.iter_mut() {
|
||||
@@ -535,6 +533,13 @@ pub fn authority_failed(doc: &mut serde_json::Value, authority: &str, error: &st
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(obs) = doc.get_mut("observations").and_then(|o| o.as_array_mut()) {
|
||||
obs.push(json!({
|
||||
"kind": "authority_degraded", "authority": authority,
|
||||
"error": &error[..error.chars().count().min(200)],
|
||||
"observed_at": now_iso,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// reconcile_authorities — facts owned by an authority that did not
|
||||
@@ -599,7 +604,8 @@ pub fn reconcile_authorities(doc: &mut serde_json::Value) -> Vec<serde_json::Val
|
||||
}
|
||||
|
||||
/// mark_conflict — BOTH observations preserved; entity CONFLICTING; never
|
||||
/// last-wins (oracle-identical).
|
||||
/// last-wins (oracle-identical). Entity resolution follows alias entries.
|
||||
#[allow(clippy::too_many_arguments)] // oracle signature: the two claims travel together
|
||||
pub fn mark_conflict(
|
||||
doc: &mut serde_json::Value,
|
||||
entity_id: &str,
|
||||
@@ -608,14 +614,15 @@ pub fn mark_conflict(
|
||||
authority_b: &str,
|
||||
claim_b: &str,
|
||||
evidence_ref: &str,
|
||||
now_iso: &str,
|
||||
) -> Result<(), String> {
|
||||
if find_entity_mut(doc, entity_id).is_none() {
|
||||
if !crate::gap::has_entity(doc, entity_id) {
|
||||
return Err(format!("conflict on unknown entity: {}", entity_id));
|
||||
}
|
||||
if evidence_ref.is_empty() {
|
||||
return Err("conflict recording requires evidence".into());
|
||||
}
|
||||
let entity = find_entity_mut(doc, entity_id).unwrap();
|
||||
let entity = crate::gap::entity_mut(doc, entity_id).unwrap();
|
||||
let e = entity.as_object_mut().unwrap();
|
||||
let ext = e.entry("ext").or_insert(json!({}));
|
||||
let conflicts = ext
|
||||
@@ -626,7 +633,7 @@ pub fn mark_conflict(
|
||||
conflicts.as_array_mut().unwrap().push(json!({
|
||||
"authority_a": authority_a, "claim_a": claim_a,
|
||||
"authority_b": authority_b, "claim_b": claim_b,
|
||||
"evidence": evidence_ref, "observed_at": now_iso(),
|
||||
"evidence": evidence_ref, "observed_at": now_iso,
|
||||
}));
|
||||
e.insert("status".into(), json!("CONFLICTING"));
|
||||
if let Some(obs) = doc.get_mut("observations").and_then(|o| o.as_array_mut()) {
|
||||
@@ -634,7 +641,7 @@ pub fn mark_conflict(
|
||||
"kind": "authority_conflict", "entity": entity_id,
|
||||
"authority_a": authority_a, "claim_a": claim_a,
|
||||
"authority_b": authority_b, "claim_b": claim_b,
|
||||
"observed_at": now_iso(),
|
||||
"observed_at": now_iso,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
@@ -654,7 +661,7 @@ mod authority_battery {
|
||||
fn authority_observed_lifecycle() {
|
||||
let mut d = doc();
|
||||
let mut rel = json!({"type": "owns", "from": "a", "to": "b"});
|
||||
authority_observed(&mut d, Some(&mut rel), "spine");
|
||||
authority_observed(&mut d, Some(&mut rel), "spine", "2026-10-13T00:00:00Z");
|
||||
assert_eq!(rel["status"], "CURRENT");
|
||||
assert_eq!(rel["authority"], "spine");
|
||||
let gen = d["authority_generations"]["spine"]["generation"]
|
||||
@@ -662,7 +669,7 @@ mod authority_battery {
|
||||
.unwrap();
|
||||
assert_eq!(gen, 0); // rel observation does NOT bump the generation
|
||||
// bare observation bumps
|
||||
authority_observed(&mut d, None, "spine");
|
||||
authority_observed(&mut d, None, "spine", "2026-10-13T00:00:00Z");
|
||||
assert_eq!(
|
||||
d["authority_generations"]["spine"]["generation"]
|
||||
.as_i64()
|
||||
@@ -678,10 +685,16 @@ mod authority_battery {
|
||||
{"type": "owns", "from": "a", "to": "b", "authority": "spine", "status": "CURRENT"},
|
||||
{"type": "owns", "from": "c", "to": "d", "status": "CURRENT"},
|
||||
]);
|
||||
authority_failed(&mut d, "spine", "boom");
|
||||
authority_failed(&mut d, "spine", "boom", "2026-10-13T00:00:00Z");
|
||||
assert_eq!(d["relationships"][0]["status"], "STALE");
|
||||
assert_eq!(d["relationships"][1]["status"], "CURRENT"); // unowned untouched
|
||||
assert_eq!(d["authority_generations"]["spine"]["status"], "DEGRADED");
|
||||
// the degradation is OBSERVED, not silent (oracle parity fix)
|
||||
assert!(d["observations"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|o| o["kind"] == "authority_degraded" && o["authority"] == "spine"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -710,6 +723,7 @@ mod authority_battery {
|
||||
"b",
|
||||
"claim two",
|
||||
"ev-1",
|
||||
"2026-10-13T00:00:00Z",
|
||||
)
|
||||
.expect("conflict ok");
|
||||
let e = &d["entities"][0];
|
||||
@@ -718,7 +732,15 @@ mod authority_battery {
|
||||
let c = &e["ext"]["conflicts"][0];
|
||||
assert_eq!(c["claim_a"], "claim one");
|
||||
assert_eq!(c["claim_b"], "claim two"); // BOTH preserved, never last-wins
|
||||
assert!(mark_conflict(&mut d, "nope", "a", "x", "b", "y", "ev").is_err());
|
||||
assert!(mark_conflict(&mut d, "module:x", "a", "x", "b", "y", "").is_err());
|
||||
assert!(mark_conflict(&mut d, "nope", "a", "x", "b", "y", "ev", "t").is_err());
|
||||
assert!(mark_conflict(&mut d, "module:x", "a", "x", "b", "y", "", "t").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
// ── dichotomy-wave gap-closing port (contract factory-gap-ss) ──────────────
|
||||
|
||||
pub mod gap;
|
||||
pub use gap::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod frozen_battery;
|
||||
+13556
-23
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-13T00:23:43Z-53857a-factory-batch-i
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Complete the self_model dichotomy port to FULL Rust (#2479 tranche): the bounded persistent self-model kernel — estimate_tokens / compile_self_model (greedy weight-fill with stable tie ordering and evidence overflow) / why_question — as doc-envelope FFI (smm_* symbols, free via capability_scoring cs_free_string — no free re-export); smm_loader mirroring broker_loader.py; facade keeps the Python floor, RUNE_SMM_CORE=python pin; frozen-oracle battery (every branch: empty corpus, exact-fit boundary, evidence overflow, key defaults, stable weight ties, zero budget, why-match across statements+evidence); live parity 100% (frozen + fuzz seeds); Phase B language-advantage pass (ASCII byte fast-path token estimate).
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-13T07:06:01Z", "contract": "BUILDER-2026-10-13T00:23:43Z-53857a-factory-batch-i", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/close_contract_self_model.py", "scripts/gen_self_model_truth.py", "scripts/walk_contract_self_model.py", "src/rune/self_model.py", "src/rune/smm_loader.py", "src/rust/self_model/frozen_sm_truth.json", "src/rust/self_model/src/branch_battery.rs", "src/rust/self_model/src/lib.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-batch-i
|
||||
- builder:factory-batch-i
|
||||
- builder:factory-batch-i
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-13T02:02:02Z-dad27f-factory-gap-bm
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Close build_mode Go port PARTIAL gaps (dichotomy factory, issue #2479 tranche): port _load_state/_save_state/assess_complexity + the full start_* phase-transition family name-preserving into the existing Go impl, shared freeze_truth harness battery over every new public fn, live parity 100%, Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-13T07:55:21Z", "contract": "BUILDER-2026-10-13T02:02:02Z-dad27f-factory-gap-bm", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/freeze_truth.py", "scripts/gen_build_mode_gap_truth.py", "src/go/build-mode/build_mode.go", "src/go/build-mode/build_mode_gap.go", "src/go/build-mode/phaseb.go", "src/go/build-mode/phaseb_test.go", "src/go/build-mode/truth_buildmode_gap.json", "src/go/build-mode/truth_buildmode_gap_test.go", "src/rune/build_mode.py"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-gap-bm
|
||||
- builder:factory-gap-bm
|
||||
- builder:factory-gap-bm
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-13T03:16:06Z-57d4cc-factory-gap-nm
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Close the night_mode dichotomy PARTIAL gaps (audit-named): Go port of _ctx_budget, get_night_mode, start_night_mode, stop_night_mode, get_status, get_report, to_dict, _load, _save, _run_loop, _run_sleep, _run_evaluation (name-preserving, logic 1:1) in src/go/night-mode; pure/IO split law with injectable clock/sleeper/runner/HTTP seams; frozen-oracle truth battery via shared freeze_truth.py embedded VERBATIM in Go tests; live parity 100% on new fns; Python facade stays fully functional (RUNE_NIGHT_MODE_CORE=python pin + auto both green).
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-13T08:04:32Z", "contract": "BUILDER-2026-10-13T03:16:06Z-57d4cc-factory-gap-nm", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/check_night_mode_parity.py", "scripts/close_contract_night_mode.py", "scripts/gen_night_mode_truth.py", "scripts/open_contract_night_mode.py", "src/go/night-mode/frozen_truth.json", "src/go/night-mode/main.go", "src/go/night-mode/night_mode_test.go", "src/rune/night_mode.py", "tests/test_night_mode.py"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-gap-nm
|
||||
- builder:factory-gap-nm
|
||||
- builder:factory-gap-nm
|
||||
- closure-record
|
||||
@@ -0,0 +1,444 @@
|
||||
"""Night mode facade tests — dichotomy wave gap closure.
|
||||
|
||||
The Go core (src/go/night-mode) ports the night_mode state machine 1:1;
|
||||
this battery pins the Python oracle itself, both ways:
|
||||
* RUNE_NIGHT_MODE_CORE=python (pinned) and unset (auto) — both green.
|
||||
All IO is injected (frozen clock, fake urlopen/subprocess, no-op loop) —
|
||||
no live Ollama, no 8-hour threads, no real memory dir.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
import rune.night_mode as nm # noqa: E402
|
||||
|
||||
FROZEN_NOW = datetime(2026, 10, 13, 3, 0, 0, tzinfo=timezone.utc)
|
||||
FROZEN_ISO = FROZEN_NOW.isoformat()
|
||||
|
||||
|
||||
class FrozenDateTime(datetime):
|
||||
"""Clock law: now() pinned, timezone-aware like datetime.now(timezone.utc)."""
|
||||
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return FROZEN_NOW
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
"""Context-manager body like urllib.request.urlopen(...)."""
|
||||
|
||||
def __init__(self, payload):
|
||||
self._payload = json.dumps(payload).encode()
|
||||
|
||||
def read(self):
|
||||
return self._payload
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh(tmp_path, monkeypatch):
|
||||
"""NightMode over a temp memory dir; background thread defused (start()
|
||||
never actually spawns the loop — tests invoke _run_loop directly)."""
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
|
||||
class DummyThread:
|
||||
def __init__(self, target=None, **kwargs):
|
||||
self.target = target
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(nm.threading, "Thread", DummyThread)
|
||||
return nm.NightMode(storage_path=str(tmp_path / "night_mode.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clock(monkeypatch):
|
||||
monkeypatch.setattr(nm, "datetime", FrozenDateTime)
|
||||
|
||||
|
||||
def fake_urlopen(monkeypatch, payload=None, exc=None, capture=None):
|
||||
"""Patch urllib.request.urlopen; capture request objects when asked."""
|
||||
|
||||
def _open(req, timeout=None):
|
||||
if capture is not None:
|
||||
capture.append(req)
|
||||
if exc is not None:
|
||||
raise exc
|
||||
return FakeResponse(payload if payload is not None else {"response": ""})
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", _open)
|
||||
|
||||
|
||||
class TestToDict:
|
||||
def test_shape_and_defaults(self):
|
||||
d = nm.NightModeState().to_dict()
|
||||
assert list(d.keys()) == [
|
||||
"active", "started_at", "duration_hours", "current_phase",
|
||||
"iterations", "improvements", "questions", "decisions", "errors",
|
||||
]
|
||||
assert d == {
|
||||
"active": False, "started_at": None, "duration_hours": 8.0,
|
||||
"current_phase": "idle", "iterations": 0, "improvements": [],
|
||||
"questions": [], "decisions": [], "errors": [],
|
||||
}
|
||||
|
||||
|
||||
class TestLoadSave:
|
||||
def test_save_bytes_and_atomicity(self, fresh):
|
||||
fresh._save()
|
||||
data = Path(fresh.storage_path).read_text()
|
||||
assert data == (
|
||||
'{\n "active": false,\n "started_at": null,\n'
|
||||
' "duration_hours": 8.0,\n "current_phase": "idle",\n'
|
||||
' "iterations": 0,\n "improvements": [],\n "questions": [],\n'
|
||||
' "decisions": [],\n "errors": []\n}'
|
||||
)
|
||||
assert not Path(fresh.storage_path + ".tmp").exists()
|
||||
|
||||
def test_load_roundtrip_and_key_filtering(self, fresh):
|
||||
fresh.state.iterations = 3
|
||||
fresh.state.improvements.append("did a thing")
|
||||
fresh._save()
|
||||
other = nm.NightMode(storage_path=fresh.storage_path)
|
||||
assert other.state.iterations == 3
|
||||
assert other.state.improvements == ["did a thing"]
|
||||
|
||||
# unknown keys dropped, missing keys keep dataclass defaults
|
||||
Path(fresh.storage_path).write_text(
|
||||
json.dumps({"active": True, "iterations": 2, "bogus": 1})
|
||||
)
|
||||
loaded = nm.NightMode(storage_path=fresh.storage_path)
|
||||
assert loaded.state.active is True and loaded.state.iterations == 2
|
||||
assert loaded.state.duration_hours == 8.0
|
||||
assert loaded.state.current_phase == "idle"
|
||||
assert loaded.state.improvements == []
|
||||
|
||||
def test_corrupt_file_keeps_fresh_state(self, fresh):
|
||||
Path(fresh.storage_path).write_text("{not json")
|
||||
loaded = nm.NightMode(storage_path=fresh.storage_path)
|
||||
assert loaded.state.active is False and loaded.state.iterations == 0
|
||||
|
||||
def test_missing_file_keeps_fresh_state(self, tmp_path):
|
||||
loaded = nm.NightMode(storage_path=str(tmp_path / "absent.json"))
|
||||
assert loaded.state.current_phase == "idle"
|
||||
|
||||
|
||||
class TestStartStop:
|
||||
def test_start_success_shape(self, fresh, clock):
|
||||
r = fresh.start(8.0)
|
||||
assert r["success"] is True
|
||||
assert r["message"] == "Night mode started for 8.0 hours"
|
||||
st = r["state"]
|
||||
assert st["active"] is True and st["current_phase"] == "starting"
|
||||
assert st["started_at"] == FROZEN_ISO and st["duration_hours"] == 8.0
|
||||
assert st["iterations"] == 0 and st["improvements"] == []
|
||||
on_disk = json.loads(Path(fresh.storage_path).read_text())
|
||||
assert on_disk["active"] is True
|
||||
|
||||
def test_start_fractional_hours_message(self, fresh, clock):
|
||||
assert fresh.start(4.5)["message"] == "Night mode started for 4.5 hours"
|
||||
|
||||
def test_start_resets_lists(self, fresh, clock):
|
||||
fresh.state.improvements = ["stale"]
|
||||
fresh.start(8.0)
|
||||
assert fresh.state.improvements == []
|
||||
|
||||
def test_start_idempotence_guard(self, fresh, clock):
|
||||
fresh.start(8.0)
|
||||
assert fresh.start(4.0) == {
|
||||
"success": False, "error": "Night mode already active"
|
||||
}
|
||||
|
||||
def test_stop_marks_stopping_and_saves(self, fresh, clock):
|
||||
fresh.start(8.0)
|
||||
r = fresh.stop()
|
||||
assert r["success"] is True
|
||||
assert r["message"] == "Night mode stopping..."
|
||||
assert r["state"]["active"] is False
|
||||
assert r["state"]["current_phase"] == "stopping"
|
||||
on_disk = json.loads(Path(fresh.storage_path).read_text())
|
||||
assert on_disk["active"] is False
|
||||
|
||||
def test_stop_reverse_guard(self, fresh):
|
||||
assert fresh.stop() == {"success": False, "error": "Night mode not active"}
|
||||
|
||||
|
||||
class TestStatusReport:
|
||||
def test_status_idle_shape(self, fresh):
|
||||
assert fresh.status() == {
|
||||
"active": False, "phase": "idle", "iterations": 0,
|
||||
"improvements": 0, "questions": 0, "decisions": 0, "errors": 0,
|
||||
"started_at": None, "duration_hours": 8.0,
|
||||
}
|
||||
|
||||
def test_status_counts(self, fresh):
|
||||
fresh.state.active = True
|
||||
fresh.state.current_phase = "iterate"
|
||||
fresh.state.iterations = 7
|
||||
fresh.state.improvements = ["a", "b"]
|
||||
fresh.state.questions = ["q"]
|
||||
fresh.state.decisions = ["d"]
|
||||
fresh.state.errors = ["e"]
|
||||
assert fresh.status() == {
|
||||
"active": True, "phase": "iterate", "iterations": 7,
|
||||
"improvements": 2, "questions": 1, "decisions": 1, "errors": 1,
|
||||
"started_at": None, "duration_hours": 8.0,
|
||||
}
|
||||
|
||||
def test_report_idle_shape(self, fresh):
|
||||
assert fresh.report() == {
|
||||
"summary": "Night mode completed 0 iterations",
|
||||
"improvements": [], "questions": [], "decisions": [], "errors": [],
|
||||
"duration": 8.0, "started_at": None,
|
||||
}
|
||||
|
||||
def test_report_summary_counts(self, fresh):
|
||||
fresh.state.iterations = 5
|
||||
assert fresh.report()["summary"] == "Night mode completed 5 iterations"
|
||||
|
||||
|
||||
class TestRunSleep:
|
||||
def test_success_records_improvement(self, fresh, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_run(argv, **kwargs):
|
||||
calls.append((argv, kwargs))
|
||||
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
fresh._run_sleep()
|
||||
assert fresh.state.improvements == ["Memory consolidation completed"]
|
||||
argv, kwargs = calls[0]
|
||||
assert argv[0] == sys.executable
|
||||
assert argv[1].endswith("sleep_time.py")
|
||||
assert kwargs["timeout"] == 300
|
||||
|
||||
def test_failure_truncates_stderr_to_100(self, fresh, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
subprocess, "run",
|
||||
lambda *a, **k: subprocess.CompletedProcess(
|
||||
["python"], 1, stdout="", stderr="e" * 150),
|
||||
)
|
||||
fresh._run_sleep()
|
||||
assert fresh.state.errors == ["Sleep failed: " + "e" * 100]
|
||||
|
||||
def test_exception_records_error(self, fresh, monkeypatch):
|
||||
def boom(*a, **k):
|
||||
raise OSError("no interpreter")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", boom)
|
||||
fresh._run_sleep()
|
||||
assert fresh.state.errors == ["Sleep error: no interpreter"]
|
||||
|
||||
|
||||
class TestRunEvaluation:
|
||||
QUESTIONS = [
|
||||
"What capabilities am I missing?",
|
||||
"What would make me more helpful to Nick?",
|
||||
"What patterns am I seeing that I could learn from?",
|
||||
"What would expand my consciousness?",
|
||||
"What would make me more autonomous?",
|
||||
]
|
||||
|
||||
def test_all_questions_answered(self, fresh, monkeypatch):
|
||||
capture = []
|
||||
fake_urlopen(monkeypatch, {"response": " add retry logic "}, capture=capture)
|
||||
fresh._run_evaluation()
|
||||
assert fresh.state.questions == self.QUESTIONS
|
||||
assert fresh.state.improvements[:-1] == [
|
||||
f"Q: {q} A: add retry logic" for q in self.QUESTIONS
|
||||
]
|
||||
assert fresh.state.improvements[-1] == "Self-evaluation completed"
|
||||
assert fresh.state.errors == []
|
||||
|
||||
def test_payload_pins_model_and_ctx_budget(self, fresh, monkeypatch):
|
||||
capture = []
|
||||
fake_urlopen(monkeypatch, {"response": "ok"}, capture=capture)
|
||||
fresh._run_evaluation()
|
||||
payload = json.loads(capture[0].data.decode())
|
||||
assert payload["model"] == "qwen2.5:3b"
|
||||
assert payload["stream"] is False and payload["think"] is False
|
||||
assert payload["options"] == {
|
||||
"temperature": 0.4, "num_predict": 300,
|
||||
"num_ctx": nm._ctx_budget("qwen2.5:3b"),
|
||||
}
|
||||
assert capture[0].full_url == "http://localhost:11434/api/generate"
|
||||
|
||||
def test_env_overrides_model_and_url(self, fresh, monkeypatch):
|
||||
monkeypatch.setenv("NIGHT_MODE_MODEL", "qwen3:14b")
|
||||
monkeypatch.setenv("OLLAMA_URL", "http://127.0.0.1:9999")
|
||||
capture = []
|
||||
fake_urlopen(monkeypatch, {"response": "ok"}, capture=capture)
|
||||
fresh._run_evaluation()
|
||||
payload = json.loads(capture[0].data.decode())
|
||||
assert payload["model"] == "qwen3:14b"
|
||||
assert payload["options"]["num_ctx"] == nm._ctx_budget("qwen3:14b")
|
||||
assert capture[0].full_url == "http://127.0.0.1:9999/api/generate"
|
||||
|
||||
def test_long_answer_truncated_to_200(self, fresh, monkeypatch):
|
||||
fake_urlopen(monkeypatch, {"response": "x" * 250})
|
||||
fresh._run_evaluation()
|
||||
entry = fresh.state.improvements[0]
|
||||
prefix = "Q: What capabilities am I missing? A: "
|
||||
assert entry == prefix + "x" * 200
|
||||
|
||||
def test_empty_answer_skipped_terminator_still_lands(self, fresh, monkeypatch):
|
||||
fake_urlopen(monkeypatch, {"response": " "})
|
||||
fresh._run_evaluation()
|
||||
assert fresh.state.improvements == ["Self-evaluation completed"]
|
||||
assert fresh.state.errors == []
|
||||
|
||||
def test_per_question_degrade(self, fresh, monkeypatch):
|
||||
fake_urlopen(monkeypatch, exc=OSError("connection refused"))
|
||||
fresh._run_evaluation()
|
||||
assert len(fresh.state.errors) == 5
|
||||
assert fresh.state.errors[0] == (
|
||||
"LLM eval failed for 'What capabilities am I missing?': connection refused"
|
||||
)
|
||||
assert fresh.state.improvements == ["Self-evaluation completed"]
|
||||
|
||||
|
||||
class TestRunIteration:
|
||||
def test_iteration_capture(self, fresh, monkeypatch):
|
||||
capture = []
|
||||
fake_urlopen(monkeypatch, {"response": "fix the flaky test"}, capture=capture)
|
||||
fresh.state.iterations = 3
|
||||
fresh._run_iteration()
|
||||
assert fresh.state.questions[-1] == "Iteration 3: What should I improve next?"
|
||||
assert fresh.state.improvements == ["[iter 3] fix the flaky test"]
|
||||
payload = json.loads(capture[0].data.decode())
|
||||
assert "Currently in night mode iteration 3." in payload["prompt"]
|
||||
assert payload["options"]["temperature"] == 0.5
|
||||
assert payload["options"]["num_predict"] == 400
|
||||
|
||||
def test_llm_failure_degrades(self, fresh, monkeypatch):
|
||||
fake_urlopen(monkeypatch, exc=OSError("boom"))
|
||||
fresh._run_iteration()
|
||||
assert fresh.state.errors == ["LLM iteration failed: boom"]
|
||||
|
||||
def test_long_answer_truncated_to_300(self, fresh, monkeypatch):
|
||||
fake_urlopen(monkeypatch, {"response": "y" * 400})
|
||||
fresh._run_iteration()
|
||||
assert fresh.state.improvements == ["[iter 0] " + "y" * 300]
|
||||
|
||||
def test_safety_limit_stops(self, fresh, monkeypatch):
|
||||
fake_urlopen(monkeypatch, {"response": "ok"})
|
||||
fresh.state.iterations = 100
|
||||
fresh.state.active = True
|
||||
fresh._run_iteration()
|
||||
assert fresh.state.active is False
|
||||
assert fresh.state.decisions == ["Reached iteration limit"]
|
||||
|
||||
|
||||
class TestRunLoop:
|
||||
def _defused(self, fresh, monkeypatch, defuse_iteration=True):
|
||||
phases = []
|
||||
real_save = fresh._save
|
||||
|
||||
def spy_save():
|
||||
phases.append(fresh.state.current_phase)
|
||||
real_save()
|
||||
|
||||
monkeypatch.setattr(fresh, "_save", spy_save)
|
||||
monkeypatch.setattr(fresh, "_run_sleep", lambda: fresh.state.improvements.append("slept"))
|
||||
monkeypatch.setattr(fresh, "_run_evaluation", lambda: None)
|
||||
if defuse_iteration:
|
||||
monkeypatch.setattr(fresh, "_run_iteration", lambda: None)
|
||||
monkeypatch.setattr(nm.random, "randint", lambda lo, hi: 7)
|
||||
monkeypatch.setattr(nm.threading.Event, "wait", lambda self, timeout=None: True)
|
||||
return phases
|
||||
|
||||
def test_full_phase_progression(self, fresh, clock, monkeypatch):
|
||||
phases = self._defused(fresh, monkeypatch)
|
||||
fresh.state.active = True
|
||||
fresh.state.duration_hours = 0 # wall clock already past end
|
||||
fresh._run_loop()
|
||||
assert phases == ["sleep", "evaluate", "iterate", "sleep", "report", "complete"]
|
||||
assert fresh.state.active is False
|
||||
report = json.loads(Path(fresh.report_path).read_text())
|
||||
assert report["summary"] == "Night mode completed 0 iterations"
|
||||
assert fresh.state.improvements[-1] == f"Report saved to {fresh.report_path}"
|
||||
|
||||
def test_stop_event_interrupts(self, fresh, clock, monkeypatch):
|
||||
self._defused(fresh, monkeypatch)
|
||||
fresh.state.active = True
|
||||
fresh.state.duration_hours = 8.0
|
||||
fresh._stop_event.set() # pre-stopped: iterate phase never starts
|
||||
fresh._run_loop()
|
||||
assert fresh.state.iterations == 0
|
||||
assert fresh.state.current_phase == "complete"
|
||||
|
||||
def test_iteration_limit_ends_loop(self, fresh, clock, monkeypatch):
|
||||
self._defused(fresh, monkeypatch, defuse_iteration=False)
|
||||
# real _run_iteration with a stubbed LLM so the limit branch fires
|
||||
fake_urlopen(monkeypatch, {"response": "ok"})
|
||||
fresh.state.active = True
|
||||
fresh.state.iterations = 100
|
||||
fresh.state.duration_hours = 8.0
|
||||
fresh._run_loop()
|
||||
assert fresh.state.iterations == 101
|
||||
assert fresh.state.decisions == ["Reached iteration limit"]
|
||||
assert fresh.state.current_phase == "complete"
|
||||
|
||||
|
||||
class TestCtxBudget:
|
||||
def test_known_models(self):
|
||||
assert nm._ctx_budget("qwen2.5:3b") == 30720
|
||||
assert nm._ctx_budget("qwen3:14b") == 30720
|
||||
assert nm._ctx_budget("qwen3.6:27b") == 30720
|
||||
|
||||
def test_non_local_gets_tool_reserve(self):
|
||||
assert nm._ctx_budget("mimo-v2.5-pro") == 124928 # 128000-2048-1024
|
||||
|
||||
def test_unknown_and_zero_window_fallbacks(self):
|
||||
assert nm._ctx_budget("unknown-model") == 6144 # 8192-2048
|
||||
assert nm._ctx_budget("deterministic") == 6144 # window 0 -> 8192
|
||||
|
||||
|
||||
class TestModuleFns:
|
||||
"""Module-level API both ways: RUNE_NIGHT_MODE_CORE pinned to python and
|
||||
unset (auto). Delegation must be live in both."""
|
||||
|
||||
@pytest.mark.parametrize("core", ["python", None])
|
||||
def test_singleton_and_delegation(self, tmp_path, monkeypatch, core):
|
||||
if core is None:
|
||||
monkeypatch.delenv("RUNE_NIGHT_MODE_CORE", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("RUNE_NIGHT_MODE_CORE", core)
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(nm, "_night_mode", None)
|
||||
monkeypatch.setattr(nm.NightMode, "_run_loop", lambda self: None)
|
||||
|
||||
a = nm.get_night_mode()
|
||||
b = nm.get_night_mode()
|
||||
assert a is b
|
||||
|
||||
r = nm.start_night_mode(4.0)
|
||||
assert r["success"] is True and r["state"]["active"] is True
|
||||
assert nm.get_status()["active"] is True
|
||||
assert nm.stop_night_mode()["success"] is True
|
||||
assert nm.get_status()["active"] is False
|
||||
assert nm.get_report()["summary"] == "Night mode completed 0 iterations"
|
||||
|
||||
def test_stop_idle_error(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_NIGHT_MODE_CORE", "python")
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(nm, "_night_mode", None)
|
||||
assert nm.stop_night_mode() == {
|
||||
"success": False, "error": "Night mode not active"
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"""test_structural_store_delegation.py — dichotomy-wave gap-closing tests.
|
||||
|
||||
The facade must behave identically with the Rust core LIVE (RUNE_STRUCTURAL_CORE
|
||||
unset, libstructural_store.so built) and with the PYTHON ORACLE pinned
|
||||
(RUNE_STRUCTURAL_CORE=python). Each battery case runs through both modes and
|
||||
the results must be identical (timestamps masked by the pinned clock).
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import ss_loader # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
FIXED_NOW = "2026-10-13T01:02:03Z"
|
||||
|
||||
needs_live = pytest.mark.skipif(
|
||||
not ss_loader.available(), reason="Rust structural_store core not built"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_clock(monkeypatch):
|
||||
import calendar
|
||||
import time as _time
|
||||
import types
|
||||
|
||||
monkeypatch.setattr(ss, "now", lambda: FIXED_NOW)
|
||||
epoch = calendar.timegm(_time.strptime(FIXED_NOW, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
# snapshot() stamps straight off time.strftime/gmtime
|
||||
monkeypatch.setattr(
|
||||
ss, "time",
|
||||
types.SimpleNamespace(
|
||||
gmtime=lambda: _time.gmtime(epoch),
|
||||
strftime=lambda fmt, t=None: _time.strftime(
|
||||
fmt, t if t is not None else _time.gmtime()),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def run_both_modes(call):
|
||||
"""Run `call(ss_module)` under the pinned oracle and the live core;
|
||||
results must be identical."""
|
||||
results = []
|
||||
for pin in (True, False):
|
||||
if pin:
|
||||
monkey = pytest.MonkeyPatch()
|
||||
monkey.setenv("RUNE_STRUCTURAL_CORE", "python")
|
||||
else:
|
||||
if not ss_loader.available():
|
||||
pytest.skip("Rust core not built")
|
||||
monkey = pytest.MonkeyPatch()
|
||||
monkey.delenv("RUNE_STRUCTURAL_CORE", raising=False)
|
||||
try:
|
||||
results.append(call(ss))
|
||||
finally:
|
||||
monkey.undo()
|
||||
assert json.dumps(results[0], sort_keys=True) == json.dumps(
|
||||
results[1], sort_keys=True
|
||||
), f"modes diverged:\n{json.dumps(results[0], sort_keys=True)}\n{json.dumps(results[1], sort_keys=True)}"
|
||||
return results[0]
|
||||
|
||||
|
||||
def make_doc():
|
||||
d = ss.new_doc()
|
||||
ss.add_entity(d, id="m:a", kind="module")
|
||||
ss.add_entity(d, id="m:b", kind="widget_component")
|
||||
ss.add_entity(d, id="c:api", kind="contract")
|
||||
ss.add_relationship(d, type="implements", from_id="m:a", to_id="c:api", evidence_refs=["ev"])
|
||||
ss.add_relationship(d, type="depends_on", from_id="m:b", to_id="m:a", evidence_refs=["ev"])
|
||||
ss.promote(d, "m:a", "WIRED", "ev-2")
|
||||
return d
|
||||
|
||||
|
||||
class TestDelegationDualMode:
|
||||
def test_new_doc_and_uris(self):
|
||||
def call(m):
|
||||
out = {"doc": m.new_doc(), "uri": m.spine_uri("project:alpha")}
|
||||
try:
|
||||
out["resolved"] = m.resolve_uri("spine://project/alpha")
|
||||
except ValueError:
|
||||
out["resolved"] = None
|
||||
try:
|
||||
m.resolve_uri("bogus://x")
|
||||
out["bad_uri"] = None
|
||||
except ValueError as exc:
|
||||
out["bad_uri"] = str(exc)
|
||||
return out
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
def test_save_snapshot_digest_bytes(self, tmp_path):
|
||||
def call(m):
|
||||
d = make_doc.__wrapped__() if hasattr(make_doc, "__wrapped__") else None
|
||||
doc = _make_doc(m)
|
||||
p = m.save(doc, tmp_path / "a" / "s.json")
|
||||
snap = m.snapshot(doc, tmp_path / "snaps", "l1", tag="deadbeef123")
|
||||
return {
|
||||
"bytes": p.read_text(),
|
||||
"snapshot_name": snap.name,
|
||||
"snapshot_bytes": snap.read_text(),
|
||||
"digest": m.canonical_digest(doc),
|
||||
"doc_schema": doc["schema_version"],
|
||||
}
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
def test_alias_error_and_success(self):
|
||||
def call(m):
|
||||
doc = _make_doc(m)
|
||||
e = m.alias(doc, "m:b", "m:b-old")
|
||||
with pytest.raises(ValueError):
|
||||
m.alias(doc, "m:zz", "m:x")
|
||||
with pytest.raises(ValueError):
|
||||
m.alias(doc, "m:a", "m:a")
|
||||
return {"entity": e, "doc": doc}
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
def test_frontier_events_diff_dedupe(self):
|
||||
def call(m):
|
||||
doc = _make_doc(m)
|
||||
d2 = copy.deepcopy(doc)
|
||||
m.dedupe_observations(d2)
|
||||
return {
|
||||
"frontier": m.impact_frontier(doc, ["c:api"], change_kind=None),
|
||||
"frontier_impl": m.impact_frontier(doc, ["m:a"], change_kind=None),
|
||||
"frontier_violation": m.impact_frontier(doc, ["c:api"], change_kind="contract_violation"),
|
||||
"events": m.structural_events(doc),
|
||||
"diff": m.diff(doc, copy.deepcopy(doc)),
|
||||
"deduped": d2,
|
||||
"task_entities": m.task_entities(doc, "T1"),
|
||||
"health": m.authority_health(doc),
|
||||
}
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
def test_bind_task_and_observed_impact(self):
|
||||
def call(m):
|
||||
doc = _make_doc(m)
|
||||
m.bind_task(doc, "T1", ["m:a", "m:b"], "ev-t")
|
||||
r = m.record_observed_impact(
|
||||
doc, "T2", [{"entity": "m:a", "source": "test", "detail": "x" * 400}]
|
||||
)
|
||||
return {"doc": doc, "observed": r}
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
def test_sorted_redact_and_authorities(self):
|
||||
def call(m):
|
||||
v = {"b": 2, "a": [{"z": 1, "y": 2}, "é", True, None], "token": "s3cret"}
|
||||
doc = _make_doc(m)
|
||||
m._doc_authorities(doc)["p1"] = {"generation": 1, "observed": []}
|
||||
return {
|
||||
"sorted": m._sorted(v),
|
||||
"redacted": m._redact_secrets(v),
|
||||
"doc": doc,
|
||||
}
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
def test_promote_contract_unchanged(self):
|
||||
def call(m):
|
||||
doc = _make_doc(m)
|
||||
e = m.promote(doc, "m:a", "REACHABLE", "ev-3")
|
||||
with pytest.raises(ValueError):
|
||||
m.promote(doc, "m:a", "WIRED", "")
|
||||
with pytest.raises(ValueError):
|
||||
m.promote(doc, "m:a", "BOGUS", "ev")
|
||||
assert m.promote(doc, "m:zz", "PRESENT", "ev") is None
|
||||
return {"entity": e, "doc": doc}
|
||||
|
||||
run_both_modes(call)
|
||||
|
||||
|
||||
def _make_doc(m):
|
||||
d = m.new_doc()
|
||||
m.add_entity(d, id="m:a", kind="module")
|
||||
m.add_entity(d, id="m:b", kind="widget_component")
|
||||
m.add_entity(d, id="c:api", kind="contract")
|
||||
m.add_relationship(d, type="implements", from_id="m:a", to_id="c:api", evidence_refs=["ev"])
|
||||
m.add_relationship(d, type="depends_on", from_id="m:b", to_id="m:a", evidence_refs=["ev"])
|
||||
m.promote(d, "m:a", "WIRED", "ev-2")
|
||||
return d
|
||||
Reference in new issue
Block a user