Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85f4f3ca5e | ||
|
|
b0d7380fb5 | ||
|
|
3d21fe3dac | ||
|
|
257bf77f91 | ||
|
|
60197993e5 | ||
|
|
b9b089fa49 | ||
|
|
49ba669fa6 | ||
|
|
593cddba5d | ||
|
|
d2fd546aa0 | ||
|
|
4475fed547 | ||
|
|
3d158800c7 | ||
|
|
07096f2c59 | ||
|
|
b4315878e4 | ||
|
|
4204667f0a | ||
|
|
9acfb73645 | ||
|
|
a691a701d5 | ||
|
|
bf2f3f0a25 | ||
|
|
59ff243cc1 | ||
|
|
537c65b10d | ||
|
|
621b90236d | ||
|
|
a3045b0b75 | ||
|
|
405dcc78e0 | ||
|
|
2597b58b88 |
No files matched your search
@@ -231,9 +231,9 @@ jobs:
|
||||
- name: curator-core candidate-decay parity gate (HARD — #2479 batch: ledger decay + journal render + cosine)
|
||||
run: |
|
||||
cd src/go/curator-core && go build -o /tmp/curator-core-${{ gitea.run_id }} . && /tmp/curator-core-${{ gitea.run_id }} replay truth_candidate_decay.json && go test -run TestCandidateDecayParity -v
|
||||
- name: cron-engine parity gate (HARD — #2479 batches 1+126: parser + lifecycle)
|
||||
- name: cron-engine parity gate (HARD — #2479 batches 1+126 + gap-close: parser + lifecycle + execution family)
|
||||
run: |
|
||||
cd src/go/cron-engine && go build -o /tmp/cron-engine-${{ gitea.run_id }} . && /tmp/cron-engine-${{ gitea.run_id }} replay truth.json && go test -run TestLifecycleParity -v
|
||||
cd src/go/cron-engine && go build -o /tmp/cron-engine-${{ gitea.run_id }} . && /tmp/cron-engine-${{ gitea.run_id }} replay truth.json && /tmp/cron-engine-${{ gitea.run_id }} replay-gap truth_gap.json && go test -run 'TestLifecycleParity|TestGapParity' -v
|
||||
- name: goal-engine parity gate (HARD — #2479 batches 2+129: verifier + lifecycle)
|
||||
run: |
|
||||
cd src/go/goal-engine && go build -o /tmp/goal-engine-${{ gitea.run_id }} . && /tmp/goal-engine-${{ gitea.run_id }} replay truth.json && go test -run TestLifecycleParity -v
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.293.1**
|
||||
**Current release: v0.299.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.**
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{"trap": "oracle CRASH parity: if the oracle crashes (AttributeError etc.) on an input class, the port returns the matching error envelope", "port": "never silently skip — the facade's fallback reproduces the exact exception", "proved_by": "execution_trace fuzz"},
|
||||
{"trap": "glob+mtime ordering ties within the same second", "port": "explicit os.utime in test fixtures; deterministic per-case dirs", "proved_by": "sleep_time"},
|
||||
{"trap": "Go evaluation order: arg-list trace(x, f.Mutating()) snapshots BEFORE mutation; Python passes live references", "port": "capture returns/reports into locals first", "proved_by": "failure_auto_filer"},
|
||||
{"trap": "absolute temp paths in frozen truth are machine-specific", "port": "repo-relative names or REPO_ROOT placeholder + battery substitution", "proved_by": "paths"}
|
||||
{"trap": "absolute temp paths in frozen truth are machine-specific", "port": "repo-relative names or REPO_ROOT placeholder + battery substitution", "proved_by": "paths"},
|
||||
{"trap": "ThreadPoolExecutor.submit AFTER shutdown raises from the submit loop itself — run_parallel's per-task capture never sees it, the WHOLE call raises", "port": "Go port panics with 'cannot schedule new futures after shutdown' at the same site (crash parity, recovered by battery/parity)", "proved_by": "parallel_processor (2026-10-14)"},
|
||||
{"trap": "callable corpora cannot ride JSON truth rows", "port": "freeze a shared kind/fn/value SPEC grammar via a rig that builds real Python callables; the battery rebuilds Go tasks from the same rows (extends the memory_consolidation seam pattern)", "proved_by": "parallel_processor (2026-10-14)"},
|
||||
{"trap": "threading.active_count() in oracle output is process-random (pool threads, harness state)", "port": "pin via rig patch to a constant; Go side reads the matching PP_ACTIVE_THREADS seam (default: native goroutine count)", "proved_by": "parallel_processor (2026-10-14)"}
|
||||
]
|
||||
}
|
||||
@@ -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: 43%** (82/192 runtime modules at FULL)
|
||||
**FULL completion: 47%** (90/192 runtime modules at FULL)
|
||||
|
||||
Function-level detail: 63% (1440/2278 runtime functions matched) — secondary; includes in-flight work
|
||||
Function-level detail: 66% (1511/2279 runtime functions matched) — secondary; includes in-flight work
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 82 | 34347 | 80%+ of functions ported |
|
||||
| PARTIAL | 70 | 28304 | 20–80% — real logic exists, gaps named below |
|
||||
| FULL | 90 | 37767 | 80%+ of functions ported |
|
||||
| PARTIAL | 62 | 24926 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 40 | 11513 | no name-preserving implementation |
|
||||
| Total runtime | 192 | 74164 | |
|
||||
| Total runtime | 192 | 74206 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -64,15 +64,11 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
|---|---|---|---|
|
||||
| rune_operator | 2746 | 25/68 | _build_model_router, _sorted_tool_names, _filter_safety_noise, _wire_auto_filer, _reconcile_guarded, get_operator |
|
||||
| memory_store | 2333 | 30/68 | _ctx_budget, now_iso, ensure_memory_type_column, _embed, _temp_json, _entities_to_json |
|
||||
| provider_registry | 679 | 11/28 | _ctx_budget, _format_tools_openai, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
|
||||
| warm_router | 1518 | 17/33 | _ensure_use_history_table, record_outcome, record_neuron_fire, base_level_activation, _db, _aaak |
|
||||
| cron_engine | 440 | 13/27 | get_cron_engine, run_task, start_engine, stop_engine, to_dict, fire_on_task_complete |
|
||||
| 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 |
|
||||
| parallel_processor | 194 | 3/14 | _shutdown_processor, get_processor, run_parallel_with_args, get_status, test_parallel, run_parallel_with_args |
|
||||
| structural_model | 388 | 9/20 | workspace_record, workspace_advance, _init_project, save, reload, add |
|
||||
| claim_audit | 484 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
|
||||
| model_profiles | 438 | 5/15 | get_profile_path, load_profile, save_profile, build_profile_from_ollama, scan_all_models, get_or_create_profile |
|
||||
| capability_broker | 798 | 10/19 | _native_tool_schemas, load_registry, _cards_are_stale, load_cards, generate_card, build_cards |
|
||||
| token_optimizer | 514 | 16/25 | get_optimizer, _tc_core, _to_core, to_dict, _to_state, _apply_state |
|
||||
| behavior | 537 | 6/14 | _bh_core, connect, _correction_matches_python, _habit_matches_python, _reinforce_rust, _reinforce_python |
|
||||
@@ -86,7 +82,6 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| audit_ledger | 212 | 2/8 | _compute_hash, get_entries, log_promotion, log_gate_result, log_drift_alert, main |
|
||||
| context_pressure | 262 | 3/9 | log_measurement, read_log, log_stats, _now, handle_hook, main |
|
||||
| core_server | 234 | 6/12 | make_server, log_message, _send_json, _send_ndjson, do_GET, do_POST |
|
||||
| microkernel | 434 | 6/12 | now_iso, discover, _extract_manifest, _load_registry, _save_registry, _run_hook |
|
||||
| model_registry | 253 | 2/8 | test_registry, to_dict, get_profile, get_enabled_profiles, get_profiles_by_tier, list_profiles |
|
||||
| progressive_escalation | 279 | 2/8 | execute_with_escalation, test_escalation, handle_hook, to_dict, execute, verify |
|
||||
| voicebox | 164 | 5/11 | _run, start_recording, stop_recording, _transcribe_and_type, on_press, on_release |
|
||||
@@ -97,14 +92,11 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| drift_compression | 307 | 3/7 | get_compression_ratio, compress_entry, warn_before_compress, get_compression_stats |
|
||||
| drift_observability | 399 | 5/9 | _do_core, main, _load_alerts, _save_alerts |
|
||||
| layout_contract | 336 | 2/6 | define_region, bind_component, amend_layout_contract, record_layout_observation |
|
||||
| model_connect | 471 | 7/11 | _ctx_budget, _now, _get_cloud_models, handle_hook |
|
||||
| pins | 282 | 7/11 | _store_path, _detail_blob, unpin, list_pins |
|
||||
| prompt_sync | 487 | 11/15 | _now, _rust_available, get_store, main |
|
||||
| salience_gate | 276 | 1/5 | _sg_core, test_salience, handle_hook, to_dict |
|
||||
| self_improve | 246 | 1/5 | _now, suggest_improvements, handle_hook, main |
|
||||
| task_gate | 301 | 5/9 | _tg_core, _now, list_gates, main |
|
||||
| tier_manager | 262 | 6/10 | demotion_sweep, get_tier_config, should_search, should_compress |
|
||||
| witness | 446 | 11/15 | _now, log_entry, read_log, log_stats |
|
||||
| connection_pool | 226 | 7/10 | get_pool, _kernel_state, _get_connection_core |
|
||||
| construction_activation | 237 | 1/4 | _scaffold_for, _auto_structural_awareness, dispatch_from_decision |
|
||||
| construction_mode | 412 | 2/5 | _contains_any, to_dict, from_dict |
|
||||
@@ -143,7 +135,7 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| sleep_time | 1056 | 26/26 | GO |
|
||||
| spine | 975 | 25/25 | RUST |
|
||||
| structural_store | 921 | 34/41 | RUST |
|
||||
| scaffold | 881 | 30/30 | GO |
|
||||
| scaffold | 908 | 30/30 | GO |
|
||||
| context_broker | 870 | 24/24 | GO |
|
||||
| hook_engine | 860 | 8/8 | GO |
|
||||
| local_tool_runtime | 809 | 30/30 | GO |
|
||||
@@ -151,6 +143,7 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| memory_consolidation | 770 | 22/22 | GO |
|
||||
| contracts | 743 | 11/11 | RUST |
|
||||
| persona_gate | 724 | 23/23 | GO |
|
||||
| provider_registry | 687 | 28/28 | GO |
|
||||
| abstraction | 656 | 16/16 | GO |
|
||||
| safety_override | 649 | 20/20 | RUST |
|
||||
| adaptive_router | 625 | 19/19 | GO |
|
||||
@@ -166,12 +159,17 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| build_mode | 521 | 33/33 | GO |
|
||||
| growth_delta | 520 | 17/18 | GO |
|
||||
| event_wait | 518 | 14/14 | GO |
|
||||
| model_connect | 471 | 11/11 | GO |
|
||||
| worthiness | 469 | 12/13 | unclassified |
|
||||
| tool_protocol | 461 | 22/27 | GO |
|
||||
| auto_tasks | 460 | 14/14 | GO |
|
||||
| programme_continuation | 458 | 11/12 | RUST |
|
||||
| goal | 453 | 20/20 | GO |
|
||||
| release_notes | 446 | 16/16 | GO |
|
||||
| witness | 446 | 15/15 | GO |
|
||||
| cron_engine | 440 | 27/27 | GO |
|
||||
| model_profiles | 438 | 15/15 | GO |
|
||||
| microkernel | 434 | 12/12 | GO |
|
||||
| model_routing | 432 | 13/13 | GO |
|
||||
| night_mode | 421 | 19/19 | GO |
|
||||
| build_basis | 375 | 11/13 | RUST |
|
||||
@@ -185,6 +183,7 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| lexicon_evolution | 304 | 7/8 | unclassified |
|
||||
| message_graph | 295 | 12/13 | unclassified |
|
||||
| toolchain | 295 | 5/6 | RUST |
|
||||
| salience_gate | 283 | 6/6 | RUST |
|
||||
| shard_manager | 276 | 9/9 | RUST |
|
||||
| auto_context | 273 | 8/8 | GO |
|
||||
| spine_language | 269 | 5/5 | RUST |
|
||||
@@ -195,6 +194,7 @@ Function-level detail: 63% (1440/2278 runtime functions matched) — secondary;
|
||||
| project_registry | 205 | 10/12 | RUST |
|
||||
| forge_client | 203 | 18/18 | GO |
|
||||
| behaviour_ledger | 195 | 10/12 | RUST |
|
||||
| parallel_processor | 194 | 14/14 | GO |
|
||||
| construction_depth | 172 | 2/2 | RUST |
|
||||
| self_modify | 149 | 7/8 | unclassified |
|
||||
| forge | 146 | 11/11 | GO |
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"message_graph",
|
||||
"message_queue",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_routing",
|
||||
"mutation_guards",
|
||||
"night_mode",
|
||||
@@ -113,7 +114,6 @@
|
||||
"graph_memory": "rune-wave-ck",
|
||||
"drift_core": "rune-wave-cm",
|
||||
"cron_engine": "rune-wave-co",
|
||||
"model_profiles": "rune-wave-cp",
|
||||
"microkernel": "rune-wave-cq",
|
||||
"voice_hybrid": "rune-wave-cr",
|
||||
"parallel_processor": "rune-wave-cs",
|
||||
@@ -123,7 +123,9 @@
|
||||
"builder_bridge": "rune-wave-cw",
|
||||
"drift_compression": "rune-wave-cx",
|
||||
"event_envelope": "rune-wave-cy",
|
||||
"stage_guards": "rune-wave-cz"
|
||||
"stage_guards": "rune-wave-cz",
|
||||
"salience_gate": "rune-wave-cy2",
|
||||
"voice_sync": "rune-wave-db"
|
||||
},
|
||||
"excluded": [
|
||||
"TUI-LAST modules",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.293.1"
|
||||
version = "0.299.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check_cron_engine_parity.py — LIVE parity for the cron_engine GO port
|
||||
(#2479 dichotomy gap-close): drives the Python oracle (src/rune/
|
||||
cron_engine.py) and the GO binary through IDENTICAL scenarios and diffs
|
||||
their JSON outputs — beyond frozen-truth replay, this exercises the two
|
||||
implementations LIVE (real subprocesses, real files) under a pinned
|
||||
clock + hermetic HOME.
|
||||
|
||||
Python side mirrors the Go binary's `scenario` mode exactly (op set,
|
||||
output shape). Paths are tokenized to <HOME> before comparison.
|
||||
|
||||
Usage: python3 scripts/check_cron_engine_parity.py <go-binary>
|
||||
(build first: cd src/go/cron-engine && go build -o /tmp/cron-engine .)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
os.environ.setdefault("RUNE_CRON_ENGINE_CORE", "python")
|
||||
|
||||
from rune import cron_engine as ce # noqa: E402
|
||||
import rune.subsystem_health as _SH # noqa: E402
|
||||
|
||||
_SH.record_failure = lambda *a, **k: True # hermetic: no ledger writes
|
||||
|
||||
FROZEN_NOW = "2026-10-13T09:20:00"
|
||||
|
||||
|
||||
def frozen_clock(iso):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
base = datetime.fromisoformat(iso)
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return base.replace(tzinfo=tz) if tz is not None else base
|
||||
|
||||
return FrozenDatetime
|
||||
|
||||
|
||||
class StopTime:
|
||||
def __init__(self, engine):
|
||||
self._e = engine
|
||||
|
||||
def sleep(self, _s):
|
||||
if self._e is not None:
|
||||
self._e.running = False
|
||||
|
||||
|
||||
def run_python(sc):
|
||||
ce.datetime = frozen_clock(sc["frozen_now"])
|
||||
home = tempfile.mkdtemp(prefix="ce-live-py-")
|
||||
os.environ["HOME"] = home
|
||||
storage = os.path.join(home, ".rune", "cron.json")
|
||||
logp = os.path.join(home, ".rune", "cron.log")
|
||||
|
||||
e = ce.CronEngine.__new__(ce.CronEngine)
|
||||
e.storage_path = storage
|
||||
e.tasks = {}
|
||||
e.running = False
|
||||
e.thread = None
|
||||
e._last_run_minute = {}
|
||||
e._load()
|
||||
|
||||
def rd(p):
|
||||
return open(p).read() if os.path.exists(p) else None
|
||||
|
||||
def dump():
|
||||
return {
|
||||
"running": e.running,
|
||||
"storage_path": e.storage_path,
|
||||
"log_path": logp,
|
||||
"tasks": [{"name": n, "task": t.to_dict()} for n, t in e.tasks.items()],
|
||||
"_last_run_minute": dict(e._last_run_minute), # snapshot (no live aliasing)
|
||||
}
|
||||
|
||||
steps = []
|
||||
for op in sc["ops"]:
|
||||
k = op["op"]
|
||||
if k == "add":
|
||||
t = e.add_task(op["name"], op["expr"], op["command"], op.get("description", ""))
|
||||
steps.append({"op": "add", "task": t.to_dict()})
|
||||
elif k == "enable":
|
||||
steps.append({"op": "enable", "ok": e.enable_task(op["name"])})
|
||||
elif k == "disable":
|
||||
steps.append({"op": "disable", "ok": e.disable_task(op["name"])})
|
||||
elif k == "remove":
|
||||
steps.append({"op": "remove", "ok": e.remove_task(op["name"])})
|
||||
elif k == "run":
|
||||
steps.append({"op": "run", "result": e.run_task(op["name"])})
|
||||
elif k == "fire":
|
||||
steps.append({"op": "fire", "fired": e.fire_on_task_complete()})
|
||||
elif k == "loop":
|
||||
real = ce.time
|
||||
ce.time = StopTime(e)
|
||||
try:
|
||||
e._run_loop()
|
||||
finally:
|
||||
ce.time = real
|
||||
steps.append({"op": "loop"})
|
||||
elif k == "start":
|
||||
real = ce.time
|
||||
ce.time = StopTime(e)
|
||||
try:
|
||||
e.start()
|
||||
e.stop()
|
||||
finally:
|
||||
ce.time = real
|
||||
steps.append({"op": "start+stop"})
|
||||
elif k == "stop":
|
||||
e.stop()
|
||||
steps.append({"op": "stop"})
|
||||
elif k == "save":
|
||||
e._save()
|
||||
steps.append({"op": "save", "ok": True})
|
||||
elif k == "load":
|
||||
e._load()
|
||||
steps.append({"op": "load", "ok": True})
|
||||
elif k == "storage":
|
||||
steps.append({"op": "storage", "bytes": rd(storage)})
|
||||
elif k == "log":
|
||||
steps.append({"op": "log", "bytes": rd(logp)})
|
||||
elif k == "state":
|
||||
steps.append({"op": "state", "state": dump()})
|
||||
else:
|
||||
raise ValueError(k)
|
||||
return {"home": home, "out": {"steps": steps, "final": dump()}}
|
||||
|
||||
|
||||
def run_go(binary, sc_path):
|
||||
r = subprocess.run([binary, "scenario", str(sc_path)],
|
||||
capture_output=True, text=True, timeout=120)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"go scenario failed: {r.stderr}")
|
||||
# the binary's temp home differs per run — recover it from final.storage_path
|
||||
out = json.loads(r.stdout)
|
||||
home = os.path.dirname(os.path.dirname(out["final"]["storage_path"]))
|
||||
return {"home": home, "out": out}
|
||||
|
||||
|
||||
def tokenize(obj, home):
|
||||
if isinstance(obj, str):
|
||||
return obj.replace(home, "<HOME>")
|
||||
if isinstance(obj, list):
|
||||
return [tokenize(x, home) for x in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: tokenize(v, home) for k, v in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
SCENARIOS = [
|
||||
# 1: lifecycle + run envelopes + storage/log bytes
|
||||
{"frozen_now": FROZEN_NOW, "ops": [
|
||||
{"op": "add", "name": "z-first", "expr": "on_complete", "command": "echo one"},
|
||||
{"op": "add", "name": "a-second", "expr": "*/5 * * * *", "command": "echo '中文 twø'", "description": "d-ü"},
|
||||
{"op": "add", "name": "bad", "expr": "* * * * *", "command": "nope-cmd-xyz"},
|
||||
{"op": "add", "name": "quoted", "expr": "* * * * *", "command": "echo 'unterminated"},
|
||||
{"op": "state"},
|
||||
{"op": "run", "name": "z-first"},
|
||||
{"op": "run", "name": "bad"},
|
||||
{"op": "run", "name": "quoted"},
|
||||
{"op": "run", "name": "ghost"},
|
||||
{"op": "disable", "name": "a-second"},
|
||||
{"op": "fire"},
|
||||
{"op": "storage"},
|
||||
{"op": "log"},
|
||||
{"op": "state"},
|
||||
]},
|
||||
# 2: scheduler loop — matching, non-matching, dedup, disabled
|
||||
{"frozen_now": FROZEN_NOW, "ops": [
|
||||
{"op": "add", "name": "match", "expr": "* * * * *", "command": "echo m"},
|
||||
{"op": "add", "name": "nomatch", "expr": "5 4 * * *", "command": "echo n"},
|
||||
{"op": "add", "name": "off", "expr": "* * * * *", "command": "echo o"},
|
||||
{"op": "disable", "name": "off"},
|
||||
{"op": "loop"},
|
||||
{"op": "loop"},
|
||||
{"op": "storage"},
|
||||
{"op": "log"},
|
||||
{"op": "state"},
|
||||
]},
|
||||
# 3: persistence round-trip + start/stop + remove
|
||||
{"frozen_now": FROZEN_NOW, "ops": [
|
||||
{"op": "add", "name": "keep", "expr": "0 9 * * 1", "command": "echo k"},
|
||||
{"op": "add", "name": "drop", "expr": "0 9 * * 1", "command": "echo d"},
|
||||
{"op": "remove", "name": "drop"},
|
||||
{"op": "run", "name": "keep"},
|
||||
{"op": "storage"},
|
||||
{"op": "start"},
|
||||
{"op": "state"},
|
||||
{"op": "stop"},
|
||||
]},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
binary = sys.argv[1] if len(sys.argv) > 1 else "/tmp/cron-engine"
|
||||
ok = 0
|
||||
for i, sc in enumerate(SCENARIOS, 1):
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
||||
json.dump(sc, f)
|
||||
sc_path = f.name
|
||||
try:
|
||||
py = run_python(sc)
|
||||
go = run_go(binary, sc_path)
|
||||
finally:
|
||||
os.unlink(sc_path)
|
||||
pyj = tokenize(py["out"], py["home"])
|
||||
goj = tokenize(go["out"], go["home"])
|
||||
if pyj == goj:
|
||||
ok += 1
|
||||
print(f"scenario {i}: LIVE PARITY OK ({len(sc['ops'])} ops)")
|
||||
else:
|
||||
print(f"scenario {i}: LIVE PARITY FAILED")
|
||||
pj, gj = json.dumps(pyj, indent=1, sort_keys=True), json.dumps(goj, indent=1, sort_keys=True)
|
||||
for pl, gl in zip(pj.splitlines(), gj.splitlines()):
|
||||
if pl != gl:
|
||||
print(f" py: {pl}")
|
||||
print(f" go: {gl}")
|
||||
break
|
||||
return 1
|
||||
print(f"LIVE PARITY: {ok}/{len(SCENARIOS)} scenarios identical (oracle vs GO)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,344 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check_microkernel_parity.py — live oracle-vs-Go parity sweep for the
|
||||
microkernel gap closure (dichotomy wave #2479, worktree rune-wave-cq).
|
||||
|
||||
The frozen envelope (truth_microkernel_gap.json, 25 cases) pins the gap
|
||||
fns byte-identically. THIS script drives the LIVE Python oracle
|
||||
(src/rune/microkernel.py) and the Go binary's parity op mode across every
|
||||
branch class with FRESH fixtures (not the frozen ones): extract_manifest
|
||||
(prefix traps, nested literals, unicode), discover (mixed dir), registry
|
||||
load/save shapes, and fire/_run_hook (ok-json / raw-stdout truncation /
|
||||
rc-error / timeout / skipped / ghost-file / empty-file / two-listeners /
|
||||
no-listeners).
|
||||
|
||||
Normalization law (shared with the rig): wall-clock durations → "<ms>",
|
||||
fire results name-sorted, fixture paths → <dir>. Data keys alphabetical
|
||||
(the Go map marshal is key-sorted; plugins canonicalize the payload).
|
||||
|
||||
Usage: python3 scripts/check_microkernel_parity.py [--bin <path>]
|
||||
Exit 0 on 100% parity, 1 with the first divergence.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import microkernel as mk # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T11:05:00"
|
||||
FROZEN_NOW_AWARE = FROZEN_NOW + "+00:00"
|
||||
|
||||
OK_JSON = (
|
||||
"import json, sys\n"
|
||||
"data = sys.argv[sys.argv.index('--data') + 1]\n"
|
||||
"print(json.dumps({'echo': json.loads(data)}))\n"
|
||||
)
|
||||
RAW_LONG = "print('h\\u00e9llo-\\u2192ok ' * 90)\n"
|
||||
BOOM = ("import sys\n"
|
||||
"sys.stderr.write('\\u00e9rr\\u00f4r-' * 80)\n"
|
||||
"sys.exit(3)\n")
|
||||
SLOW = "import time\ntime.sleep(4)\n"
|
||||
|
||||
FAILS: list[str] = []
|
||||
CHECKS = [0]
|
||||
|
||||
|
||||
def check(name, oracle_val, go_val):
|
||||
CHECKS[0] += 1
|
||||
a = json.dumps(oracle_val, sort_keys=True, ensure_ascii=False)
|
||||
b = json.dumps(go_val, sort_keys=True, ensure_ascii=False)
|
||||
if a != b:
|
||||
FAILS.append(f"{name}:\n oracle: {a[:300]}\n go: {b[:300]}")
|
||||
|
||||
|
||||
# ── clock pin (aware — fire() mixes naive/aware now()) ────────────────────
|
||||
|
||||
def pin_clock():
|
||||
base = datetime.fromisoformat(FROZEN_NOW).replace(tzinfo=timezone.utc)
|
||||
|
||||
class Frozen(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
if tz is not None:
|
||||
return base.replace(tzinfo=tz)
|
||||
return base
|
||||
|
||||
old = mk.datetime
|
||||
mk.datetime = Frozen
|
||||
return old
|
||||
|
||||
|
||||
# ── Go side ────────────────────────────────────────────────────────────────
|
||||
|
||||
def run_go(binpath, req, env_dir=None):
|
||||
env = dict(os.environ)
|
||||
if env_dir:
|
||||
env["RUNE_MEMORY_DIR"] = env_dir
|
||||
p = subprocess.run([str(binpath), "parity"], input=json.dumps(req),
|
||||
capture_output=True, text=True, env=env, timeout=120)
|
||||
if p.returncode != 0:
|
||||
raise RuntimeError(f"go op {req.get('op')} exited {p.returncode}: "
|
||||
f"{p.stdout} {p.stderr}")
|
||||
return json.loads(p.stdout)
|
||||
|
||||
|
||||
# ── normalization ──────────────────────────────────────────────────────────
|
||||
|
||||
def norm_fire(result):
|
||||
out = copy.deepcopy(result)
|
||||
if "total_duration_ms" in out:
|
||||
out["total_duration_ms"] = "<ms>"
|
||||
for r in out.get("results", []):
|
||||
if "duration_ms" in r:
|
||||
r["duration_ms"] = "<ms>"
|
||||
out["results"] = sorted(out.get("results", []), key=lambda r: r.get("name", ""))
|
||||
return out
|
||||
|
||||
|
||||
def norm_logs(log):
|
||||
return [{"event": e.get("event"), "timestamp": e.get("timestamp"),
|
||||
"hooks_fired": e.get("hooks_fired"), "duration_ms": "<ms>"}
|
||||
for e in log]
|
||||
|
||||
|
||||
def oracle_fire_with(manifest, event, data, timeout):
|
||||
"""Fire through the live oracle; the manifest's _file must already
|
||||
point at an existing fixture script (or a ghost for not-found)."""
|
||||
work = tempfile.mkdtemp(prefix="mk_par_fire_")
|
||||
mk.REGISTRY_DB = os.path.join(work, "registry.json")
|
||||
mk.register(dict(manifest))
|
||||
result = mk.fire(event, data=data, timeout=timeout)
|
||||
log = mk._load_registry().get("log", [])
|
||||
return {"result": norm_fire(result), "log": norm_logs(log)}
|
||||
|
||||
|
||||
def go_registry_blob(manifest):
|
||||
return {
|
||||
"plugins": {manifest["name"]: {
|
||||
"manifest": {"name": manifest["name"]},
|
||||
"registered_at": FROZEN_NOW_AWARE,
|
||||
"active": bool(manifest.get("auto_activate", True)),
|
||||
"file": manifest.get("_file", ""),
|
||||
}},
|
||||
"capabilities": {},
|
||||
"hooks": {ev: [manifest["name"]] for ev, on in manifest.get("hooks", {}).items() if on},
|
||||
"log": [],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bin", default="/tmp/mk-parity-bin")
|
||||
args = ap.parse_args()
|
||||
binpath = Path(args.bin)
|
||||
|
||||
old_clock = pin_clock()
|
||||
|
||||
# ── now_iso: FORMAT parity (wall clock — both must emit the shape) ──
|
||||
CHECKS[0] += 1
|
||||
shape = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$")
|
||||
if not (shape.match(mk.now_iso()) and shape.match(run_go(binpath, {"op": "now_iso"})["result"])):
|
||||
FAILS.append(f"now_iso shape: oracle {mk.now_iso()!r} go {run_go(binpath, {'op': 'now_iso'})['result']!r}")
|
||||
|
||||
# ── extract_manifest: content sweep ──────────────────────────────
|
||||
contents = [
|
||||
'PLUGIN = {\n "name": "x",\n "capabilities": ["a", "b"],\n "hooks": {"e": "handle_hook"},\n "priority": 2,\n "auto_activate": False,\n}\n',
|
||||
'PLUGINX = {\n "name": "prefix-trap",\n}\n', # startswith("PLUGIN")
|
||||
'PLUGIN={"name":"dense","n":0.5}\n', # no spaces
|
||||
"PLUGIN = {'k': 'v', 't': (1, 2), 'l': [1, [2, 3]]}\n", # tuple literal
|
||||
'PLUGIN = {\n "name": "é中",\n "s": "tab\\tnewline\\nquote\\"q",\n}\n',
|
||||
'PLUGIN = {\n "name": "trail",\n "arr": [1, 2, 3,],\n}\n', # trailing comma in list
|
||||
'no manifest here\n',
|
||||
'PLUGIN = {\n "name": "never-closed",\n',
|
||||
]
|
||||
for i, content in enumerate(contents):
|
||||
fd, fpath = tempfile.mkstemp(suffix=".py")
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(content)
|
||||
try:
|
||||
oracle = copy.deepcopy(mk._extract_manifest(fpath))
|
||||
finally:
|
||||
os.unlink(fpath)
|
||||
go = run_go(binpath, {"op": "extract_manifest", "content": content})["result"]
|
||||
check(f"extract_manifest[{i}]", oracle, go)
|
||||
|
||||
# ── discover: mixed fixture dir ──────────────────────────────────
|
||||
work = tempfile.mkdtemp(prefix="mk_par_disc_")
|
||||
fixture = Path(work) / "scripts"
|
||||
fixture.mkdir()
|
||||
files = {
|
||||
"alpha.py": 'PLUGIN = {\n "name": "alpha",\n "capabilities": ["cap"],\n "hooks": {"evt": "run"},\n "priority": 1,\n}\n',
|
||||
"beta.py": 'PLUGIN = {\n "name": "beta",\n "capabilities": ["cap"],\n "priority": 9,\n}\n',
|
||||
"gamma.py": 'PLUGIN = {"version": "0"}\n', # no name → error row
|
||||
"delta.py": 'def plain():\n pass\n', # no PLUGIN → skipped
|
||||
"_under.py": 'PLUGIN = {"name": "under"}\n',
|
||||
"microkernel.py": 'PLUGIN = {"name": "self"}\n',
|
||||
}
|
||||
for fname, src in files.items():
|
||||
(fixture / fname).write_text(src)
|
||||
|
||||
real_listdir = os.listdir
|
||||
old_dir, old_db = mk.SCRIPTS_DIR, mk.REGISTRY_DB
|
||||
mk.SCRIPTS_DIR = str(fixture)
|
||||
mk.REGISTRY_DB = os.path.join(work, "registry.json")
|
||||
os.listdir = lambda p: sorted(files) if str(p) == str(fixture) else real_listdir(p)
|
||||
try:
|
||||
oracle_result = copy.deepcopy(mk.discover())
|
||||
oracle_state = mk._load_registry()
|
||||
finally:
|
||||
os.listdir = real_listdir
|
||||
mk.SCRIPTS_DIR, mk.REGISTRY_DB = old_dir, old_db
|
||||
oracle_proj = {
|
||||
"plugins": {n: {"active": p.get("active"), "registered_at": p.get("registered_at"),
|
||||
"file": p.get("file", "").replace(str(fixture), "<dir>")}
|
||||
for n, p in oracle_state["plugins"].items()},
|
||||
"capabilities": oracle_state["capabilities"],
|
||||
"hooks": oracle_state["hooks"],
|
||||
}
|
||||
|
||||
go = run_go(binpath, {"op": "discover", "dir": str(fixture), "now": FROZEN_NOW_AWARE})
|
||||
go_proj = {
|
||||
"plugins": {n: {"active": p["active"], "registered_at": p["registered_at"],
|
||||
"file": p["file"].replace(str(fixture), "<dir>")}
|
||||
for n, p in go["plugins"].items()},
|
||||
"capabilities": go["capabilities"],
|
||||
"hooks": go["hooks"],
|
||||
}
|
||||
check("discover.result", oracle_result, go["result"])
|
||||
check("discover.registry", oracle_proj, go_proj)
|
||||
|
||||
# ── registry load/save shapes ────────────────────────────────────
|
||||
old_db = mk.REGISTRY_DB
|
||||
load_dir = tempfile.mkdtemp(prefix="mk_par_load_")
|
||||
|
||||
full = json.dumps({
|
||||
"plugins": {"q": {"manifest": {"name": "q", "priority": 3},
|
||||
"registered_at": FROZEN_NOW_AWARE, "active": False,
|
||||
"file": "/y/q.py"}},
|
||||
"capabilities": {"c": ["q"]},
|
||||
"hooks": {"e2": ["q"]},
|
||||
"log": [{"event": "e2", "timestamp": FROZEN_NOW_AWARE,
|
||||
"hooks_fired": 2, "duration_ms": 3.25}],
|
||||
}, indent=2)
|
||||
|
||||
def oracle_load(content):
|
||||
mk.REGISTRY_DB = os.path.join(load_dir, "registry.json")
|
||||
try:
|
||||
db = mk.REGISTRY_DB
|
||||
if content is None:
|
||||
if os.path.exists(db):
|
||||
os.unlink(db)
|
||||
else:
|
||||
with open(db, "w") as f:
|
||||
f.write(content)
|
||||
reg = mk._load_registry()
|
||||
return {"plugins": {n: {"active": p.get("active"),
|
||||
"registered_at": p.get("registered_at"),
|
||||
"file": p.get("file", "")}
|
||||
for n, p in reg.get("plugins", {}).items()},
|
||||
"capabilities": reg.get("capabilities", {}),
|
||||
"hooks": reg.get("hooks", {}),
|
||||
"log": reg.get("log", [])}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
|
||||
for label, content in (("missing", None), ("corrupt", "}{ nope"), ("full", full)):
|
||||
oracle = oracle_load(content)
|
||||
go = run_go(binpath, {"op": "registry_load", "content": content}, env_dir=load_dir)
|
||||
check(f"registry_load.{label}", oracle,
|
||||
{"plugins": {n: {"active": p["active"],
|
||||
"registered_at": p["registered_at"], "file": p["file"]}
|
||||
for n, p in go["plugins"].items()},
|
||||
"capabilities": go["capabilities"], "hooks": go["hooks"], "log": go["log"]})
|
||||
|
||||
# roundtrip
|
||||
mk.REGISTRY_DB = os.path.join(load_dir, "registry.json")
|
||||
try:
|
||||
first = mk._load_registry()
|
||||
mk._save_registry(first)
|
||||
again = mk._load_registry()
|
||||
oracle_rt = {"stable": first == again}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
go_rt = run_go(binpath, {"op": "registry_roundtrip"}, env_dir=load_dir)
|
||||
check("registry_roundtrip", oracle_rt, {"stable": go_rt["stable"]})
|
||||
|
||||
# ── fire / _run_hook branch matrix ───────────────────────────────
|
||||
evt = "evt"
|
||||
fire_cases = [
|
||||
("ok_json", OK_JSON, {"name": "okp", "hooks": {evt: True}}, evt, {"a": 1, "b": "x"}, 30),
|
||||
("raw_text", RAW_LONG, {"name": "rawp", "hooks": {evt: True}}, evt, None, 30),
|
||||
("rc_error", BOOM, {"name": "boomp", "hooks": {evt: True}}, evt, None, 30),
|
||||
("timeout", SLOW, {"name": "slowp", "hooks": {evt: True}}, evt, None, 1),
|
||||
("skipped", OK_JSON, {"name": "offp", "hooks": {evt: True}, "auto_activate": False}, evt, None, 30),
|
||||
("ghost_file", None, {"name": "ghostp", "hooks": {evt: True}, "_file": "/nonexistent/ghost.py"}, evt, None, 30),
|
||||
("empty_file", None, {"name": "nofilep", "hooks": {evt: True}}, evt, None, 30),
|
||||
("no_listeners", OK_JSON, {"name": "lonely", "capabilities": ["c"]}, "nope_evt", None, 30),
|
||||
]
|
||||
for label, script, manifest, event, data, timeout in fire_cases:
|
||||
manifest = dict(manifest)
|
||||
if script is not None:
|
||||
work = tempfile.mkdtemp(prefix="mk_par_fx_")
|
||||
fpath = os.path.join(work, "plugin.py")
|
||||
with open(fpath, "w") as f:
|
||||
f.write(script)
|
||||
manifest["_file"] = fpath
|
||||
oracle = oracle_fire_with(manifest, event, data, timeout)
|
||||
blob = go_registry_blob(manifest)
|
||||
go = run_go(binpath, {"op": "fire", "registry": blob, "event": event,
|
||||
"data": data, "timeout_ms": timeout * 1000,
|
||||
"now": FROZEN_NOW_AWARE})
|
||||
go = {"result": norm_fire(go["result"]), "log": norm_logs(go["log"])}
|
||||
check(f"fire.{label}", oracle, go)
|
||||
|
||||
# two listeners in parallel
|
||||
work2 = tempfile.mkdtemp(prefix="mk_par_two_")
|
||||
tmpdb2 = os.path.join(work2, "registry.json")
|
||||
old_db = mk.REGISTRY_DB
|
||||
mk.REGISTRY_DB = tmpdb2
|
||||
try:
|
||||
for nm in ("zz", "aa"):
|
||||
fpath = os.path.join(work2, nm + ".py")
|
||||
with open(fpath, "w") as f:
|
||||
f.write(OK_JSON)
|
||||
mk.register({"name": nm, "hooks": {evt: True}, "_file": fpath})
|
||||
oracle = {"result": norm_fire(mk.fire(evt, data={"z": 1}, timeout=30)),
|
||||
"log": norm_logs(mk._load_registry().get("log", []))}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
blob = {
|
||||
"plugins": {
|
||||
"zz": {"manifest": {"name": "zz"}, "registered_at": FROZEN_NOW_AWARE,
|
||||
"active": True, "file": os.path.join(work2, "zz.py")},
|
||||
"aa": {"manifest": {"name": "aa"}, "registered_at": FROZEN_NOW_AWARE,
|
||||
"active": True, "file": os.path.join(work2, "aa.py")},
|
||||
},
|
||||
"capabilities": {}, "hooks": {evt: ["zz", "aa"]}, "log": [],
|
||||
}
|
||||
go = run_go(binpath, {"op": "fire", "registry": blob, "event": evt,
|
||||
"data": {"z": 1}, "timeout_ms": 30000, "now": FROZEN_NOW_AWARE})
|
||||
go = {"result": norm_fire(go["result"]), "log": norm_logs(go["log"])}
|
||||
check("fire.two_listeners", oracle, go)
|
||||
|
||||
mk.datetime = old_clock
|
||||
|
||||
print(f"{'FAIL' if FAILS else 'OK'}: {CHECKS[0] - len(FAILS)}/{CHECKS[0]} checks passed")
|
||||
for f in FAILS:
|
||||
print(" " + f)
|
||||
return 1 if FAILS else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check_model_profiles_parity.py — live oracle-vs-Go parity sweep for the
|
||||
model_profiles gap closure (#2479, contract factory-gap-mp).
|
||||
|
||||
The frozen envelope (frozen_mp_truth.json, 36 cases) pins the seam
|
||||
surface byte-identically. THIS script drives the LIVE Python oracle
|
||||
(src/rune/model_profiles.py) and the Go binary across the broader
|
||||
surface the frozen corpus samples: every model family + unknown,
|
||||
parameter/quantization edge shapes, the sanitization matrix, the
|
||||
to_dict/from_dict lens (incl. unknown-key filtering and defaults),
|
||||
build assembly across families (uncensored tier, display/size
|
||||
overrides), record sequences (dedup, empty-topic, success mix),
|
||||
prompt-context shapes, the scan sweep against a canned Ollama server
|
||||
(fresh + existing-preservation + banker's rounding + nameless skip +
|
||||
dead-port degrade), and save/load round-trips (byte-identical raw
|
||||
text). Timestamps are pinned via the binary's "now" seam and the
|
||||
oracle's module-global datetime patch; stores are per-case hermetic
|
||||
temp dirs on both sides.
|
||||
|
||||
Usage: PYTHONPATH=src python3 scripts/check_model_profiles_parity.py
|
||||
[--bin src/go/model-profiles/model-profiles]
|
||||
Exit 0 on 100% parity, 1 with the first divergence.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
import rune.model_profiles as mp # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T09:41:23.123456"
|
||||
|
||||
NAMES = [
|
||||
"qwen2.5:7b", "Qwen3:14b", "llama3.1:8b", "mistral-7b", "gemma2:9b",
|
||||
"phi3:mini", "deepseek-r1:32b", "DavidAU-Qwen-14b", "some-new-model",
|
||||
"library/qwen3:14b", "a?b:c/d", "model-70B-GGUF", "Qwen2.5-0.5b",
|
||||
"dolphin-uncensored:7b", "qwen2.5:14b-instruct-q5_K_M",
|
||||
"llama-3-8b-Q4_K_M", "deepseek-r1:70b-IQ2_M", "phi-4-14b-int4",
|
||||
"model-3B-FP16", "tiny-INT8", "x-GGUF-fp32",
|
||||
]
|
||||
|
||||
QUANT_NAMES = [
|
||||
"m-Q4_K_M", "m-IQ3_XS", "m-q5_k_m", "m-iq2_m", "m-GGUF", "m-gguf",
|
||||
"m-FP16", "m-FP32", "m-INT8", "m-INT4", "m-none",
|
||||
]
|
||||
|
||||
BUILD_CASES = [
|
||||
("qwen3:14b", {"display_name": "Qwen3 14B", "size_gb": 9.0}),
|
||||
("mistral-7b", None),
|
||||
("llama3.1:8b", {"display_name": "L", "size_gb": 4.5}),
|
||||
("gemma2:9b", {"display_name": "G"}),
|
||||
("phi3:mini", {"size_gb": 2.1}),
|
||||
("deepseek-r1:32b", {"display_name": "DS", "size_gb": 19.2}),
|
||||
("DavidAU-X:7b", None),
|
||||
("dolphin-uncensored:7b", None),
|
||||
("mystery-model", None),
|
||||
("mystery-model", {"display_name": "M", "size_gb": 1.5}),
|
||||
]
|
||||
|
||||
RECORD_SEQS = [
|
||||
[[True, ""]],
|
||||
[[False, "refuses weapons"]],
|
||||
[[False, "t1"], [False, "t1"]],
|
||||
[[False, ""]],
|
||||
[[True, ""], [False, "a"], [True, ""], [False, "b"], [False, "a"]],
|
||||
[[False, "x"], [False, "y"], [False, "z"]],
|
||||
]
|
||||
|
||||
CTX_SEEDS = [
|
||||
{"name": "full:1b", "strengths": ["s1", "s2", "s3", "s4"],
|
||||
"prompting_tips": ["tip one", "tip two"],
|
||||
"known_refusal_topics": ["r1", "r2", "r3", "r4"], "safety_tier": 2},
|
||||
{"name": "tips:1b", "prompting_tips": ["only tip"]},
|
||||
{"name": "tier:1b", "safety_tier": 1},
|
||||
{"name": "refusals:1b", "known_refusal_topics": ["r1"]},
|
||||
]
|
||||
|
||||
SCAN_MODELS = [
|
||||
{"name": "qwen2.5:7b", "size": 7516192768},
|
||||
{"name": "banker-lo:2b", "size": 2415919104},
|
||||
{"name": "banker-hi:2b", "size": 2952790016},
|
||||
{"name": "llama3.1:8b", "size": 4831838208},
|
||||
{"size": 999},
|
||||
{"name": "sizeless:1b"},
|
||||
]
|
||||
|
||||
|
||||
class _TagsHandler(BaseHTTPRequestHandler):
|
||||
payload = b"{}"
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(self.payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(self.payload)
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
def canned_server(fixture):
|
||||
handler = type("H", (_TagsHandler,),
|
||||
{"payload": json.dumps(fixture).encode()})
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
return srv
|
||||
|
||||
|
||||
def run_go(binpath, req):
|
||||
p = subprocess.run([str(binpath)], input=json.dumps(req),
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"go op {req.get('op')} exited {p.returncode}: {p.stdout} {p.stderr}")
|
||||
return json.loads(p.stdout)["result"]
|
||||
|
||||
|
||||
def fresh_store():
|
||||
d = tempfile.mkdtemp(prefix="mp_parity_")
|
||||
return os.path.join(d, ".rune", "model_profiles")
|
||||
|
||||
|
||||
def oracle_in_store(store):
|
||||
"""Point the oracle at a hermetic store (PROFILES_DIR is module-global)."""
|
||||
mp.PROFILES_DIR = store
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bin", default=str(
|
||||
REPO / "src/go/model-profiles/model-profiles"))
|
||||
args = ap.parse_args()
|
||||
binpath = Path(args.bin)
|
||||
if not binpath.exists():
|
||||
binpath = Path("/tmp/mp-go")
|
||||
fails = total = 0
|
||||
|
||||
def canon(v):
|
||||
# Integral-float law (the context_broker precedent): the oracle's
|
||||
# build path assigns int 0 where the dataclass declares float
|
||||
# (size_gb=0 vs 0.0) — numerically identical, textually distinct.
|
||||
# Canonicalize integral floats to int on BOTH sides before compare.
|
||||
if isinstance(v, float) and v.is_integer():
|
||||
return int(v)
|
||||
if isinstance(v, dict):
|
||||
return {k: canon(x) for k, x in v.items()}
|
||||
if isinstance(v, list):
|
||||
return [canon(x) for x in v]
|
||||
return v
|
||||
|
||||
def check(name, oracle_val, go_val):
|
||||
nonlocal fails, total
|
||||
total += 1
|
||||
if json.dumps(canon(oracle_val), sort_keys=True) != json.dumps(canon(go_val), sort_keys=True):
|
||||
fails += 1
|
||||
print(f"DIVERGE {name}:\n oracle: {json.dumps(oracle_val)[:300]}\n"
|
||||
f" go: {json.dumps(go_val)[:300]}")
|
||||
|
||||
# pure detection surface (no store, no clock)
|
||||
for n in NAMES:
|
||||
check(f"family:{n}", mp.detect_family(n),
|
||||
run_go(binpath, {"op": "detect_family", "name": n}))
|
||||
for n in NAMES + QUANT_NAMES:
|
||||
check(f"params:{n}", mp.detect_parameters(n),
|
||||
run_go(binpath, {"op": "detect_parameters", "name": n}))
|
||||
check(f"quant:{n}", mp.detect_quantization(n),
|
||||
run_go(binpath, {"op": "detect_quantization", "name": n}))
|
||||
for inter, succ in [(0, 0), (1, 1), (10, 7), (4, 0), (3, 2)]:
|
||||
p = mp.ModelProfile(name="x", interactions=inter, successes=succ)
|
||||
check(f"success_rate:{inter}/{succ}", p.success_rate,
|
||||
run_go(binpath, {"op": "success_rate",
|
||||
"interactions": inter, "successes": succ}))
|
||||
|
||||
# sanitization matrix (compare file NAMES — store roots differ)
|
||||
for n in NAMES:
|
||||
store = fresh_store()
|
||||
oracle_in_store(store)
|
||||
oracle_name = os.path.basename(mp.get_profile_path(n))
|
||||
go_name = run_go(binpath, {"op": "get_profile_path",
|
||||
"name": n, "store": store})
|
||||
check(f"path:{n}", oracle_name, go_name)
|
||||
|
||||
# to_dict / from_dict lens
|
||||
full = mp.ModelProfile(
|
||||
name="qwen2.5:7b", display_name="Q ✓", family="qwen",
|
||||
parameters="7B", quantization="Q4_K_M", size_gb=4.7,
|
||||
strengths=["a", "b"], weaknesses=["c"], prompting_tips=["t1"],
|
||||
safety_tier=2, known_refusal_topics=["r1"],
|
||||
thinking_format="<think>", response_style="concise",
|
||||
context_window=32768, optimal_temperature=0.7,
|
||||
interactions=10, successes=7, failures=3,
|
||||
last_used="2026-10-12T00:00:00+00:00",
|
||||
created="2026-10-01T00:00:00+00:00")
|
||||
check("to_dict:full", full.to_dict(),
|
||||
run_go(binpath, {"op": "to_dict", **full.to_dict()}))
|
||||
for doc in ({"name": "n", "bogus": 1, "context_window": 8192},
|
||||
{"name": "n2"},
|
||||
dataclasses.asdict(full)):
|
||||
check(f"from_dict:{doc.get('name')}",
|
||||
dataclasses.asdict(mp.ModelProfile.from_dict(doc)),
|
||||
run_go(binpath, {"op": "from_dict", **doc}))
|
||||
|
||||
# build assembly (pin the oracle clock; binary takes "now")
|
||||
import datetime as dtmod
|
||||
real_dt = dtmod.datetime
|
||||
|
||||
class FrozenDT(real_dt):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
base = real_dt(2026, 10, 13, 9, 41, 23, 123456)
|
||||
return base.replace(tzinfo=tz) if tz is not None else base
|
||||
|
||||
mp.datetime = FrozenDT
|
||||
try:
|
||||
for name, info in BUILD_CASES:
|
||||
oracle = dataclasses.asdict(
|
||||
mp.build_profile_from_ollama(name, info))
|
||||
go = run_go(binpath, {"op": "build_profile", "name": name,
|
||||
"model_info": info, "now": FROZEN_NOW + "+00:00"})
|
||||
check(f"build:{name}", oracle, go)
|
||||
finally:
|
||||
mp.datetime = real_dt
|
||||
|
||||
# record sequences + prompt context (hermetic store each, both
|
||||
# sides replay the same sequence from empty, then compare docs)
|
||||
mp.datetime = FrozenDT
|
||||
try:
|
||||
for i, seq in enumerate(RECORD_SEQS):
|
||||
ostore, gstore = fresh_store(), fresh_store()
|
||||
oracle_in_store(ostore)
|
||||
for success, topic in seq:
|
||||
mp.record_interaction(f"m{i}:1b", success, topic)
|
||||
for success, topic in seq:
|
||||
run_go(binpath, {"op": "record", "name": f"m{i}:1b",
|
||||
"success": success, "topic": topic,
|
||||
"store": gstore, "now": FROZEN_NOW + "+00:00"})
|
||||
oracle_in_store(ostore)
|
||||
o_doc = dataclasses.asdict(mp.load_profile(f"m{i}:1b"))
|
||||
g_doc = run_go(binpath, {"op": "load_profile",
|
||||
"name": f"m{i}:1b", "store": gstore})
|
||||
check(f"record-x[{i}]", o_doc, g_doc)
|
||||
finally:
|
||||
mp.datetime = real_dt
|
||||
|
||||
for seed in CTX_SEEDS:
|
||||
name = seed["name"]
|
||||
ostore, gstore = fresh_store(), fresh_store()
|
||||
oracle_in_store(ostore)
|
||||
mp.save_profile(mp.ModelProfile.from_dict(seed))
|
||||
go_state = dict(seed)
|
||||
run_go(binpath, {"op": "save_profile", "store": gstore, **go_state})
|
||||
oracle_in_store(ostore)
|
||||
o_ctx = mp.get_prompt_context(name)
|
||||
g_ctx = run_go(binpath, {"op": "prompt_context",
|
||||
"name": name, "store": gstore})
|
||||
check(f"ctx:{name}", o_ctx, g_ctx)
|
||||
|
||||
# save/load round-trip byte parity (raw file text)
|
||||
for doc in (dataclasses.asdict(full), {"name": "plain-model"},
|
||||
{"name": "d:1b", "family": "deepseek", "size_gb": 0.0,
|
||||
"safety_tier": 3, "optimal_temperature": 0.6}):
|
||||
ostore, gstore = fresh_store(), fresh_store()
|
||||
oracle_in_store(ostore)
|
||||
mp.save_profile(mp.ModelProfile.from_dict(doc))
|
||||
with open(mp.get_profile_path(doc["name"])) as f:
|
||||
o_raw = f.read()
|
||||
g_raw = run_go(binpath, {"op": "save_profile",
|
||||
"store": gstore, **doc})
|
||||
check(f"save-raw:{doc['name']}", o_raw, g_raw)
|
||||
|
||||
# scan sweep (canned server; existing-preservation; dead degrade)
|
||||
for label, fixture, seed in (
|
||||
("fresh", {"models": SCAN_MODELS}, None),
|
||||
("existing", {"models": SCAN_MODELS[:3]}, {"name": "qwen2.5:7b",
|
||||
"family": "qwen", "interactions": 5, "successes": 4,
|
||||
"known_refusal_topics": ["old"], "size_gb": 1.0}),
|
||||
("empty", {"models": []}, None),
|
||||
("nameless", {"models": [{"size": 1}, {}]}, None)):
|
||||
srv = canned_server(fixture)
|
||||
url = f"http://127.0.0.1:{srv.server_address[1]}"
|
||||
try:
|
||||
ostore, gstore = fresh_store(), fresh_store()
|
||||
oracle_in_store(ostore)
|
||||
if seed:
|
||||
mp.save_profile(mp.ModelProfile.from_dict(seed))
|
||||
# seed the GO store with the same file the oracle has
|
||||
os.makedirs(gstore, exist_ok=True)
|
||||
for f in os.listdir(ostore):
|
||||
shutil.copy(os.path.join(ostore, f),
|
||||
os.path.join(gstore, f))
|
||||
saved_url = mp.OLLAMA_URL
|
||||
mp.OLLAMA_URL = url
|
||||
mp.datetime = FrozenDT
|
||||
try:
|
||||
o_profiles = [dataclasses.asdict(p)
|
||||
for p in mp.scan_all_models()]
|
||||
finally:
|
||||
mp.OLLAMA_URL = saved_url
|
||||
mp.datetime = real_dt
|
||||
g_profiles = run_go(binpath, {"op": "scan", "store": gstore,
|
||||
"ollama_url": url,
|
||||
"now": FROZEN_NOW + "+00:00"})
|
||||
check(f"scan:{label}", o_profiles, g_profiles)
|
||||
finally:
|
||||
srv.shutdown()
|
||||
|
||||
# dead-port degrade
|
||||
ostore, gstore = fresh_store(), fresh_store()
|
||||
oracle_in_store(ostore)
|
||||
saved_url = mp.OLLAMA_URL
|
||||
mp.OLLAMA_URL = "http://127.0.0.1:1"
|
||||
try:
|
||||
o_dead = mp.scan_all_models()
|
||||
finally:
|
||||
mp.OLLAMA_URL = saved_url
|
||||
g_dead = run_go(binpath, {"op": "scan", "store": gstore,
|
||||
"ollama_url": "http://127.0.0.1:1"})
|
||||
check("scan:dead", o_dead, g_dead)
|
||||
|
||||
print(f"parity: {total - fails}/{total} match")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check_parallel_processor_parity.py — live oracle-vs-Go parity sweep for
|
||||
the parallel_processor gap closure (#2479, contract factory-gap-pp).
|
||||
|
||||
The frozen envelope (truth.json) pins the ported surface byte-identically.
|
||||
THIS script drives the LIVE Python oracle and the Go binary (stdin op
|
||||
dispatch, `parallel-processor op`) across every op family — run_parallel,
|
||||
run_parallel_with_args (full parse-branch matrix), class pools at several
|
||||
worker counts, status, singleton/global lifecycle, crash parity — plus the
|
||||
CLI surface (status / test / unknown / usage), comparing sort_keys JSON
|
||||
(structural) or exact lines. Exit 0 on 100% parity.
|
||||
|
||||
Usage: PYTHONPATH=scripts/pp_freeze_seams:src python3 scripts/check_parallel_processor_parity.py
|
||||
[--bin src/go/parallel-processor/parallel-processor]
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
# the rig (on PYTHONPATH) pins threading.active_count to this and binds
|
||||
# the spec seams; the Go seam is pinned to the same value per process.
|
||||
FROZEN_ACTIVE = 4
|
||||
|
||||
import rune.parallel_processor as pp # noqa: E402
|
||||
|
||||
|
||||
def s(kind, **kw):
|
||||
d = {"kind": kind}
|
||||
d.update(kw)
|
||||
return d
|
||||
|
||||
|
||||
# ── oracle side (rig seams give the spec grammar real callables) ────────────
|
||||
|
||||
def py_op(op, req):
|
||||
if op == "run_parallel":
|
||||
return pp.run_parallel([_zero(t) for t in req["tasks"]])
|
||||
if op == "run_parallel_with_args":
|
||||
return pp.run_parallel_with_args([_task(t) for t in req["tasks"]])
|
||||
if op == "class_run_parallel":
|
||||
p = pp.ParallelProcessor(req["max_workers"])
|
||||
r = p.run_parallel([_zero(t) for t in req["tasks"]])
|
||||
p.shutdown(wait=True)
|
||||
return r
|
||||
if op == "class_run_parallel_with_args":
|
||||
p = pp.ParallelProcessor(req["max_workers"])
|
||||
r = p.run_parallel_with_args([_task(t) for t in req["tasks"]])
|
||||
p.shutdown(wait=True)
|
||||
return r
|
||||
if op == "get_status":
|
||||
return pp.get_status()
|
||||
if op == "class_get_status":
|
||||
p = pp.ParallelProcessor(req["max_workers"])
|
||||
r = {"result": p.get_status(), "max_workers": req["max_workers"]}
|
||||
p.shutdown(wait=True)
|
||||
return r
|
||||
if op == "get_processor_status":
|
||||
return pp._pp_get_processor_status()
|
||||
if op == "shutdown_then_status":
|
||||
return pp._pp_shutdown_then_status()
|
||||
if op == "shutdown_then_run":
|
||||
return pp._pp_shutdown_then_run()
|
||||
if op == "new_bad":
|
||||
return pp._pp_new_bad()
|
||||
if op == "shutdown_processor_cycle":
|
||||
return pp._pp_shutdown_processor_cycle()
|
||||
raise ValueError(f"unknown op {op}")
|
||||
|
||||
|
||||
def _zero(spec):
|
||||
return pp._pp_spec_zero(spec)
|
||||
|
||||
|
||||
def _task(spec):
|
||||
return pp._pp_spec_task(spec)
|
||||
|
||||
|
||||
# ── Go side (one fresh process per op) ──────────────────────────────────────
|
||||
|
||||
def go_op(binpath, op, req):
|
||||
full = dict(req)
|
||||
full["op"] = op
|
||||
env = dict(os.environ)
|
||||
env["PP_ACTIVE_THREADS"] = str(FROZEN_ACTIVE)
|
||||
p = subprocess.run([str(binpath), "op"], input=json.dumps(full),
|
||||
capture_output=True, text=True, env=env, cwd=str(REPO))
|
||||
if p.returncode != 0:
|
||||
raise RuntimeError(f"go op {op} exited {p.returncode}: {p.stdout} {p.stderr}")
|
||||
return json.loads(p.stdout)
|
||||
|
||||
|
||||
def jdump(v):
|
||||
return json.dumps(v, sort_keys=True)
|
||||
|
||||
|
||||
# ── the sweep ───────────────────────────────────────────────────────────────
|
||||
|
||||
OPS = [
|
||||
("run_parallel", {"tasks": [s("call", fn="const", ret=1), s("call", fn="boom", msg="x"), 42, None, "s", True, 4.5, [1], {"a": 1}]}),
|
||||
("run_parallel", {"tasks": []}),
|
||||
("run_parallel", {"tasks": [s("call", fn="sleepret", ms=50, ret="a"), s("call", fn="sleepret", ms=5, ret="b"), s("call", fn="sleepret", ms=25, ret="c")]}),
|
||||
("run_parallel_with_args", {"tasks": [
|
||||
s("call", fn="echo", args=[1, 2], kwargs={"scale": 10}),
|
||||
s("call", fn="add", args=[7, 8]),
|
||||
s("call", fn="add", args=[2.25, 1]),
|
||||
s("call", fn="boom", msg="wops"),
|
||||
s("raw", value=3.5),
|
||||
s("emptytuple"),
|
||||
s("notcall", value=[3, 4]),
|
||||
s("argsnotseq", fn="echo", value="xy"),
|
||||
s("kwnotdict", fn="echo", value="kw"),
|
||||
]}),
|
||||
("class_run_parallel", {"max_workers": 2, "tasks": [s("call", fn="const", ret="p"), s("call", fn="const", ret="q"), s("call", fn="boom", msg="in-class"), s("call", fn="sleepret", ms=15, ret="r")]}),
|
||||
("class_run_parallel", {"max_workers": 8, "tasks": [s("raw", value=False)] * 3 + [s("call", fn="const", ret="ok")]}),
|
||||
("class_run_parallel_with_args", {"max_workers": 1, "tasks": [s("call", fn="echo"), s("notcall", value=0), s("call", fn="add", args=[20, 22])]}),
|
||||
("get_status", {}),
|
||||
("class_get_status", {"max_workers": 1}),
|
||||
("class_get_status", {"max_workers": 9}),
|
||||
("get_processor_status", {}),
|
||||
("shutdown_then_status", {}),
|
||||
("shutdown_then_run", {}),
|
||||
("new_bad", {}),
|
||||
("shutdown_processor_cycle", {}),
|
||||
]
|
||||
|
||||
|
||||
def cli_lines(argv, env_extra=None, cwd=None):
|
||||
env = dict(os.environ)
|
||||
if env_extra:
|
||||
env.update(env_extra)
|
||||
p = subprocess.run(argv, capture_output=True, text=True, env=env, cwd=cwd or str(REPO))
|
||||
return p.returncode, p.stdout, p.stderr
|
||||
|
||||
|
||||
def cli_parity(binpath):
|
||||
"""CLI surface: status / test / unknown / usage — the oracle's main()."""
|
||||
checks = []
|
||||
go_env = {"PP_ACTIVE_THREADS": str(FROZEN_ACTIVE)}
|
||||
|
||||
rc_p, out_p, _ = cli_lines([sys.executable, str(REPO / "src/rune/parallel_processor.py"), "status"])
|
||||
rc_g, out_g, _ = cli_lines([str(binpath), "status"], go_env)
|
||||
checks.append(("cli status", rc_p == rc_g and out_p == out_g, out_p, out_g))
|
||||
|
||||
rc_p, out_p, _ = cli_lines([sys.executable, str(REPO / "src/rune/parallel_processor.py"), "test"])
|
||||
rc_g, out_g, _ = cli_lines([str(binpath), "test"])
|
||||
# wall-clock lines vary by design; the Results line and the verdict
|
||||
# line are the deterministic contract
|
||||
det = lambda out: (out.strip().splitlines()[-2], out.strip().splitlines()[-1]) # noqa: E731
|
||||
checks.append(("cli test (results+verdict)", rc_p == rc_g and det(out_p) == det(out_g), det(out_p), det(out_g)))
|
||||
|
||||
rc_p, out_p, _ = cli_lines([sys.executable, str(REPO / "src/rune/parallel_processor.py"), "bogus"])
|
||||
rc_g, out_g, _ = cli_lines([str(binpath), "bogus"])
|
||||
checks.append(("cli unknown", rc_p == rc_g and out_p == out_g, out_p, out_g))
|
||||
|
||||
rc_p, out_p, _ = cli_lines([sys.executable, str(REPO / "src/rune/parallel_processor.py")])
|
||||
rc_g, out_g, _ = cli_lines([str(binpath)])
|
||||
checks.append(("cli usage", rc_p == rc_g and out_p == out_g, out_p, out_g))
|
||||
return checks
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bin", default=str(REPO / "src/go/parallel-processor/parallel-processor"))
|
||||
args = ap.parse_args()
|
||||
binpath = Path(args.bin)
|
||||
|
||||
failures = 0
|
||||
total = 0
|
||||
for op, req in OPS:
|
||||
total += 1
|
||||
py = py_op(op, req)
|
||||
go = go_op(binpath, op, req)
|
||||
if jdump(py) != jdump(go):
|
||||
failures += 1
|
||||
print(f"DIVERGE {op} {json.dumps(req)[:80]}")
|
||||
print(f" python: {jdump(py)[:300]}")
|
||||
print(f" go: {jdump(go)[:300]}")
|
||||
|
||||
for name, ok, want, got in cli_parity(binpath):
|
||||
total += 1
|
||||
if not ok:
|
||||
failures += 1
|
||||
print(f"DIVERGE {name}\n python: {want!r}\n go: {got!r}")
|
||||
|
||||
print(f"parity: {total - failures}/{total} match" + (" — FAILURES" if failures else ""))
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave salience_gate gap contract (RUST port).
|
||||
Full lifecycle walk in ONE process (shared-store clobber hazard; segment
|
||||
transitions can take ~60s each — do NOT split across processes). Carries
|
||||
the already-pushed parity SHAs as evidence:
|
||||
feat 9539d5b6 close PARTIAL gaps — dichotomy wave
|
||||
phaseB 2350e02e Phase B language-advantage pass — dichotomy wave
|
||||
|
||||
Usage: close_contract_salience_gate.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault(
|
||||
"CS_RUST_LIB",
|
||||
"/mnt/workdrive/rune/src/rust/capability_scoring/target/release/libcapability_scoring.so",
|
||||
)
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
PARITY_SHA = "9539d5b6"
|
||||
PHASEB_SHA = "2350e02e"
|
||||
EVIDENCE = f"parity {PARITY_SHA}, phase B {PHASEB_SHA}"
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-sg")
|
||||
|
||||
r = iface.submit_need(
|
||||
"salience_gate dichotomy gap-close (RUST): name-preserving port of the "
|
||||
"4 audit-missing functions into src/rust/salience_gate (the module's "
|
||||
"existing delegation side — classification side=RUST; no Go "
|
||||
"salience-gate dir exists): _sg_core (kernel mirror of the facade "
|
||||
"availability probe, pin lattice), to_dict (SalienceDecision struct + "
|
||||
"projection, evaluate_salience flows through it), handle_hook (event "
|
||||
"echo, data accepted+ignored), test_salience (embedded oracle smoke, "
|
||||
"print-byte-matched), plus oracle-level evaluate_salience_dict JSON "
|
||||
"view (shared freeze harness cannot serialize the dataclass return — "
|
||||
"construction_decision normalize_need_json precedent). Dichotomy audit "
|
||||
"1/5 PARTIAL -> 6/6 FULL. Three parity defects fixed (frozen-truth "
|
||||
"battery caught all): novel-family single-append (oracle guards ONE "
|
||||
"if; duplicate append inflated witness rule), failure-family "
|
||||
"doesn't-work/not-working arm omitted, word_boundary_contains Python-"
|
||||
"\\b parity (alphanumeric + underscore + UTF-8 continuation bytes). "
|
||||
"Frozen truth via shared harness scripts/freeze_truth.py: "
|
||||
"frozen_sg_truth.verify.json 52 rows byte-identical under "
|
||||
"freeze_truth.py --verify in BOTH core states (floor SG_RUST_LIB-"
|
||||
"poisoned AND kernel-built); frozen_sg_truth.json 63-row envelope "
|
||||
"(multi-family evaluation-order rows + _sg_core row — the shared "
|
||||
"gate's RUNE_SALIENCE_GATE_CORE pin cannot reach this oracle). "
|
||||
"branch_battery.rs replays both verbatim via include_str (12 tests); "
|
||||
"poisoned-truth law: generator pins RUNE_SALIENCE_CORE=python AND "
|
||||
"poisons SG_RUST_LIB (the module's real floor pins — the facade "
|
||||
"evaluate path ignores RUNE_*_CORE). Live parity: gen "
|
||||
"--replay facade live + kernel direct 119/119 (exact evaluation-order "
|
||||
"kernel rows; facade rows under the oracle's declared sorted-"
|
||||
"comparison contract); crate canary 6/6; oracle smoke green in 3 pin "
|
||||
"states (live, probe-pinned, pure floor). Phase B (2350e02e): "
|
||||
"ASCII-anchored Cow lowercase avoidance (zero-alloc common path, "
|
||||
"Unicode mappings never skipped), std-only hot-path bench (ns/call "
|
||||
"over the 56-envelope frozen corpus + ns/scan), #[must_use] pure "
|
||||
"surface, idiomatic single-append family guards. rustfmt clean, "
|
||||
"clippy -D warnings clean. Evidence: verification/salience-gate-*.log "
|
||||
"(cargotest/clippy/freeze-verify/replay/canary/python-pins/goladder/"
|
||||
"audit, all exit 0, Read back). Delivered on gitea/master: feat "
|
||||
"9539d5b6, phase B 2350e02e.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:300])
|
||||
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", "verifying", "closed"):
|
||||
t = iface.transition(c, phase, evidence_ref=EVIDENCE)
|
||||
ok = t.get("success")
|
||||
print(f"transition->{phase}:", "ok" if ok else json.dumps(t)[:300])
|
||||
if not ok:
|
||||
sys.exit(1)
|
||||
|
||||
doc = iface.scaffold.load()
|
||||
e = iface.scaffold.contract(doc, c)
|
||||
print("FINAL PHASE:", e.get("phase") if e else "MISSING")
|
||||
if not e or e.get("phase") != "closed":
|
||||
sys.exit(1)
|
||||
print("CLOSED", c)
|
||||
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_cron_engine_gap_truth.py — cron-engine gap-closing truth battery
|
||||
(#2479 dichotomy wave, GO side).
|
||||
|
||||
Closes the PARTIAL audit gaps: to_dict, run_task (method + module),
|
||||
fire_on_task_complete, start/stop, _run_loop, _execute_task,
|
||||
_log_execution/_log_error, get_cron_engine/start_engine/stop_engine
|
||||
(parseCronField/shouldRun already existed; extra branch rows frozen too).
|
||||
|
||||
Uses the SHARED harness (scripts/freeze_truth.py) with the hook_engine
|
||||
wrapper-shim law: CronEngine methods are stateful and touch the clock,
|
||||
HOME (~/.rune/cron.log), subprocesses and threads — none of which the
|
||||
bare harness pins. Every row is frozen against a module-level WRAPPER
|
||||
(_ce_* below, in THIS module registered as
|
||||
gen_cron_engine_gap_truth_shims):
|
||||
|
||||
* FROZEN clock: cron_engine.datetime patched with the harness's frozen
|
||||
class (module-level import → the patch reaches every method).
|
||||
* HERMETIC per-row HOME: fresh mkdtemp per wrapper call; every path in
|
||||
the capture is tokenized to <CE_HOME> (no absolute paths in truth).
|
||||
* _run_loop rows: cron_engine.time swapped for a stopper whose sleep()
|
||||
flips the engine's running flag — exactly ONE loop iteration, then a
|
||||
clean exit (deterministic under the frozen clock).
|
||||
* record_failure (rune.subsystem_health) is neutralized during rows —
|
||||
the oracle imports it lazily inside except blocks, and its ledger
|
||||
writes are outside the captured surface.
|
||||
* capture shape: {"result", "post": {running, storage_path, tasks
|
||||
(insertion-ordered [name, to_dict] pairs), _last_run_minute},
|
||||
"files" (~/.rune dir inventory), "storage", "log"}.
|
||||
|
||||
Verify (this file ships its own --verify — same byte-compare law as the
|
||||
harness, plus the shim-module registration the stock CLI cannot do):
|
||||
|
||||
python3 scripts/gen_cron_engine_gap_truth.py --verify
|
||||
|
||||
Usage: python3 scripts/gen_cron_engine_gap_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from freeze_truth import freeze, write_envelope, _frozen_clock # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T09:20:00"
|
||||
TOKEN = "<CE_HOME>"
|
||||
|
||||
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW
|
||||
os.environ["RUNE_CRON_ENGINE_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
from rune import cron_engine as CE # noqa: E402
|
||||
import rune.subsystem_health as _SH # noqa: E402
|
||||
|
||||
_CE = {"ready": False}
|
||||
|
||||
|
||||
def _prepare():
|
||||
"""Idempotent per-process pinning: frozen clock + neutral recorder."""
|
||||
if not _CE["ready"]:
|
||||
CE.datetime = _frozen_clock(FROZEN_NOW)
|
||||
_SH.record_failure = lambda *a, **k: True
|
||||
_CE["ready"] = True
|
||||
return CE
|
||||
|
||||
|
||||
class _StopTime:
|
||||
"""time-module stand-in: first sleep() stops the engine's loop."""
|
||||
|
||||
def __init__(self, engine):
|
||||
self._engine = engine
|
||||
|
||||
def sleep(self, _s):
|
||||
if self._engine is not None:
|
||||
self._engine.running = False
|
||||
|
||||
|
||||
def _hermetic_home():
|
||||
home = tempfile.mkdtemp(prefix="ce-freeze-")
|
||||
os.environ["HOME"] = home
|
||||
return home
|
||||
|
||||
|
||||
def _log_path(home):
|
||||
return os.path.join(home, ".rune", "cron.log")
|
||||
|
||||
|
||||
def _build_engine(state, home):
|
||||
e = CE.CronEngine.__new__(CE.CronEngine)
|
||||
e.storage_path = state["storage_path"].replace(TOKEN, home)
|
||||
e.tasks = {}
|
||||
for name, t in state.get("tasks", {}).items():
|
||||
e.tasks[name] = CE.CronTask(**t)
|
||||
e.running = state.get("running", False)
|
||||
e.thread = None
|
||||
e._last_run_minute = dict(state.get("_last_run_minute", {}))
|
||||
return e
|
||||
|
||||
|
||||
def _seed_log(state, home):
|
||||
d = os.path.dirname(_log_path(home))
|
||||
for fname, size in state.get("seed_log", {}).items():
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(os.path.join(d, fname), "wb") as f:
|
||||
f.write(b"x" * size)
|
||||
|
||||
|
||||
def _capture(e, home, result=None):
|
||||
d = os.path.dirname(_log_path(home))
|
||||
files = {}
|
||||
if os.path.isdir(d):
|
||||
files = {f: os.path.getsize(os.path.join(d, f))
|
||||
for f in sorted(os.listdir(d))
|
||||
if os.path.isfile(os.path.join(d, f))}
|
||||
|
||||
def rd(p):
|
||||
return open(p).read() if os.path.exists(p) else None
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"post": {
|
||||
"running": e.running,
|
||||
"storage_path": e.storage_path.replace(home, TOKEN),
|
||||
"tasks": [[n, t.to_dict()] for n, t in e.tasks.items()],
|
||||
"_last_run_minute": e._last_run_minute,
|
||||
},
|
||||
"files": files,
|
||||
"storage": rd(e.storage_path),
|
||||
"log": rd(_log_path(home)),
|
||||
}
|
||||
|
||||
|
||||
# ── wrappers (the frozen fn names) ────────────────────────────────
|
||||
|
||||
def _ce_to_dict(task):
|
||||
_prepare()
|
||||
return CE.CronTask(**task).to_dict()
|
||||
|
||||
|
||||
def _ce_parse(field, low, high):
|
||||
_prepare()
|
||||
return sorted(CE.CronEngine._parse_cron_field(field, low, high))
|
||||
|
||||
|
||||
def _ce_should_run(state, name, y, mo, d, h, mi):
|
||||
ce = _prepare()
|
||||
home = _hermetic_home()
|
||||
e = _build_engine(state, home)
|
||||
now = ce.datetime(y, mo, d, h, mi, tzinfo=ce.timezone.utc)
|
||||
return e._should_run(e.tasks[name], now)
|
||||
|
||||
|
||||
def _ce_run_task(state, name):
|
||||
_prepare()
|
||||
home = _hermetic_home()
|
||||
e = _build_engine(state, home)
|
||||
return _capture(e, home, result=e.run_task(name))
|
||||
|
||||
|
||||
def _ce_fire(state):
|
||||
_prepare()
|
||||
home = _hermetic_home()
|
||||
e = _build_engine(state, home)
|
||||
return _capture(e, home, result=e.fire_on_task_complete())
|
||||
|
||||
|
||||
def _ce_stop(state):
|
||||
_prepare()
|
||||
home = _hermetic_home()
|
||||
e = _build_engine(state, home)
|
||||
e.stop()
|
||||
return _capture(e, home)
|
||||
|
||||
|
||||
def _ce_execute(state, name):
|
||||
_prepare()
|
||||
home = _hermetic_home()
|
||||
e = _build_engine(state, home)
|
||||
e._execute_task(e.tasks[name])
|
||||
return _capture(e, home)
|
||||
|
||||
|
||||
def _ce_run_loop(state):
|
||||
ce = _prepare()
|
||||
home = _hermetic_home()
|
||||
_seed_log(state, home)
|
||||
e = _build_engine(state, home)
|
||||
real = ce.time
|
||||
ce.time = _StopTime(e)
|
||||
try:
|
||||
e._run_loop()
|
||||
finally:
|
||||
ce.time = real
|
||||
return _capture(e, home)
|
||||
|
||||
|
||||
def _ce_log_execution(state, name, rc):
|
||||
_prepare()
|
||||
home = _hermetic_home()
|
||||
_seed_log(state, home)
|
||||
e = _build_engine(state, home)
|
||||
|
||||
class _R: # CompletedProcess stand-in: only returncode is read
|
||||
returncode = rc
|
||||
|
||||
e._log_execution(e.tasks[name], _R())
|
||||
return _capture(e, home)
|
||||
|
||||
|
||||
def _ce_log_error(state, name, msg):
|
||||
_prepare()
|
||||
home = _hermetic_home()
|
||||
_seed_log(state, home)
|
||||
e = _build_engine(state, home)
|
||||
e._log_error(e.tasks[name], msg)
|
||||
return _capture(e, home)
|
||||
|
||||
|
||||
def _ce_singleton(op, name="", expr="*/5 * * * *", command="echo hi"):
|
||||
ce = _prepare()
|
||||
home = _hermetic_home()
|
||||
ce._cron_engine = None
|
||||
if op == "get":
|
||||
e = ce.get_cron_engine()
|
||||
return _capture(e, home, result=len(e.list_tasks()))
|
||||
if op == "start_stop":
|
||||
# REAL sleeper: the loop thread blocks in its 60s sleep, so the
|
||||
# mid-capture (running=True) is race-free and stop()'s 5s join
|
||||
# timeout is exercised — mirrors the Go battery exactly.
|
||||
ce.start_engine()
|
||||
mid = _capture(ce._cron_engine, home)
|
||||
ce.stop_engine()
|
||||
end = _capture(ce._cron_engine, home)
|
||||
end["extra"] = mid
|
||||
return end
|
||||
if op == "module_run":
|
||||
e = ce.get_cron_engine()
|
||||
e.add_task(name, expr, command, "")
|
||||
r = ce.run_task(name)
|
||||
return _capture(ce._cron_engine, home, result=r)
|
||||
if op == "module_list":
|
||||
e = ce.get_cron_engine()
|
||||
e.add_task(name, expr, command, "d1")
|
||||
e.add_task(name + "-2", "*/5 * * * *", "echo two", "d2")
|
||||
return _capture(e, home, result=[t["name"] for t in ce.list_tasks()])
|
||||
raise ValueError(op)
|
||||
|
||||
|
||||
# ── case matrix ───────────────────────────────────────────────────
|
||||
|
||||
def _task(name, expr, command, **over):
|
||||
t = {"name": name, "cron_expression": expr, "command": command,
|
||||
"description": "", "enabled": True, "last_run": None,
|
||||
"next_run": None, "run_count": 0}
|
||||
t.update(over)
|
||||
return t
|
||||
|
||||
|
||||
def _state(tasks=None, **over):
|
||||
s = {"storage_path": TOKEN + "/.rune/cron.json", "tasks": tasks or {},
|
||||
"running": False, "_last_run_minute": {}}
|
||||
s.update(over)
|
||||
return s
|
||||
|
||||
|
||||
T_ECHO = _task("t-echo", "*/5 * * * *", "echo hello")
|
||||
|
||||
|
||||
def cases():
|
||||
C = []
|
||||
|
||||
# to_dict — pure dataclass rendering (defaults, non-ascii, full state)
|
||||
C.append(("_ce_to_dict", [dict(
|
||||
T_ECHO, last_run="2026-01-02T03:04:05+00:00",
|
||||
next_run="2026-01-02T03:04:05+00:00", run_count=7, description="d")], {}))
|
||||
C.append(("_ce_to_dict",
|
||||
[{"name": "m", "cron_expression": "* * * * *", "command": "echo m"}], {}))
|
||||
C.append(("_ce_to_dict", [dict(T_ECHO, name="ü-task", description="中文 <>&")], {}))
|
||||
|
||||
# _parse_cron_field — extra branches beyond the batch-1 matrix
|
||||
for field, low, high in [("*/15", 0, 59), ("5/15", 0, 59), ("10-5", 0, 59),
|
||||
("1-10/2,30", 0, 59), ("*", 1, 12),
|
||||
("2,4-6/2", 1, 12), ("0", 0, 23), ("9-17", 0, 23),
|
||||
("*/5", 0, 23)]:
|
||||
C.append(("_ce_parse", [field, low, high], {}))
|
||||
|
||||
# _should_run — match/mismatch/dedup/field-count/weekday-7
|
||||
S = lambda t, **kw: (_state({"t-echo": t}, **kw), "t-echo")
|
||||
C.append(("_ce_should_run", list(S(T_ECHO)) + [2026, 10, 13, 9, 20], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(_task("t-echo", "30 12 * * *", "echo x"))) + [2026, 10, 13, 9, 20], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(T_ECHO, _last_run_minute={"t-echo": "2026-10-13 09:20"}))
|
||||
+ [2026, 10, 13, 9, 20], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(_task("t-echo", "* * * *", "echo x"))) + [2026, 10, 13, 9, 20], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(_task("t-echo", "* * * * 0", "echo x"))) + [2026, 10, 11, 0, 0], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(_task("t-echo", "* * * * 7", "echo x"))) + [2026, 10, 11, 0, 0], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(_task("t-echo", "* * * * 1", "echo x"))) + [2026, 10, 5, 0, 0], {}))
|
||||
C.append(("_ce_should_run",
|
||||
list(S(_task("t-echo", "* * * * 1", "echo x"))) + [2026, 10, 11, 0, 0], {}))
|
||||
|
||||
# run_task — every envelope branch
|
||||
C.append(("_ce_run_task", [_state({"t-echo": T_ECHO}), "t-echo"], {}))
|
||||
C.append(("_ce_run_task", [_state({"t-echo": T_ECHO}), "ghost"], {}))
|
||||
C.append(("_ce_run_task",
|
||||
[_state({"t-nope": _task("t-nope", "* * * * *", "nope-cmd-xyz")}), "t-nope"], {}))
|
||||
C.append(("_ce_run_task",
|
||||
[_state({"t-q": _task("t-q", "* * * * *", "echo 'unterminated")}), "t-q"], {}))
|
||||
C.append(("_ce_run_task",
|
||||
[_state({"t-false": _task("t-false", "* * * * *", "false")}), "t-false"], {}))
|
||||
C.append(("_ce_run_task",
|
||||
[_state({"t-sh": _task("t-sh", "* * * * *", "sh -c 'echo out; echo err 1>&2'")}),
|
||||
"t-sh"], {}))
|
||||
C.append(("_ce_run_task",
|
||||
[_state({"t-esc": _task("t-esc", "* * * * *", "echo a\\ b")}), "t-esc"], {}))
|
||||
|
||||
# fire_on_task_complete — order, disabled, non-on_complete, failures
|
||||
C.append(("_ce_fire", [_state({
|
||||
"oc-first": _task("oc-first", "on_complete", "echo one"),
|
||||
"t-sched": _task("t-sched", "5 4 * * *", "echo sched"),
|
||||
"oc-second": _task("oc-second", "on_complete", "echo two"),
|
||||
"oc-off": _task("oc-off", "on_complete", "echo off", enabled=False),
|
||||
})], {}))
|
||||
C.append(("_ce_fire", [_state({
|
||||
"oc-bad": _task("oc-bad", "on_complete", "nope-cmd-xyz"),
|
||||
})], {}))
|
||||
|
||||
# stop (thread=None paths)
|
||||
C.append(("_ce_stop", [_state({"t-echo": T_ECHO}, running=True)], {}))
|
||||
C.append(("_ce_stop", [_state(running=False)], {}))
|
||||
|
||||
# _execute_task — success + failure-to-log paths
|
||||
C.append(("_ce_execute", [_state({"t-echo": T_ECHO}), "t-echo"], {}))
|
||||
C.append(("_ce_execute",
|
||||
[_state({"t-nope": _task("t-nope", "* * * * *", "nope-cmd-xyz")}), "t-nope"], {}))
|
||||
|
||||
# _run_loop — stopped / fires / silent / dedup-guard
|
||||
C.append(("_ce_run_loop", [_state({"t-echo": T_ECHO}, running=False)], {}))
|
||||
C.append(("_ce_run_loop", [_state({"t-echo": T_ECHO}, running=True)], {}))
|
||||
C.append(("_ce_run_loop",
|
||||
[_state({"t-sched": _task("t-sched", "5 4 * * *", "echo sched")}, running=True)], {}))
|
||||
C.append(("_ce_run_loop",
|
||||
[_state({"t-echo": T_ECHO}, running=True,
|
||||
_last_run_minute={"t-echo": "2026-10-13 09:20"})], {}))
|
||||
|
||||
# _log_execution / _log_error — fresh, append, rotation (both sides
|
||||
# of the 5MB threshold)
|
||||
ROT = 5 * 1024 * 1024
|
||||
C.append(("_ce_log_execution", [_state({"t-echo": T_ECHO}), "t-echo", 0], {}))
|
||||
C.append(("_ce_log_execution",
|
||||
[_state({"t-echo": T_ECHO}, seed_log={"cron.log": 12}), "t-echo", 3], {}))
|
||||
C.append(("_ce_log_execution",
|
||||
[_state({"t-echo": T_ECHO},
|
||||
seed_log={"cron.log": ROT + 1, "cron.log.1": 100, "cron.log.2": 7}),
|
||||
"t-echo", 0], {}))
|
||||
C.append(("_ce_log_execution",
|
||||
[_state({"t-echo": T_ECHO}, seed_log={"cron.log": ROT}), "t-echo", 0], {}))
|
||||
C.append(("_ce_log_error",
|
||||
[_state({"t-nope": _task("t-nope", "* * * * *", "nope-cmd-xyz")}),
|
||||
"t-nope", "[Errno 2] No such file or directory: 'nope-cmd-xyz'"], {}))
|
||||
|
||||
# singleton family (module-level fns)
|
||||
C.append(("_ce_singleton", ["get", "", "", ""], {}))
|
||||
C.append(("_ce_singleton", ["start_stop", "", "", ""], {}))
|
||||
C.append(("_ce_singleton", ["module_run", "m1", "*/5 * * * *", "echo via-global"], {}))
|
||||
C.append(("_ce_singleton", ["module_list", "n1", "on_complete", "echo one"], {}))
|
||||
|
||||
return C
|
||||
|
||||
|
||||
SHIMS = "gen_cron_engine_gap_truth_shims"
|
||||
|
||||
|
||||
def main():
|
||||
sys.modules[SHIMS] = sys.modules["__main__"]
|
||||
truth = freeze(SHIMS, cases(), oracle_module=SHIMS)
|
||||
target = REPO / "src" / "go" / "cron-engine" / "truth_gap.json"
|
||||
write_envelope(FROZEN_NOW, truth, target)
|
||||
print(f"wrote {target} ({len(truth)} cases)")
|
||||
|
||||
|
||||
def verify(path):
|
||||
raw = json.loads(path.read_text())
|
||||
frozen = raw["cases"] if isinstance(raw, dict) else raw
|
||||
if isinstance(raw, dict) and raw.get("frozen_now"):
|
||||
os.environ["RUNE_FROZEN_NOW"] = raw["frozen_now"]
|
||||
sys.modules[SHIMS] = sys.modules["__main__"]
|
||||
recased = [{"fn": c["fn"], "args": c.get("args", []), "kwargs": c.get("kwargs", {})}
|
||||
for c in frozen]
|
||||
fresh = freeze(SHIMS, recased, oracle_module=SHIMS)
|
||||
if json.dumps(fresh, sort_keys=True) != json.dumps(frozen, sort_keys=True):
|
||||
print(f"MISMATCH: regenerated truth differs from {path}")
|
||||
for f, g in zip(frozen, fresh):
|
||||
if json.dumps(f, sort_keys=True) != json.dumps(g, sort_keys=True):
|
||||
print(f" first divergence at fn={f['fn']}")
|
||||
print(f" frozen: {json.dumps(f['expected'])[:300]}")
|
||||
print(f" oracle: {json.dumps(g['expected'])[:300]}")
|
||||
break
|
||||
return 1
|
||||
print(f"OK: {len(frozen)} cases byte-identical against live oracle")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--verify":
|
||||
sys.exit(verify(REPO / "src" / "go" / "cron-engine" / "truth_gap.json"))
|
||||
main()
|
||||
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_microkernel_gap_truth.py — microkernel gap-closing truth battery
|
||||
(dichotomy wave #2479, worktree rune-wave-cq).
|
||||
|
||||
Closes the PARTIAL audit gaps: now_iso, discover, _extract_manifest,
|
||||
_load_registry, _save_registry, _run_hook (fire's hook runner).
|
||||
|
||||
Uses the SHARED harness (scripts/freeze_truth.py): RUNE core pinned to
|
||||
python, deep-copied inputs, envelope with its own frozen_now:
|
||||
|
||||
PYTHONPATH=src python3 scripts/freeze_truth.py \
|
||||
--verify src/go/microkernel/truth_microkernel_gap.json \
|
||||
--module rune.microkernel
|
||||
|
||||
STATEFULNESS / DETERMINISM LAWS:
|
||||
* _extract_manifest takes an fpath — the shim carries the file CONTENT
|
||||
in args, writes a temp file, calls the real oracle fn, returns the
|
||||
parsed manifest (paths never enter the result).
|
||||
* discover iterates os.listdir (filesystem order) and stamps absolute
|
||||
_file paths — the shim installs a fixture dir, pins os.listdir to the
|
||||
EXACT frozen filename order (sorted — matches the Go port's
|
||||
os.ReadDir), calls the real discover(), and normalizes every path to
|
||||
<dir>/<fname> stubs. Machine-independent, replay-stable.
|
||||
* _run_hook is a closure inside fire() — it is exercised through the
|
||||
real fire() with ONE listener per branch class (fixture scripts are
|
||||
in-file constants: deterministic stdout/stderr). wall-clock durations
|
||||
(duration_ms / total_duration_ms) are normalized to "<ms>" — the
|
||||
no-wall-clock law.
|
||||
* _load_registry/_save_registry: missing file, corrupt file, full-shape
|
||||
file (verbatim load), and a save->load roundtrip of the default shape.
|
||||
|
||||
Usage: python3 scripts/gen_microkernel_gap_truth.py (from repo root)
|
||||
python3 scripts/gen_microkernel_gap_truth.py --verify
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime as _dt, timezone as _tz
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
from freeze_truth import freeze, write_envelope # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T11:05:00"
|
||||
|
||||
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW
|
||||
os.environ["RUNE_MICROKERNEL_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
import microkernel as mk # noqa: E402
|
||||
|
||||
SHIM = "gen_microkernel_gap_truth_shims"
|
||||
|
||||
|
||||
def _pin_mk_clock():
|
||||
"""microkernel's fire() mixes datetime.now() (naive) and
|
||||
datetime.now(timezone.utc) (aware) — the harness clock is naive-only,
|
||||
so shims that reach register()/fire() pin an AWARE frozen clock.
|
||||
Durations collapse to 0.0 (normalized to <ms> by the rig regardless)."""
|
||||
base = _dt.fromisoformat(FROZEN_NOW).replace(tzinfo=_tz.utc)
|
||||
|
||||
class _Frozen(_dt):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
if tz is not None:
|
||||
return base.replace(tzinfo=tz)
|
||||
return base
|
||||
|
||||
old = mk.datetime
|
||||
mk.datetime = _Frozen
|
||||
return old
|
||||
|
||||
# ── fixture hook scripts (deterministic output; stdlib only) ──────────────
|
||||
|
||||
OK_JSON = (
|
||||
"import json, sys\n"
|
||||
"data = sys.argv[sys.argv.index('--data') + 1]\n"
|
||||
"print(json.dumps({'echo': json.loads(data)}))\n"
|
||||
)
|
||||
RAW_LONG = (
|
||||
"print('h\\u00e9llo-\\u2192ok ' * 120)\n" # 1200 codepoints, non-JSON
|
||||
)
|
||||
BOOM = (
|
||||
"import sys\n"
|
||||
"sys.stderr.write('\\u00e9rr\\u00f4r-' * 100)\n" # 600 codepoints
|
||||
"sys.exit(3)\n"
|
||||
)
|
||||
SLOW = "import time\ntime.sleep(5)\n"
|
||||
|
||||
|
||||
def _norm_fire(result):
|
||||
"""Normalize wall-clock fields + as_completed order (the rig law)."""
|
||||
out = copy.deepcopy(result)
|
||||
if "total_duration_ms" in out:
|
||||
out["total_duration_ms"] = "<ms>"
|
||||
for r in out.get("results", []):
|
||||
if "duration_ms" in r:
|
||||
r["duration_ms"] = "<ms>"
|
||||
if isinstance(r.get("error"), str):
|
||||
r["error"] = r["error"]
|
||||
out["results"] = sorted(out.get("results", []), key=lambda r: r.get("name", ""))
|
||||
return out
|
||||
|
||||
|
||||
def _norm_log(log):
|
||||
out = []
|
||||
for e in log:
|
||||
e = dict(e)
|
||||
e["duration_ms"] = "<ms>"
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
|
||||
def _registry_projection(reg, dir_token):
|
||||
plugins = {}
|
||||
for name, p in reg["plugins"].items():
|
||||
plugins[name] = {
|
||||
"active": p.get("active"),
|
||||
"registered_at": p.get("registered_at"),
|
||||
"file": p.get("file", "").replace(dir_token, "<dir>"),
|
||||
}
|
||||
return {
|
||||
"plugins": plugins,
|
||||
"capabilities": reg.get("capabilities", {}),
|
||||
"hooks": reg.get("hooks", {}),
|
||||
}
|
||||
|
||||
|
||||
# ── wrapper shims: module-level names the harness can call ────────────────
|
||||
|
||||
|
||||
def _mk_extract(content):
|
||||
fd, fpath = tempfile.mkstemp(suffix=".py", prefix="mk_gap_")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(content)
|
||||
return {"result": copy.deepcopy(mk._extract_manifest(fpath))}
|
||||
finally:
|
||||
os.unlink(fpath)
|
||||
|
||||
|
||||
def _mk_discover(filenames, contents, now):
|
||||
"""Fixture-driven discover: filenames (EXACT listing order — pass them
|
||||
sorted, matching the Go port's os.ReadDir), contents {fname: source}."""
|
||||
work = tempfile.mkdtemp(prefix="mk_disc_")
|
||||
fixture = Path(work) / "scripts"
|
||||
fixture.mkdir()
|
||||
for fname, src in contents.items():
|
||||
(fixture / fname).write_text(src)
|
||||
tmpdb = os.path.join(work, "registry.json")
|
||||
|
||||
old_dir, old_db = mk.SCRIPTS_DIR, mk.REGISTRY_DB
|
||||
real_listdir = os.listdir
|
||||
old_clock = _pin_mk_clock()
|
||||
mk.SCRIPTS_DIR = str(fixture)
|
||||
mk.REGISTRY_DB = tmpdb
|
||||
os.listdir = lambda p: list(filenames) if str(p) == str(fixture) else real_listdir(p)
|
||||
try:
|
||||
result = copy.deepcopy(mk.discover())
|
||||
state = _registry_projection(mk._load_registry(), str(fixture))
|
||||
finally:
|
||||
os.listdir = real_listdir
|
||||
mk.SCRIPTS_DIR, mk.REGISTRY_DB = old_dir, old_db
|
||||
mk.datetime = old_clock
|
||||
return {"result": result, "registry": state}
|
||||
|
||||
|
||||
def _mk_load(content):
|
||||
"""_load_registry over a fixture registry file (None = missing file).
|
||||
Projection: the port stores TYPED manifests, so the frozen contract is
|
||||
the round-tripped shape (active/registered_at/file + indexes + log),
|
||||
not the verbatim manifest dict."""
|
||||
work = tempfile.mkdtemp(prefix="mk_load_")
|
||||
db = os.path.join(work, "registry.json")
|
||||
old_db = mk.REGISTRY_DB
|
||||
mk.REGISTRY_DB = db
|
||||
try:
|
||||
if content is not None:
|
||||
with open(db, "w") as f:
|
||||
f.write(content)
|
||||
elif os.path.exists(db):
|
||||
os.unlink(db)
|
||||
loaded = mk._load_registry()
|
||||
return {"result": _registry_projection(loaded, "<none>"), "log": loaded.get("log", [])}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
|
||||
|
||||
def _mk_save_load_default():
|
||||
"""_save_registry -> _load_registry roundtrip of the default shape."""
|
||||
work = tempfile.mkdtemp(prefix="mk_rt_")
|
||||
db = os.path.join(work, "registry.json")
|
||||
old_db = mk.REGISTRY_DB
|
||||
mk.REGISTRY_DB = db
|
||||
try:
|
||||
loaded = mk._load_registry()
|
||||
mk._save_registry(loaded)
|
||||
again = mk._load_registry()
|
||||
return {"first": copy.deepcopy(loaded), "again": copy.deepcopy(again),
|
||||
"stable": loaded == again}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
|
||||
|
||||
def _mk_fire(script, manifest, event, data, timeout):
|
||||
"""fire() with ONE registered listener whose _file is the fixture
|
||||
script (or a ghost/empty path for the not-found class). Exercises the
|
||||
oracle's _run_hook closure branch selected by the fixture."""
|
||||
work = tempfile.mkdtemp(prefix="mk_fire_")
|
||||
if script is not None:
|
||||
fpath = os.path.join(work, "plugin.py")
|
||||
with open(fpath, "w") as f:
|
||||
f.write(script)
|
||||
manifest = dict(manifest)
|
||||
manifest["_file"] = fpath
|
||||
else:
|
||||
manifest = dict(manifest) # _file stays whatever the caller set
|
||||
tmpdb = os.path.join(work, "registry.json")
|
||||
|
||||
old_db = mk.REGISTRY_DB
|
||||
old_clock = _pin_mk_clock()
|
||||
mk.REGISTRY_DB = tmpdb
|
||||
try:
|
||||
mk.register(manifest)
|
||||
result = mk.fire(event, data=data, timeout=timeout)
|
||||
state = mk._load_registry()
|
||||
return {"result": _norm_fire(result), "log": _norm_log(state.get("log", []))}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
mk.datetime = old_clock
|
||||
|
||||
|
||||
def _mk_fire_two(scripts, manifests, event, data, timeout):
|
||||
"""fire() with TWO listeners (parallelism + log entry)."""
|
||||
work = tempfile.mkdtemp(prefix="mk_fire2_")
|
||||
tmpdb = os.path.join(work, "registry.json")
|
||||
old_db = mk.REGISTRY_DB
|
||||
old_clock = _pin_mk_clock()
|
||||
mk.REGISTRY_DB = tmpdb
|
||||
try:
|
||||
for script, manifest in zip(scripts, manifests):
|
||||
manifest = dict(manifest)
|
||||
if script is not None:
|
||||
fpath = os.path.join(work, manifest["name"] + ".py")
|
||||
with open(fpath, "w") as f:
|
||||
f.write(script)
|
||||
manifest["_file"] = fpath
|
||||
mk.register(manifest)
|
||||
result = mk.fire(event, data=data, timeout=timeout)
|
||||
state = mk._load_registry()
|
||||
return {"result": _norm_fire(result), "log": _norm_log(state.get("log", []))}
|
||||
finally:
|
||||
mk.REGISTRY_DB = old_db
|
||||
mk.datetime = old_clock
|
||||
|
||||
|
||||
CACHEY = (
|
||||
'PLUGIN = {\n'
|
||||
' "name": "cache",\n'
|
||||
' "version": "0.1.0",\n'
|
||||
' "description": "Cache system — event-driven invalidation.",\n'
|
||||
' "capabilities": ["cache", "invalidation"],\n'
|
||||
' "hooks": {\n'
|
||||
' "on_session_end": "handle_hook",\n'
|
||||
' "on_memory_add": "handle_hook",\n'
|
||||
' },\n'
|
||||
' "dependencies": [],\n'
|
||||
' "priority": 1,\n'
|
||||
' "auto_activate": True,\n'
|
||||
'}\n'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
sys.modules[SHIM] = sys.modules["__main__"]
|
||||
cases = []
|
||||
|
||||
# ── now_iso (pinned clock: RUNE_FROZEN_NOW → datetime frozen) ──
|
||||
cases.append(("now_iso", [], {}))
|
||||
|
||||
# ── _extract_manifest branches ────────────────────────────────
|
||||
cases.append((f"{SHIM}._mk_extract", [CACHEY], {}))
|
||||
cases.append((f"{SHIM}._mk_extract", ["X = 1\n"], {}))
|
||||
cases.append((f"{SHIM}._mk_extract", ["PLUGIN = {}\n"], {}))
|
||||
cases.append((f"{SHIM}._mk_extract", ["PLUGIN = {}\nX = 1\n"], {}))
|
||||
cases.append((f"{SHIM}._mk_extract", ['PLUGIN = {\n "name": "x",\n'], {}))
|
||||
cases.append((f"{SHIM}._mk_extract",
|
||||
["PLUGIN = {'name': 'café', 'n': 1.5, 'neg': -3, 'big': 1e3}\n"], {}))
|
||||
cases.append((f"{SHIM}._mk_extract",
|
||||
['PLUGIN = {\n # comment inside\n "name": "x",\n}\n'], {}))
|
||||
cases.append((f"{SHIM}._mk_extract",
|
||||
['PLUGIN = {\n "name": "x",\n "hooks": {"a": True, "b": False},\n "nested": {"deep": [1, 2, {"k": "v"}]},\n}\n'], {}))
|
||||
|
||||
# ── discover (fixture dir, pinned listing order = sorted) ─────
|
||||
mixed_files = ["_private.py", "a_cache.py", "b_noname.py", "c_plain.py",
|
||||
"d_falsy.py", "microkernel.py", "notes.txt"]
|
||||
mixed_contents = {
|
||||
"a_cache.py": CACHEY,
|
||||
"b_noname.py": 'PLUGIN = {\n "version": "0.1.0",\n}\n',
|
||||
"c_plain.py": "def run():\n pass\n",
|
||||
"d_falsy.py": ('PLUGIN = {\n "name": "d",\n "capabilities": ["d"],\n'
|
||||
' "hooks": {"on_x": False, "on_y": "", "on_z": 0},\n}\n'),
|
||||
"_private.py": CACHEY,
|
||||
"microkernel.py": CACHEY,
|
||||
"notes.txt": "not a plugin",
|
||||
}
|
||||
cases.append((f"{SHIM}._mk_discover", [mixed_files, mixed_contents, FROZEN_NOW], {}))
|
||||
cases.append((f"{SHIM}._mk_discover", [[], {}, FROZEN_NOW], {}))
|
||||
shared_files = ["s1.py", "s2.py"]
|
||||
shared_contents = {
|
||||
"s1.py": 'PLUGIN = {\n "name": "s1",\n "capabilities": ["shared"],\n "priority": 5,\n}\n',
|
||||
"s2.py": 'PLUGIN = {\n "name": "s2",\n "capabilities": ["shared"],\n "priority": 10,\n}\n',
|
||||
}
|
||||
cases.append((f"{SHIM}._mk_discover", [shared_files, shared_contents, FROZEN_NOW], {}))
|
||||
|
||||
# ── _load_registry / _save_registry ───────────────────────────
|
||||
full_registry = json.dumps({
|
||||
"plugins": {"p": {"manifest": {"name": "p", "priority": 1},
|
||||
"registered_at": "2026-10-13T11:05:00+00:00",
|
||||
"active": True, "file": "/x/p.py"}},
|
||||
"capabilities": {"cap": ["p"]},
|
||||
"hooks": {"evt": ["p"]},
|
||||
"log": [{"event": "evt", "timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1, "duration_ms": 12.5}],
|
||||
}, indent=2)
|
||||
cases.append((f"{SHIM}._mk_load", [None], {}))
|
||||
cases.append((f"{SHIM}._mk_load", ["{oops"], {}))
|
||||
cases.append((f"{SHIM}._mk_load", [full_registry], {}))
|
||||
cases.append((f"{SHIM}._mk_save_load_default", [], {}))
|
||||
|
||||
# ── _run_hook (through fire, one branch class per case) ───────
|
||||
evt = "on_session_end"
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[OK_JSON, {"name": "okp", "hooks": {evt: True}},
|
||||
evt, {"k": "v", "n": 2}, 30], {}))
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[RAW_LONG, {"name": "rawp", "hooks": {evt: True}}, evt, None, 30], {}))
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[BOOM, {"name": "boomp", "hooks": {evt: True}}, evt, None, 30], {}))
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[SLOW, {"name": "slowp", "hooks": {evt: True}}, evt, None, 1], {}))
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[OK_JSON, {"name": "offp", "hooks": {evt: True}, "auto_activate": False},
|
||||
evt, None, 30], {}))
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[None, {"name": "ghostp", "hooks": {evt: True}, "_file": "/nonexistent/ghost.py"},
|
||||
evt, None, 30], {}))
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[None, {"name": "nofilep", "hooks": {evt: True}}, evt, None, 30], {}))
|
||||
# fire with NO listeners: early return, no log entry
|
||||
cases.append((f"{SHIM}._mk_fire",
|
||||
[OK_JSON, {"name": "lonely", "capabilities": ["x"]}, "no_such_event", None, 30], {}))
|
||||
# two listeners in parallel: sorted results + one log entry
|
||||
cases.append((f"{SHIM}._mk_fire_two",
|
||||
[[OK_JSON, OK_JSON],
|
||||
[{"name": "zz", "hooks": {evt: True}},
|
||||
{"name": "aa", "hooks": {evt: True}}],
|
||||
evt, {"k": 1}, 30], {}))
|
||||
|
||||
# harness resolves dotted names against ONE module — freeze per case
|
||||
# against the fn's OWN module (same law as gen_hook_engine_gap_truth).
|
||||
truth = []
|
||||
for fn, args, kwargs in cases:
|
||||
if fn.startswith(f"{SHIM}."):
|
||||
mod, short = SHIM, fn.split(".")[-1]
|
||||
else:
|
||||
mod, short = "microkernel", fn
|
||||
rows = freeze(mod, [(short, args, kwargs)], oracle_module=mod)
|
||||
rows[0]["fn"] = fn
|
||||
rows[0]["oracle"] = mod
|
||||
truth.append(rows[0])
|
||||
|
||||
target = REPO / "src" / "go" / "microkernel" / "truth_microkernel_gap.json"
|
||||
write_envelope(FROZEN_NOW, truth, target)
|
||||
print(f"wrote {target} ({len(truth)} cases)")
|
||||
|
||||
|
||||
def verify(path):
|
||||
"""Per-case-module verify (same byte-compare law as the harness)."""
|
||||
raw = json.loads(path.read_text())
|
||||
frozen = raw["cases"] if isinstance(raw, dict) else raw
|
||||
if isinstance(raw, dict) and raw.get("frozen_now"):
|
||||
os.environ["RUNE_FROZEN_NOW"] = raw["frozen_now"]
|
||||
sys.modules.setdefault(SHIM, sys.modules["__main__"])
|
||||
fresh = []
|
||||
for c in frozen:
|
||||
mod = c.get("oracle", "microkernel")
|
||||
short = c["fn"].split(".")[-1]
|
||||
rows = freeze(mod, [(short, c.get("args", []), c.get("kwargs", {}))],
|
||||
oracle_module=mod)
|
||||
rows[0]["fn"] = c["fn"]
|
||||
rows[0]["oracle"] = mod
|
||||
fresh.append(rows[0])
|
||||
if json.dumps(fresh, sort_keys=True) != json.dumps(frozen, sort_keys=True):
|
||||
print(f"MISMATCH: regenerated truth differs from {path}")
|
||||
for f, g in zip(frozen, fresh):
|
||||
if json.dumps(f, sort_keys=True) != json.dumps(g, sort_keys=True):
|
||||
print(f" first divergence at fn={f['fn']}")
|
||||
print(f" frozen: {json.dumps(f['expected'])[:300]}")
|
||||
print(f" oracle: {json.dumps(g['expected'])[:300]}")
|
||||
break
|
||||
return 1
|
||||
print(f"OK: {len(frozen)} cases byte-identical against live oracles")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--verify":
|
||||
sys.exit(verify(REPO / "src" / "go" / "microkernel" / "truth_microkernel_gap.json"))
|
||||
main()
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_model_profiles_truth.py — freeze model_profiles oracle truth via
|
||||
the SHARED harness (scripts/freeze_truth.py, freeze()) through the
|
||||
scripts/mp_freeze_seams rig (auto-imported sitecustomize: HOME pinned
|
||||
to MP_FREEZE_HOME before the oracle import, per-case hermetic store
|
||||
wipe+seed, canned Ollama server, JSON-safe wrapper seams).
|
||||
|
||||
Laws honoured:
|
||||
* hermetic: PROFILES_DIR is expanduser-derived AT IMPORT — the rig
|
||||
pins HOME to the fixed /tmp/mp_gap_freeze_home root (the CI_FREEZE_HOME
|
||||
precedent: frozen absolute paths stay byte-identical across processes).
|
||||
* no wall-clock: created/last_used come from the harness's frozen
|
||||
clock (write_envelope carries RUNE_FROZEN_NOW inside the file).
|
||||
* no network: scan_all_models runs against a canned in-process server
|
||||
(or a refused port for the degrade branch); OLLAMA_URL is repointed
|
||||
per case at call time.
|
||||
* self-contained rows: every stateful seam wipes + re-seeds the store
|
||||
from its args, so --verify replays byte-identically in any order.
|
||||
* byte-parity corpus: save_profile rows carry the RAW file text — the
|
||||
Go port's Python-shaped JSON writer must reproduce it exactly
|
||||
(asdict field order, indent=2 separators, float rendering, ASCII
|
||||
escaping). Seeds keep integral floats in float form (0.0) — the
|
||||
integral-float law; int-valued size_gb/optimal_temperature seeds
|
||||
render differently (Python keeps the int) and stay out of the corpus.
|
||||
|
||||
Regenerate + re-verify (byte-identical law — two runs, then verify):
|
||||
PYTHONPATH=scripts/mp_freeze_seams:src python3 scripts/gen_model_profiles_truth.py
|
||||
PYTHONPATH=scripts/mp_freeze_seams:src python3 scripts/freeze_truth.py \
|
||||
--verify src/go/model-profiles/frozen_mp_truth.json \
|
||||
--module rune.model_profiles
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from freeze_truth import freeze, write_envelope # noqa: E402 (shared harness)
|
||||
|
||||
FROZEN_NOW = "2026-10-13T09:41:23.123456"
|
||||
OUT = REPO / "src" / "go" / "model-profiles" / "frozen_mp_truth.json"
|
||||
|
||||
FULL_STATE = {
|
||||
"name": "qwen2.5:7b",
|
||||
"display_name": "Qwen2.5 7B ✓",
|
||||
"family": "qwen",
|
||||
"parameters": "7B",
|
||||
"quantization": "Q4_K_M",
|
||||
"size_gb": 4.7,
|
||||
"strengths": ["reasoning", "coding", "instruction-following", "multilingual"],
|
||||
"weaknesses": ["verbose thinking"],
|
||||
"prompting_tips": [
|
||||
"Be direct", "Use structure", "Specify the language", "Ask for concise",
|
||||
],
|
||||
"safety_tier": 2,
|
||||
"known_refusal_topics": ["weapons", "malware"],
|
||||
"thinking_format": "<think>",
|
||||
"response_style": "concise",
|
||||
"context_window": 32768,
|
||||
"optimal_temperature": 0.7,
|
||||
"interactions": 10,
|
||||
"successes": 7,
|
||||
"failures": 3,
|
||||
"last_used": "2026-10-12T00:00:00+00:00",
|
||||
"created": "2026-10-01T00:00:00+00:00",
|
||||
}
|
||||
|
||||
QWEN_FILE = "qwen2.5_7b.json"
|
||||
|
||||
SCAN_FRESH = {"models": [
|
||||
{"name": "qwen2.5:7b", "size": 7516192768},
|
||||
{"name": "banker-tie-lo:2b", "size": 2415919104}, # 2.25 GB → 2.2 (ties-to-even)
|
||||
{"name": "banker-tie-hi:2b", "size": 2952790016}, # 2.75 GB → 2.8 (ties-to-even)
|
||||
{"size": 123}, # no name → skipped
|
||||
{"name": "noninteger:5b", "size": 5000000000}, # 4.656… → 4.7
|
||||
]}
|
||||
|
||||
SCAN_EXISTING = {"models": [
|
||||
{"name": "qwen2.5:7b", "size": 7516192768},
|
||||
{"name": "llama3.1:8b-q4_K_M", "size": 4831838208}, # 4.5 GB
|
||||
]}
|
||||
|
||||
CASES = [
|
||||
# ── get_profile_path: filesystem sanitization ────────────────────
|
||||
("seam_get_profile_path", ("qwen2.5:7b",), {}),
|
||||
("seam_get_profile_path", ("library/qwen3:14b",), {}),
|
||||
("seam_get_profile_path", ("a?b:c/d",), {}),
|
||||
("seam_get_profile_path", ("plain-model",), {}),
|
||||
|
||||
# ── load_profile: disk → dataclass, suppressed-exception floor ───
|
||||
("seam_load_profile", ("qwen2.5:7b", {QWEN_FILE: FULL_STATE}), {}),
|
||||
("seam_load_profile", ("qwen2.5:7b", None), {}), # no file → None
|
||||
("seam_load_profile", ("qwen2.5:7b", {"other_7b.json": FULL_STATE}), {}), # wrong file → None
|
||||
("seam_load_profile", ("qwen2.5:7b", {QWEN_FILE: "{not json"}), {}), # corrupt → None
|
||||
("seam_load_profile", ("qwen2.5:7b", {QWEN_FILE: { # unknown keys filtered
|
||||
"name": "qwen2.5:7b", "bogus": 1, "family": "qwen"}}), {}),
|
||||
("seam_load_profile", ("qwen2.5:7b", {QWEN_FILE: { # missing optionals → defaults
|
||||
"name": "qwen2.5:7b", "interactions": 5}}), {}),
|
||||
("seam_load_profile", ("qwen2.5:7b", {QWEN_FILE: {"family": "qwen"}}), {}), # missing name → TypeError suppressed
|
||||
|
||||
# ── save_profile: BYTE-PARITY corpus (raw file text) ─────────────
|
||||
("seam_save_profile", (FULL_STATE,), {}),
|
||||
("seam_save_profile", ({"name": "plain-model"},), {}), # all defaults
|
||||
("seam_save_profile", ({
|
||||
"name": "deepseek-r1:32b", "family": "deepseek", "parameters": "32B",
|
||||
"size_gb": 0.0, "strengths": [], "safety_tier": 3,
|
||||
"optimal_temperature": 0.6,
|
||||
},), {}),
|
||||
|
||||
# ── build_profile_from_ollama: knowledge merge + clock + tier ────
|
||||
("seam_build_profile_from_ollama", ("qwen3:14b", {"display_name": "Qwen3 14B", "size_gb": 9.0}), {}),
|
||||
("seam_build_profile_from_ollama", ("mistral-7b", None), {}),
|
||||
("seam_build_profile_from_ollama", ("dolphin-uncensored:7b", None), {}),
|
||||
("seam_build_profile_from_ollama", ("DAVIDAU-test:14b", None), {}),
|
||||
("seam_build_profile_from_ollama", ("some-new-model", {"display_name": "X"}), {}),
|
||||
("seam_build_profile_from_ollama", ("deepseek-r1:32b", {"display_name": "DS", "size_gb": 19.2}), {}),
|
||||
|
||||
# ── scan_all_models: Ollama sweep (canned server / dead port) ────
|
||||
("seam_scan_all_models", (SCAN_FRESH,), {}),
|
||||
("seam_scan_all_models", (SCAN_EXISTING, {QWEN_FILE: FULL_STATE}), {}), # existing → learned data kept
|
||||
("seam_scan_all_models", ({"models": [{"name": "phi3:mini"}]}, None), {}), # no size → 0.0
|
||||
("seam_scan_all_models", ("dead", None), {}),
|
||||
|
||||
# ── get_or_create_profile: load-or-build-and-save ────────────────
|
||||
("seam_get_or_create_profile", ("qwen2.5:7b", {QWEN_FILE: FULL_STATE}), {}),
|
||||
("seam_get_or_create_profile", ("brand-new-model", None), {}),
|
||||
|
||||
# ── record_interaction: learning accumulator (state = the contract)
|
||||
("seam_record_interaction", ("qwen3:14b", [[True, ""]], None), {}),
|
||||
("seam_record_interaction", ("fresh-fail:1b", [[False, "refuses weapons"]], None), {}),
|
||||
("seam_record_interaction", ("dup-topic:1b", [[False, "t1"], [False, "t1"]], None), {}),
|
||||
("seam_record_interaction", ("no-topic:1b", [[False, ""]], None), {}),
|
||||
("seam_record_interaction", ("qwen2.5:7b", [[False, "politics"]], {QWEN_FILE: FULL_STATE}), {}),
|
||||
|
||||
# ── get_prompt_context: operator string assembly ─────────────────
|
||||
("seam_get_prompt_context", ("qwen2.5:7b", {QWEN_FILE: FULL_STATE}), {}),
|
||||
("seam_get_prompt_context", ("tips-only:1b", {"tips-only_1b.json": {
|
||||
"name": "tips-only:1b", "prompting_tips": ["tip one"]}}), {}),
|
||||
("seam_get_prompt_context", ("unknown-model-x", None), {}), # fresh unknown → ""
|
||||
|
||||
# ── to_dict / from_dict (dataclass surface) ──────────────────────
|
||||
("seam_to_dict", (FULL_STATE,), {}),
|
||||
("seam_from_dict", ({"name": "n", "bogus_key": 1, "context_window": 8192},), {}),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# The harness clock pin is env-driven (RUNE_FROZEN_NOW) — set it for
|
||||
# the generation run too, not just --verify (write_envelope then
|
||||
# carries the same instant for byte-identical re-freezes).
|
||||
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW
|
||||
truth = freeze("model_profiles", CASES, oracle_module="rune.model_profiles")
|
||||
write_envelope(FROZEN_NOW, truth, OUT)
|
||||
print(f"froze {len(truth)} cases -> {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,418 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_mp_battery.py — ONE-SHOT generator: emit
|
||||
src/go/model-profiles/frozen_battery_test.go with the frozen truth
|
||||
(frozen_mp_truth.json) embedded VERBATIM (never hand-transcribed) plus
|
||||
the dispatch harness. Re-run after any truth regeneration."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
TRUTH = REPO / "src" / "go" / "model-profiles" / "frozen_mp_truth.json"
|
||||
OUT = REPO / "src" / "go" / "model-profiles" / "frozen_battery_test.go"
|
||||
|
||||
HARNESS = r'''package main
|
||||
|
||||
// frozen_battery_test.go — gap-closing wave (#2479 dichotomy tranche):
|
||||
// replays the shared-harness frozen oracle truth (generated by
|
||||
// scripts/gen_model_profiles_truth.py via scripts/freeze_truth.py
|
||||
// through the scripts/mp_freeze_seams rig, RUNE_FROZEN_NOW pinned)
|
||||
// VERBATIM, in order, against the store seam repointed at a fresh temp
|
||||
// dir. The case sequence is self-seeding: replaying it from an empty
|
||||
// store regenerates every byte of expected state — exactly what
|
||||
// PYTHONPATH=scripts/mp_freeze_seams:src python3 scripts/freeze_truth.py \
|
||||
// --verify src/go/model-profiles/frozen_mp_truth.json \
|
||||
// --module rune.model_profiles
|
||||
// checks against the live oracle. (GENERATED by scripts/gen_mp_battery.py
|
||||
// — do not hand-edit the truth const.)
|
||||
//
|
||||
// DIVERGENCE (documented, not a bug — the local_tool_runtime precedent):
|
||||
// load_profile({"name": <non-string>}) is ACCEPTED by the Python oracle
|
||||
// (dataclass __init__ performs no type checks) but REJECTED by the Go
|
||||
// port (typed signature). No frozen row carries that shape.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const frozenMpTruthJSON = `__TRUTH__`
|
||||
|
||||
func decodeMPArg(t *testing.T, raw json.RawMessage, dst any) {
|
||||
t.Helper()
|
||||
var v any
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
t.Fatalf("arg decode: %v", err)
|
||||
}
|
||||
if v == nil {
|
||||
return // null → leave the zero value (None default)
|
||||
}
|
||||
if err := json.Unmarshal(raw, dst); err != nil {
|
||||
t.Fatalf("arg re-decode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func profileToMap(p ModelProfile) map[string]any {
|
||||
raw, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func seedStore(t *testing.T, seed map[string]json.RawMessage) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(profilesDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, raw := range seed {
|
||||
var v any
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
t.Fatalf("seed decode: %v", err)
|
||||
}
|
||||
var data []byte
|
||||
if s, ok := v.(string); ok {
|
||||
data = []byte(s) // raw-text seed (corrupt-JSON floor)
|
||||
} else {
|
||||
data, _ = json.Marshal(v)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(profilesDir, name), data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func storeFiles() map[string]any {
|
||||
out := map[string]any{}
|
||||
entries, err := os.ReadDir(profilesDir)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(profilesDir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
continue
|
||||
}
|
||||
out[e.Name()] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dispatchFrozen — one frozen case against the ported surface. Stateful
|
||||
// ops mirror the rig seams: wipe the store, seed from the args, run,
|
||||
// return the JSON-native projection.
|
||||
func dispatchFrozen(t *testing.T, fn string, args []json.RawMessage) any {
|
||||
t.Helper()
|
||||
arg := func(i int, dst any) { decodeMPArg(t, args[i], dst) }
|
||||
argOpt := func(i int, dst any) {
|
||||
if i < len(args) {
|
||||
decodeMPArg(t, args[i], dst)
|
||||
}
|
||||
}
|
||||
switch fn {
|
||||
case "seam_get_profile_path":
|
||||
var name string
|
||||
arg(0, &name)
|
||||
got := getProfilePath(name)
|
||||
rel, err := filepath.Rel(profilesDir, got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The frozen expectation carries the rig's absolute path; the
|
||||
// contract is the sanitized FILE NAME (store root is the seam).
|
||||
return filepath.Join("$STORE", rel)
|
||||
case "seam_load_profile":
|
||||
var name string
|
||||
arg(0, &name)
|
||||
var seed map[string]json.RawMessage
|
||||
argOpt(1, &seed)
|
||||
os.RemoveAll(profilesDir)
|
||||
if seed != nil {
|
||||
seedStore(t, seed)
|
||||
}
|
||||
if p, ok := loadProfile(name); ok {
|
||||
return profileToMap(*p)
|
||||
}
|
||||
return nil
|
||||
case "seam_save_profile":
|
||||
var state map[string]json.RawMessage
|
||||
arg(0, &state)
|
||||
p := profileFromState(t, state)
|
||||
os.RemoveAll(profilesDir)
|
||||
saveProfile(p)
|
||||
raw, err := os.ReadFile(getProfilePath(p.Name))
|
||||
if err != nil {
|
||||
t.Fatalf("save wrote nothing: %v", err)
|
||||
}
|
||||
return map[string]any{"raw": string(raw), "parsed": storeFiles()}
|
||||
case "seam_build_profile_from_ollama":
|
||||
var name string
|
||||
arg(0, &name)
|
||||
var info map[string]any
|
||||
arg(1, &info)
|
||||
return profileToMap(buildProfileFromOllama(name, info))
|
||||
case "seam_scan_all_models":
|
||||
var fixture any
|
||||
arg(0, &fixture)
|
||||
var seed map[string]json.RawMessage
|
||||
argOpt(1, &seed)
|
||||
os.RemoveAll(profilesDir)
|
||||
if seed != nil {
|
||||
seedStore(t, seed)
|
||||
}
|
||||
saved := ollamaURL
|
||||
var srv *httptest.Server
|
||||
if s, ok := fixture.(string); ok && s == "dead" {
|
||||
ollamaURL = "http://127.0.0.1:1"
|
||||
} else {
|
||||
payload, _ := json.Marshal(fixture)
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(payload)
|
||||
}))
|
||||
ollamaURL = srv.URL
|
||||
}
|
||||
profiles := scanAllModels()
|
||||
ollamaURL = saved
|
||||
if srv != nil {
|
||||
srv.Close()
|
||||
}
|
||||
out := make([]any, len(profiles))
|
||||
for i, p := range profiles {
|
||||
out[i] = profileToMap(p)
|
||||
}
|
||||
return map[string]any{"profiles": out, "files": storeFiles()}
|
||||
case "seam_get_or_create_profile":
|
||||
var name string
|
||||
arg(0, &name)
|
||||
var seed map[string]json.RawMessage
|
||||
argOpt(1, &seed)
|
||||
os.RemoveAll(profilesDir)
|
||||
if seed != nil {
|
||||
seedStore(t, seed)
|
||||
}
|
||||
p := getOrCreateProfile(name)
|
||||
return map[string]any{"profile": profileToMap(p), "files": storeFiles()}
|
||||
case "seam_record_interaction":
|
||||
var name string
|
||||
arg(0, &name)
|
||||
var outcomes [][]any
|
||||
arg(1, &outcomes)
|
||||
var seed map[string]json.RawMessage
|
||||
argOpt(2, &seed)
|
||||
os.RemoveAll(profilesDir)
|
||||
if seed != nil {
|
||||
seedStore(t, seed)
|
||||
}
|
||||
for _, oc := range outcomes {
|
||||
success, _ := oc[0].(bool)
|
||||
topic, _ := oc[1].(string)
|
||||
recordInteraction(name, success, topic)
|
||||
}
|
||||
return storeFiles()
|
||||
case "seam_get_prompt_context":
|
||||
var name string
|
||||
arg(0, &name)
|
||||
var seed map[string]json.RawMessage
|
||||
argOpt(1, &seed)
|
||||
os.RemoveAll(profilesDir)
|
||||
if seed != nil {
|
||||
seedStore(t, seed)
|
||||
}
|
||||
return getPromptContext(name)
|
||||
case "seam_to_dict":
|
||||
var state map[string]json.RawMessage
|
||||
arg(0, &state)
|
||||
return profileToMap(profileFromState(t, state))
|
||||
case "seam_from_dict":
|
||||
var data map[string]json.RawMessage
|
||||
arg(0, &data)
|
||||
return profileToMap(profileFromState(t, data))
|
||||
default:
|
||||
t.Fatalf("frozen case names unknown fn %q", fn)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// profileFromState rebuilds a ModelProfile from a from_dict-shaped doc
|
||||
// (unknown keys filtered, absent keys defaulted — the oracle lens).
|
||||
func profileFromState(t *testing.T, state map[string]json.RawMessage) ModelProfile {
|
||||
t.Helper()
|
||||
p := defaultModelProfile("")
|
||||
get := func(key string) (json.RawMessage, bool) {
|
||||
raw, ok := state[key]
|
||||
return raw, ok
|
||||
}
|
||||
if raw, ok := get("name"); ok {
|
||||
if err := json.Unmarshal(raw, &p.Name); err != nil {
|
||||
t.Fatalf("state name: %v", err)
|
||||
}
|
||||
}
|
||||
strField := func(key string, dst *string) {
|
||||
if raw, ok := get(key); ok {
|
||||
if err := json.Unmarshal(raw, dst); err != nil {
|
||||
t.Fatalf("state %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
strField("display_name", &p.DisplayName)
|
||||
strField("family", &p.Family)
|
||||
strField("parameters", &p.Parameters)
|
||||
strField("quantization", &p.Quantization)
|
||||
strField("thinking_format", &p.ThinkingFormat)
|
||||
strField("response_style", &p.ResponseStyle)
|
||||
strField("last_used", &p.LastUsed)
|
||||
strField("created", &p.Created)
|
||||
numField := func(key string, dst *float64) {
|
||||
if raw, ok := get(key); ok {
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err != nil {
|
||||
t.Fatalf("state %s: %v", key, err)
|
||||
}
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
t.Fatalf("state %s: %v", key, err)
|
||||
}
|
||||
*dst = f
|
||||
}
|
||||
}
|
||||
intField := func(key string, dst *int) {
|
||||
if raw, ok := get(key); ok {
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err != nil {
|
||||
t.Fatalf("state %s: %v", key, err)
|
||||
}
|
||||
i, err := n.Int64()
|
||||
if err != nil {
|
||||
t.Fatalf("state %s: %v", key, err)
|
||||
}
|
||||
*dst = int(i)
|
||||
}
|
||||
}
|
||||
var f float64
|
||||
numField("size_gb", &f)
|
||||
p.SizeGB = f
|
||||
numField("optimal_temperature", &p.OptimalTemperature)
|
||||
intField("safety_tier", &p.SafetyTier)
|
||||
intField("context_window", &p.ContextWindow)
|
||||
intField("interactions", &p.Interactions)
|
||||
intField("successes", &p.Successes)
|
||||
intField("failures", &p.Failures)
|
||||
listField := func(key string, dst *[]string) {
|
||||
if raw, ok := get(key); ok {
|
||||
var l []string
|
||||
if err := json.Unmarshal(raw, &l); err != nil {
|
||||
t.Fatalf("state %s: %v", key, err)
|
||||
}
|
||||
if l == nil {
|
||||
l = []string{}
|
||||
}
|
||||
*dst = l
|
||||
}
|
||||
}
|
||||
listField("strengths", &p.Strengths)
|
||||
listField("weaknesses", &p.Weaknesses)
|
||||
listField("prompting_tips", &p.PromptingTips)
|
||||
listField("known_refusal_topics", &p.KnownRefusalTopics)
|
||||
return p
|
||||
}
|
||||
|
||||
// normalizeExpected rewrites the rig-rooted absolute paths the frozen
|
||||
// expectations carry into the $STORE placeholder (the store root is the
|
||||
// seam — only the sanitized file name is contractual).
|
||||
func normalizeExpected(v any) any {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
if strings.Contains(x, ".rune/model_profiles/") {
|
||||
parts := strings.Split(x, ".rune/model_profiles/")
|
||||
return filepath.Join("$STORE", parts[len(parts)-1])
|
||||
}
|
||||
return x
|
||||
case map[string]any:
|
||||
for k, e := range x {
|
||||
x[k] = normalizeExpected(e)
|
||||
}
|
||||
return x
|
||||
case []any:
|
||||
for i, e := range x {
|
||||
x[i] = normalizeExpected(e)
|
||||
}
|
||||
return x
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrozenBattery(t *testing.T) {
|
||||
setProfilesDir(filepath.Join(t.TempDir(), ".rune", "model_profiles"))
|
||||
savedNow := nowUTC
|
||||
nowUTC = func() time.Time {
|
||||
return time.Date(2026, 10, 13, 9, 41, 23, 123456000, time.UTC)
|
||||
}
|
||||
defer func() { nowUTC = savedNow }()
|
||||
var envelope struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []json.RawMessage `json:"args"`
|
||||
Kwargs map[string]any `json:"kwargs"`
|
||||
Expected any `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(frozenMpTruthJSON), &envelope); err != nil {
|
||||
t.Fatalf("frozen truth malformed: %v", err)
|
||||
}
|
||||
for i, c := range envelope.Cases {
|
||||
if len(c.Kwargs) != 0 {
|
||||
t.Fatalf("case %d %s: unexpected kwargs %v", i, c.Fn, c.Kwargs)
|
||||
}
|
||||
got := dispatchFrozen(t, c.Fn, c.Args)
|
||||
raw, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("case %d %s: marshal got: %v", i, c.Fn, err)
|
||||
}
|
||||
var gotAny any
|
||||
if err := json.Unmarshal(raw, &gotAny); err != nil {
|
||||
t.Fatalf("case %d %s: reparse: %v", i, c.Fn, err)
|
||||
}
|
||||
wantAny := normalizeExpected(c.Expected)
|
||||
if !reflect.DeepEqual(gotAny, wantAny) {
|
||||
gj, _ := json.Marshal(gotAny)
|
||||
ej, _ := json.Marshal(wantAny)
|
||||
t.Errorf("case %d %s:\n got %s\n want %s", i, c.Fn, gj, ej)
|
||||
}
|
||||
}
|
||||
fmt.Printf("replayed %d frozen cases\n", len(envelope.Cases))
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
truth = TRUTH.read_text()
|
||||
assert truth.endswith("\n"), "truth file must end with newline"
|
||||
body = HARNESS.replace("__TRUTH__", truth.rstrip("\n"))
|
||||
OUT.write_text(body)
|
||||
print(f"wrote {OUT} ({len(body)} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_parallel_processor_truth.py — freeze the parallel_processor oracle
|
||||
truth (dichotomy gap-close, wave #2479) through the SHARED harness
|
||||
(scripts/freeze_truth.py) under the scripts/pp_freeze_seams rig.
|
||||
|
||||
Row grammar: raw values ride directly (non-callable / non-tuple error
|
||||
branches need no rig); callable behavior rides the kind/fn/value spec
|
||||
grammar the rig turns into real Python callables and the Go battery
|
||||
rebuilds into specCallable tasks.
|
||||
|
||||
Usage: PYTHONPATH=scripts/pp_freeze_seams:src python3 scripts/gen_parallel_processor_truth.py
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from freeze_truth import freeze, write_envelope # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-14T08:00:00+00:00"
|
||||
OUT = REPO / "src" / "go" / "parallel-processor" / "truth.json"
|
||||
|
||||
|
||||
def s(kind, **kw):
|
||||
d = {"kind": kind}
|
||||
d.update(kw)
|
||||
return d
|
||||
|
||||
|
||||
CASES = [
|
||||
# module run_parallel — raw non-callables (direct rows, no seam)
|
||||
("run_parallel", ([42],), {}),
|
||||
("run_parallel", ([42, "x", None, True, 4.5, [1], {"a": 1}],), {}),
|
||||
("run_parallel", ([],), {}),
|
||||
# module run_parallel_with_args — raw non-tuples + the len-0 list task
|
||||
("run_parallel_with_args", ([42, "x", None],), {}),
|
||||
("run_parallel_with_args", ([[]],), {}),
|
||||
# rig seams — callable specs through the real executor paths
|
||||
("_pp_run_parallel", ([s("call", fn="const", ret=7), s("call", fn="boom", msg="kaput"), s("call", fn="const", ret="x")],), {}),
|
||||
("_pp_run_parallel", ([s("call", fn="sleepret", ms=60, ret="a"), s("call", fn="sleepret", ms=10, ret="b"), s("call", fn="sleepret", ms=35, ret="c"), s("call", fn="boom", msg="late"), s("raw", value=None)],), {}),
|
||||
("_pp_run_parallel", ([],), {}),
|
||||
("_pp_class_run_parallel", (3, [s("call", fn="const", ret=1), s("call", fn="const", ret=2), s("call", fn="boom", msg="nope"), s("call", fn="const", ret=4), s("call", fn="sleepret", ms=15, ret=5)]), {}),
|
||||
("_pp_class_run_parallel", (1, [s("call", fn="sleepret", ms=30, ret="w1"), s("call", fn="sleepret", ms=5, ret="w2"), s("call", fn="const", ret="w3")]), {}),
|
||||
# with-args branch matrix
|
||||
("_pp_run_parallel_with_args", ([
|
||||
s("call", fn="add", args=[1, 2]),
|
||||
s("call", fn="add", args=[1.5, 2]),
|
||||
s("call", fn="echo"),
|
||||
s("call", fn="echo", kwargs={"k": 1, "b": 2}),
|
||||
s("call", fn="boom", msg="bad-task"),
|
||||
],), {}),
|
||||
("_pp_run_parallel_with_args", ([
|
||||
s("raw", value=42),
|
||||
s("raw", value="x"),
|
||||
s("raw", value=None),
|
||||
s("emptytuple"),
|
||||
s("notcall", value=42),
|
||||
s("notcall", value="x"),
|
||||
s("notcall", value=None),
|
||||
s("notcall", value=True),
|
||||
s("notcall", value=4.5),
|
||||
s("notcall", value=[1, 2]),
|
||||
],), {}),
|
||||
("_pp_run_parallel_with_args", ([
|
||||
s("argsnotseq", fn="echo", value="ab"),
|
||||
s("argsnotseq", fn="echo", value=5),
|
||||
s("kwnotdict", fn="echo", value=["a"]),
|
||||
s("call", fn="sleepret", ms=10, ret="z"),
|
||||
],), {}),
|
||||
("_pp_class_run_parallel_with_args", (2, [
|
||||
s("call", fn="echo", args=[1, 2], kwargs={"scale": 10}),
|
||||
s("raw", value=3.5),
|
||||
s("call", fn="boom", msg="class-branch"),
|
||||
s("notcall", value=False),
|
||||
]), {}),
|
||||
# status surface (active_threads pinned by the rig on both sides)
|
||||
("get_status", (), {}),
|
||||
({"fn": "ParallelProcessor.get_status", "recv": {"cls": "ParallelProcessor", "max_workers": 1}, "args": [], "kwargs": {}}),
|
||||
({"fn": "ParallelProcessor.get_status", "recv": {"cls": "ParallelProcessor", "max_workers": 9}, "args": [], "kwargs": {}}),
|
||||
("_pp_get_processor_status", (), {}),
|
||||
# lifecycle: shutdown shapes, crash parity, global reset
|
||||
("_pp_shutdown_then_status", (), {}),
|
||||
("_pp_shutdown_then_run", (), {}),
|
||||
("_pp_new_bad", (), {}),
|
||||
("_pp_shutdown_processor_cycle", (), {}),
|
||||
# oracle self-test fn (returns None; its printed report is wall-clock,
|
||||
# verified structurally by the parity sweep instead)
|
||||
("test_parallel", (), {}),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
truth = freeze("rune.parallel_processor", CASES, oracle_module="rune.parallel_processor")
|
||||
write_envelope(FROZEN_NOW, truth, OUT)
|
||||
|
||||
# spot-check the shapes the battery depends on
|
||||
by_fn = {}
|
||||
for row in truth:
|
||||
by_fn.setdefault(row["fn"], []).append(row)
|
||||
assert len(truth) == len(CASES), f"freeze produced {len(truth)} rows for {len(CASES)} cases"
|
||||
assert by_fn["run_parallel"][0]["expected"] == [{"error": "'int' object is not callable"}]
|
||||
assert by_fn["_pp_shutdown_then_run"][0]["expected"]["raised"] == "cannot schedule new futures after shutdown"
|
||||
assert by_fn["_pp_new_bad"][0]["expected"]["raised"] == "max_workers must be greater than 0"
|
||||
assert by_fn["_pp_shutdown_processor_cycle"][0]["expected"] == {
|
||||
"processor_is_none": True, "recreated_max_workers": 4}
|
||||
assert by_fn["get_status"][0]["expected"] == {
|
||||
"max_workers": 4, "pool_threads": 4, "active_threads": 4}
|
||||
assert by_fn["_pp_class_run_parallel"][1]["expected"] == ["w1", "w2", "w3"]
|
||||
|
||||
print(f"wrote {OUT}: {len(truth)} cases, frozen_now={FROZEN_NOW}")
|
||||
for fn, rows in by_fn.items():
|
||||
print(f" {fn}: {len(rows)} row(s)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,515 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_provider_registry_truth.py — dichotomy-gap truth battery for
|
||||
src/rune/provider_registry.py (factory-gap-pr, #2479 tranche, GO side).
|
||||
|
||||
Two artifacts, both embedded VERBATIM by the Go battery
|
||||
(src/go/provider-registry/):
|
||||
|
||||
1. truth.json — shared-harness format ([{"fn","args","kwargs","expected"}])
|
||||
over the MODULE-LEVEL pure functions:
|
||||
_format_tools_openai, _format_tools_anthropic (every branch),
|
||||
_ctx_budget (unknown + every DEFAULT_PROFILES id),
|
||||
_execute_tool_calls / _execute_tool_calls_anthropic (JSON args,
|
||||
malformed args, non-serializable tool result).
|
||||
Verifiable offline with:
|
||||
PYTHONPATH=src python3 scripts/freeze_truth.py --verify truth.json \
|
||||
--module rune.provider_registry
|
||||
|
||||
2. truth_registry.json — method-battery scenarios for the ProviderRegistry
|
||||
surface (to_dict, _load_config, _save_config, connect, disconnect,
|
||||
list_providers, get_provider, list_models, _fetch_models dispatch,
|
||||
_fetch_ollama_models, _fetch_openai_models, _fetch_anthropic_models,
|
||||
_clean_model_name, generate branches, _generate_* payload construction
|
||||
+ response parsing). HTTP is fully scripted: urllib.request.urlopen is
|
||||
replaced by a routing fake (never a socket); response-shaping truth is
|
||||
frozen from the LIVE oracle. Non-deterministic latency_ms is excluded
|
||||
(no-clock law: the oracle's clock is not injectable) and compared
|
||||
neither on the Python side nor the Go side.
|
||||
|
||||
Re-verify both artifacts byte-for-byte with --check.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
os.environ["RUNE_PROVIDER_REGISTRY_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
import provider_registry as pr # noqa: E402
|
||||
from freeze_truth import freeze, write as write_truth # noqa: E402
|
||||
|
||||
OUT_HARNESS = REPO / "src" / "go" / "provider-registry" / "truth.json"
|
||||
OUT_REGISTRY = REPO / "src" / "go" / "provider-registry" / "truth_registry.json"
|
||||
|
||||
DENIED_PATH = "/nonexistent/zzz.txt"
|
||||
|
||||
|
||||
# ── scripted HTTP (never a socket) ──────────────────────────────────────────
|
||||
|
||||
class _FakeResp:
|
||||
def __init__(self, body: bytes):
|
||||
self._body = body
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
class _FakeURLOpen:
|
||||
"""Route table: [{"suffix": "/api/tags", "body": {...}} | {"exc": msg}].
|
||||
POST request bodies are captured for payload-construction truth."""
|
||||
|
||||
def __init__(self, routes):
|
||||
self.routes = routes
|
||||
self.captured = []
|
||||
|
||||
def __call__(self, req, timeout=None):
|
||||
url = req.full_url
|
||||
for r in self.routes:
|
||||
if url.endswith(r["suffix"]):
|
||||
if "exc" in r:
|
||||
raise urllib.error.URLError(r["exc"])
|
||||
self.captured.append({
|
||||
"url": url,
|
||||
"body": json.loads(req.data.decode()) if req.data else None,
|
||||
})
|
||||
return _FakeResp(json.dumps(r["body"]).encode())
|
||||
raise AssertionError(f"unexpected url: {url}")
|
||||
|
||||
|
||||
def _provider_fields(p):
|
||||
return {
|
||||
"name": p.name, "url": p.url, "provider_type": p.provider_type,
|
||||
"api_key": p.api_key, "enabled": p.enabled,
|
||||
"last_health_check": p.last_health_check,
|
||||
"health_status": p.health_status,
|
||||
"models": copy.deepcopy(p.models),
|
||||
}
|
||||
|
||||
|
||||
def _providers_state(reg):
|
||||
return {name: _provider_fields(p) for name, p in reg.providers.items()}
|
||||
|
||||
|
||||
def _config_state(reg):
|
||||
out = {}
|
||||
cfg = Path(reg.config_path)
|
||||
if cfg.exists():
|
||||
out["config"] = json.loads(cfg.read_text())
|
||||
return out
|
||||
|
||||
|
||||
def _strip_latency(result):
|
||||
out = {k: copy.deepcopy(v) for k, v in result.items() if k != "latency_ms"}
|
||||
return out
|
||||
|
||||
|
||||
# ── part 1: module-level pure battery (shared harness) ──────────────────────
|
||||
|
||||
TOOLS = [
|
||||
{"name": "read_file", "description": "Read a file",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}}},
|
||||
{"name": "write_file"}, # no description, no parameters
|
||||
]
|
||||
|
||||
# scripted provider responses (shared by both batteries)
|
||||
OLLAMA_TAG = {
|
||||
"models": [{
|
||||
"name": "qwen2.5:3b", "size": 3425952140,
|
||||
"details": {"family": "qwen2", "parameter_size": "3B"},
|
||||
}],
|
||||
}
|
||||
OLLAMA_NO_NAME = {"models": [{"size": 10}]}
|
||||
OPENAI_MODELS = {"data": [{"id": "gpt-x"}, {"id": "mimo-v2.5-pro"}]}
|
||||
ANTHROPIC_MODELS = {"content": [
|
||||
{"type": "text", "text": " thinking "},
|
||||
{"type": "tool_use", "id": "tu_9", "name": "read_file",
|
||||
"input": {"path": DENIED_PATH}},
|
||||
], "usage": {"input_tokens": 12, "output_tokens": 7}}
|
||||
OLLAMA_GEN = {"response": " hi there ", "eval_count": 42}
|
||||
OPENAI_PLAIN = {"choices": [{"message": {"content": " hello "}}],
|
||||
"usage": {"total_tokens": 17}}
|
||||
OPENAI_TOOLS = {"choices": [{"message": {
|
||||
"content": "calling tool",
|
||||
"tool_calls": [{"id": "call_1", "type": "function",
|
||||
"function": {"name": "read_file",
|
||||
"arguments": json.dumps({"path": DENIED_PATH})}}],
|
||||
}}], "usage": {"total_tokens": 5}}
|
||||
|
||||
|
||||
def harness_cases():
|
||||
cases = []
|
||||
# _format_tools_openai / _format_tools_anthropic — every branch
|
||||
cases.append(("_format_tools_openai", ([],), {}))
|
||||
cases.append(("_format_tools_openai", (TOOLS[:1],), {}))
|
||||
cases.append(("_format_tools_openai", (TOOLS[1:],), {}))
|
||||
cases.append(("_format_tools_openai", (TOOLS,), {}))
|
||||
cases.append(("_format_tools_anthropic", ([],), {}))
|
||||
cases.append(("_format_tools_anthropic", (TOOLS[:1],), {}))
|
||||
cases.append(("_format_tools_anthropic", (TOOLS[1:],), {}))
|
||||
cases.append(("_format_tools_anthropic", (TOOLS,), {}))
|
||||
# _ctx_budget — unknown id + every DEFAULT_PROFILES id
|
||||
for model_id in ("unknown-model-xyz", "deterministic", "qwen2.5:3b",
|
||||
"qwen3:14b", "qwen3.6:27b", "mimo-v2.5-pro"):
|
||||
cases.append(("_ctx_budget", (model_id,), {}))
|
||||
# _execute_tool_calls — parsed args, malformed args, non-serializable
|
||||
cases.append(("_execute_tool_calls",
|
||||
([{"id": "call_1", "function": {
|
||||
"name": "read_file",
|
||||
"arguments": json.dumps({"path": DENIED_PATH})}}],), {}))
|
||||
cases.append(("_execute_tool_calls",
|
||||
([{"id": "call_2", "function": {
|
||||
"name": "read_file", "arguments": "not-json"}}],), {}))
|
||||
cases.append(("_execute_tool_calls",
|
||||
([{"id": "call_3", "function": {
|
||||
"name": "no_such_tool_xyz", "arguments": "{}"}}],), {}))
|
||||
cases.append(("_execute_tool_calls_anthropic",
|
||||
([{"type": "tool_use", "id": "tu_1", "name": "read_file",
|
||||
"input": {"path": DENIED_PATH}}],), {}))
|
||||
cases.append(("_execute_tool_calls_anthropic",
|
||||
([{"type": "tool_use", "id": "tu_2",
|
||||
"name": "no_such_tool_xyz", "input": {}}],), {}))
|
||||
return cases
|
||||
|
||||
|
||||
# ── part 2: registry method battery (scripted HTTP, hermetic config) ───────
|
||||
|
||||
def registry_rows():
|
||||
rows = []
|
||||
# hermetic config dir — no registry may read the ambient providers.json
|
||||
_td = tempfile.TemporaryDirectory()
|
||||
_HERMETIC_CFG = str(Path(_td.name) / "providers.json")
|
||||
|
||||
def row(fn, name, inp, expect, expect_error=None):
|
||||
rec = {"fn": fn, "name": name, "input": inp, "expected": expect}
|
||||
if expect_error is not None:
|
||||
rec["expected_error"] = expect_error
|
||||
rows.append(rec)
|
||||
|
||||
def make_provider(**kw):
|
||||
kw.setdefault("enabled", True)
|
||||
kw.setdefault("models", [])
|
||||
kw.setdefault("health_status", "unknown")
|
||||
return pr.ModelProvider(**kw)
|
||||
|
||||
def make_reg(providers=None, config_path=None):
|
||||
reg = pr.ProviderRegistry(config_path=config_path or _HERMETIC_CFG)
|
||||
for n, p in (providers or {}).items():
|
||||
reg.providers[n] = p
|
||||
return reg
|
||||
|
||||
# ── to_dict ─────────────────────────────────────────────────────────
|
||||
p = make_provider(name="p1", url="http://x", provider_type="openai",
|
||||
api_key="sk-secret", models=[{"name": "m1"}],
|
||||
enabled=False, last_health_check="2026-01-01",
|
||||
health_status="error")
|
||||
row("to_dict", "full", {"provider": _provider_fields(p)}, p.to_dict())
|
||||
|
||||
# ── _load_config ────────────────────────────────────────────────────
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
missing = str(Path(td) / "a" / "providers.json")
|
||||
reg = pr.ProviderRegistry(config_path=missing)
|
||||
row("_load_config", "missing_file", {"config_path": "TMP:a/providers.json"},
|
||||
{"providers": _providers_state(reg)})
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
cfg = str(Path(td) / "providers.json")
|
||||
Path(cfg).write_text("{not json")
|
||||
reg = pr.ProviderRegistry(config_path=cfg)
|
||||
row("_load_config", "malformed", {"config_path": "TMP:malformed", "content": "{not json"},
|
||||
{"providers": _providers_state(reg)})
|
||||
|
||||
full_cfg = {"providers": {
|
||||
"alpha": {"url": "http://a", "type": "openai", "api_key": "k1", "enabled": False},
|
||||
"beta": {"url": "http://b"}, # defaults: type=custom, enabled=true, api_key=None
|
||||
}}
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
cfg = str(Path(td) / "providers.json")
|
||||
Path(cfg).write_text(json.dumps(full_cfg))
|
||||
reg = pr.ProviderRegistry(config_path=cfg)
|
||||
row("_load_config", "full_and_defaults", {"config_path": "TMP:full", "content": full_cfg},
|
||||
{"providers": _providers_state(reg)})
|
||||
|
||||
# ── _save_config (masking law) ──────────────────────────────────────
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
cfg = str(Path(td) / "nested" / "providers.json")
|
||||
reg = make_reg({
|
||||
"secret": make_provider(name="secret", url="http://s",
|
||||
provider_type="openai", api_key="sk-live",
|
||||
enabled=True),
|
||||
"plain": make_provider(name="plain", url="http://p",
|
||||
provider_type="ollama", api_key=None,
|
||||
enabled=False),
|
||||
}, config_path=cfg)
|
||||
reg._save_config()
|
||||
row("_save_config", "masks_api_key",
|
||||
{"config_path": "TMP:nested",
|
||||
"providers": _providers_state(reg)},
|
||||
{"written": json.loads(Path(cfg).read_text())})
|
||||
|
||||
# ── connect / disconnect / list_providers / get_provider ────────────
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
cfg = str(Path(td) / "providers.json")
|
||||
reg = pr.ProviderRegistry(config_path=cfg)
|
||||
prov = reg.connect("zeta", "http://z", "openai", "zk")
|
||||
row("connect", "persists_and_masks",
|
||||
{"config_path": "TMP:connect", "name": "zeta", "url": "http://z",
|
||||
"provider_type": "openai", "api_key": "zk"},
|
||||
{"provider": prov.to_dict(), **_config_state(reg)})
|
||||
reg.connect("alpha", "http://a", "ollama", None)
|
||||
row("list_providers", "sorted_names",
|
||||
{"config_path": "TMP:list", "providers": _providers_state(reg)},
|
||||
{"providers": reg.list_providers()})
|
||||
got = reg.get_provider("alpha")
|
||||
row("get_provider", "hit",
|
||||
{"config_path": "TMP:get", "name": "alpha",
|
||||
"providers": _providers_state(reg)},
|
||||
{"provider": got.to_dict() if got else None})
|
||||
miss = reg.get_provider("nope")
|
||||
row("get_provider", "miss", {"config_path": "TMP:get", "name": "nope",
|
||||
"providers": _providers_state(reg)},
|
||||
{"provider": miss.to_dict() if miss else None})
|
||||
pre = _providers_state(reg)
|
||||
seed = json.loads(Path(cfg).read_text()) if Path(cfg).exists() else None
|
||||
reg.disconnect("zeta")
|
||||
inp = {"config_path": "TMP:disc", "name": "zeta", "providers": pre}
|
||||
if seed is not None:
|
||||
inp["seed_config"] = seed
|
||||
row("disconnect", "present", inp,
|
||||
{"providers": _providers_state(reg), **_config_state(reg)})
|
||||
pre2 = _providers_state(reg)
|
||||
seed2 = json.loads(Path(cfg).read_text()) if Path(cfg).exists() else None
|
||||
reg.disconnect("ghost")
|
||||
inp2 = {"config_path": "TMP:disc2", "name": "ghost", "providers": pre2}
|
||||
if seed2 is not None:
|
||||
inp2["seed_config"] = seed2
|
||||
row("disconnect", "absent_noop", inp2,
|
||||
{"providers": _providers_state(reg), **_config_state(reg)})
|
||||
|
||||
# ── _clean_model_name (delegates to model_connect) ──────────────────
|
||||
reg = pr.ProviderRegistry(config_path="/nonexistent/pr-gen.json")
|
||||
for name in ("qwen2.5:3b", "mimo-v2.5-pro",
|
||||
"hf.co/DavidAU/Qwen3.6-27B-Fable-Fusion-GGUF:IQ2_M",
|
||||
"hf.co/org/llama-3-8b", "hf.co/a/b",
|
||||
"some-unknown-name-with-dashes"):
|
||||
row("_clean_model_name", name[:24], {"name": name},
|
||||
reg._clean_model_name(name))
|
||||
|
||||
# ── _fetch_models dispatch + provider fetchers (scripted HTTP) ──────
|
||||
ollama_p = make_provider(name="ol", url="http://127.0.0.1:11434",
|
||||
provider_type="ollama")
|
||||
openai_p = make_provider(name="oa", url="http://api.example",
|
||||
provider_type="openai", api_key="sk")
|
||||
anthropic_p = make_provider(name="an", url="http://an.example",
|
||||
provider_type="anthropic")
|
||||
custom_p = make_provider(name="cu", url="http://c.example",
|
||||
provider_type="custom")
|
||||
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/api/tags", "body": OLLAMA_TAG}])):
|
||||
row("_fetch_ollama_models", "success",
|
||||
{"provider": _provider_fields(ollama_p), "routes": [{"suffix": "/api/tags", "body": OLLAMA_TAG}]},
|
||||
reg._fetch_ollama_models(ollama_p))
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/api/tags", "exc": "conn refused"}])):
|
||||
row("_fetch_ollama_models", "connection_error",
|
||||
{"provider": _provider_fields(ollama_p),
|
||||
"routes": [{"suffix": "/api/tags", "exc": "conn refused"}]},
|
||||
None, expect_error="Ollama connection failed: <urlopen error conn refused>")
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/api/tags", "body": OLLAMA_NO_NAME}])):
|
||||
row("_fetch_ollama_models", "missing_name_key",
|
||||
{"provider": _provider_fields(ollama_p),
|
||||
"routes": [{"suffix": "/api/tags", "body": OLLAMA_NO_NAME}]},
|
||||
None, expect_error="Ollama connection failed: 'name'")
|
||||
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/v1/models", "body": OPENAI_MODELS}])):
|
||||
row("_fetch_openai_models", "success",
|
||||
{"provider": _provider_fields(openai_p), "routes": [{"suffix": "/v1/models", "body": OPENAI_MODELS}]},
|
||||
reg._fetch_openai_models(openai_p))
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/v1/models", "exc": "boom"}])):
|
||||
row("_fetch_openai_models", "connection_error",
|
||||
{"provider": _provider_fields(openai_p),
|
||||
"routes": [{"suffix": "/v1/models", "exc": "boom"}]},
|
||||
None, expect_error="[OI] API connection failed: <urlopen error boom>")
|
||||
|
||||
row("_fetch_anthropic_models", "static_list",
|
||||
{"provider": _provider_fields(anthropic_p)},
|
||||
reg._fetch_anthropic_models(anthropic_p))
|
||||
|
||||
for ptype, prov, routes, body_key in (
|
||||
("ollama", ollama_p, [{"suffix": "/api/tags", "body": OLLAMA_TAG}], "OLLAMA_TAG"),
|
||||
("openai", openai_p, [{"suffix": "/v1/models", "body": OPENAI_MODELS}], "OPENAI_MODELS")):
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen(routes)):
|
||||
row("_fetch_models", f"dispatch_{ptype}",
|
||||
{"provider": _provider_fields(prov), "routes": routes},
|
||||
reg._fetch_models(prov))
|
||||
row("_fetch_models", "dispatch_anthropic",
|
||||
{"provider": _provider_fields(anthropic_p)},
|
||||
reg._fetch_models(anthropic_p))
|
||||
row("_fetch_models", "dispatch_custom_empty",
|
||||
{"provider": _provider_fields(custom_p)},
|
||||
reg._fetch_models(custom_p))
|
||||
|
||||
# ── list_models orchestration ───────────────────────────────────────
|
||||
reg2 = make_reg({"cu": custom_p})
|
||||
row("list_models", "custom_yields_none",
|
||||
{"providers": _providers_state(reg2)}, reg2.list_models())
|
||||
|
||||
reg3 = make_reg({"off": make_provider(name="off", url="http://o",
|
||||
provider_type="openai",
|
||||
enabled=False)})
|
||||
row("list_models", "disabled_skipped",
|
||||
{"providers": _providers_state(reg3)}, reg3.list_models())
|
||||
|
||||
reg4 = make_reg({"bad": make_provider(name="bad", url="http://b",
|
||||
provider_type="ollama")})
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/api/tags", "exc": "conn refused"}])):
|
||||
row("list_models", "fetch_error_degrades",
|
||||
{"providers": _providers_state(reg4),
|
||||
"routes": [{"suffix": "/api/tags", "exc": "conn refused"}]},
|
||||
reg4.list_models())
|
||||
|
||||
reg5 = make_reg({"oa": openai_p})
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/v1/models", "body": OPENAI_MODELS}])):
|
||||
row("list_models", "annotates_provider",
|
||||
{"providers": _providers_state(reg5),
|
||||
"routes": [{"suffix": "/v1/models", "body": OPENAI_MODELS}]},
|
||||
reg5.list_models())
|
||||
|
||||
reg6 = make_reg({"ol": ollama_p})
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/api/tags", "body": OLLAMA_TAG}])):
|
||||
row("list_models", "ollama_shape",
|
||||
{"providers": _providers_state(reg6),
|
||||
"routes": [{"suffix": "/api/tags", "body": OLLAMA_TAG}]},
|
||||
reg6.list_models())
|
||||
|
||||
# ── generate orchestration branches ─────────────────────────────────
|
||||
row("generate", "provider_not_found",
|
||||
{"providers": _providers_state(reg2), "prompt": "hi",
|
||||
"provider_name": "ghost"},
|
||||
reg2.generate("hi", "ghost"))
|
||||
empty_reg = pr.ProviderRegistry(config_path="/nonexistent/pr-gen.json")
|
||||
row("generate", "no_providers", {"prompt": "hi"}, empty_reg.generate("hi"))
|
||||
row("generate", "unsupported_type",
|
||||
{"providers": _providers_state(reg2), "prompt": "hi",
|
||||
"provider_name": "cu", "model": "m"},
|
||||
reg2.generate("hi", "cu", "m"))
|
||||
row("generate", "no_models_available",
|
||||
{"providers": _providers_state(reg2), "prompt": "hi",
|
||||
"provider_name": "cu"},
|
||||
reg2.generate("hi", "cu"))
|
||||
|
||||
# ── _generate_* payload construction + response parsing ─────────────
|
||||
def gen_case(fn, name, provider, routes, expect=None, expect_error=None,
|
||||
**call_kw):
|
||||
fake = _FakeURLOpen(routes)
|
||||
method = getattr(pr.ProviderRegistry, fn)
|
||||
with patch("urllib.request.urlopen", fake):
|
||||
result = method(reg, provider, **call_kw)
|
||||
inp = {"provider": _provider_fields(provider),
|
||||
"routes": [copy.deepcopy(r) for r in routes]}
|
||||
inp.update({k: copy.deepcopy(v) for k, v in call_kw.items()})
|
||||
rec = {"expected_request": fake.captured[0]["body"] if fake.captured else None}
|
||||
if expect_error is not None:
|
||||
rec["expected_error"] = expect_error
|
||||
else:
|
||||
rec["expected"] = _strip_latency(result)
|
||||
row(fn, name, inp, rec)
|
||||
|
||||
gen_case("_generate_ollama", "payload_and_response", ollama_p,
|
||||
[{"suffix": "/api/generate", "body": OLLAMA_GEN}],
|
||||
prompt="hello", model="qwen2.5:3b", max_tokens=500,
|
||||
temperature=0.7)
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/api/generate", "exc": "no server"}])):
|
||||
row("_generate_ollama", "error_degrades",
|
||||
{"provider": _provider_fields(ollama_p),
|
||||
"routes": [{"suffix": "/api/generate", "exc": "no server"}],
|
||||
"prompt": "hello", "model": "qwen2.5:3b", "max_tokens": 500,
|
||||
"temperature": 0.7},
|
||||
{"expected_request": None, "expected_error":
|
||||
"<urlopen error no server>"})
|
||||
|
||||
gen_case("_generate_openai", "plain", openai_p,
|
||||
[{"suffix": "/v1/chat/completions", "body": OPENAI_PLAIN}],
|
||||
prompt="hello", model="mimo-v2.5-pro", max_tokens=500,
|
||||
temperature=0.7, system_prompt="be brief", tools=None)
|
||||
gen_case("_generate_openai", "with_tools", openai_p,
|
||||
[{"suffix": "/v1/chat/completions", "body": OPENAI_TOOLS}],
|
||||
prompt="hello", model="mimo-v2.5-pro", max_tokens=500,
|
||||
temperature=0.7, system_prompt=None, tools=TOOLS)
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/v1/chat/completions", "exc": "401"}])):
|
||||
row("_generate_openai", "error_degrades",
|
||||
{"provider": _provider_fields(openai_p),
|
||||
"routes": [{"suffix": "/v1/chat/completions", "exc": "401"}],
|
||||
"prompt": "hello", "model": "m", "max_tokens": 500,
|
||||
"temperature": 0.7, "system_prompt": None, "tools": None},
|
||||
{"expected_request": None, "expected_error": "<urlopen error 401>"})
|
||||
|
||||
gen_case("_generate_anthropic", "plain", anthropic_p,
|
||||
[{"suffix": "/v1/messages", "body": ANTHROPIC_MODELS}],
|
||||
prompt="hello", model="claude-x", max_tokens=500,
|
||||
temperature=0.7, system_prompt="be brief", tools=None)
|
||||
gen_case("_generate_anthropic", "with_tools", anthropic_p,
|
||||
[{"suffix": "/v1/messages", "body": ANTHROPIC_MODELS}],
|
||||
prompt="hello", model="claude-x", max_tokens=500,
|
||||
temperature=0.7, system_prompt=None, tools=TOOLS)
|
||||
with patch("urllib.request.urlopen", _FakeURLOpen([
|
||||
{"suffix": "/v1/messages", "exc": "overloaded"}])):
|
||||
row("_generate_anthropic", "error_degrades",
|
||||
{"provider": _provider_fields(anthropic_p),
|
||||
"routes": [{"suffix": "/v1/messages", "exc": "overloaded"}],
|
||||
"prompt": "hello", "model": "claude-x", "max_tokens": 500,
|
||||
"temperature": 0.7, "system_prompt": None, "tools": None},
|
||||
{"expected_request": None, "expected_error":
|
||||
"<urlopen error overloaded>"})
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
check = "--check" in sys.argv
|
||||
cases = harness_cases()
|
||||
truth = freeze("rune.provider_registry", cases, oracle_module="rune.provider_registry")
|
||||
|
||||
if check:
|
||||
from freeze_truth import verify
|
||||
rc = verify(OUT_HARNESS, "rune.provider_registry")
|
||||
rows = registry_rows()
|
||||
frozen = json.loads(OUT_REGISTRY.read_text())
|
||||
if json.dumps(rows, sort_keys=True) != json.dumps(frozen, sort_keys=True):
|
||||
print("MISMATCH: truth_registry.json drifted from live oracle")
|
||||
return 1
|
||||
print(f"OK: {len(rows)} registry scenarios byte-identical against live oracle")
|
||||
return rc
|
||||
|
||||
write_truth(truth, OUT_HARNESS)
|
||||
rows = registry_rows()
|
||||
OUT_REGISTRY.write_text(json.dumps(rows, indent=2) + "\n")
|
||||
print(f"wrote {OUT_HARNESS} ({len(truth)} cases)")
|
||||
print(f"wrote {OUT_REGISTRY} ({len(rows)} scenarios)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,359 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_salience_gate_truth.py — freeze/replay parity truth for salience_gate
|
||||
(dichotomy wave #2479 gap-close, shared harness scripts/freeze_truth.py).
|
||||
|
||||
Freeze mode (default): exercises the LIVE Python oracle floor across every
|
||||
public function and branch and writes the shared-harness scheme
|
||||
[{fn, args, kwargs, expected}] twice:
|
||||
* src/rust/salience_gate/src/frozen_sg_truth.verify.json — the shared
|
||||
verify file (order-stable rows only; byte-identical under
|
||||
`freeze_truth.py --verify ... --module rune.salience_gate` regardless of
|
||||
the Rust core's build state).
|
||||
* src/rust/salience_gate/src/frozen_sg_truth.json — envelope carrying ALL
|
||||
rows, including multi-family evaluate rows whose floor evaluation order
|
||||
differs from the delegation path's sorted order (the kernel mirrors the
|
||||
evaluation order — the crate battery compares those exactly).
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_SALIENCE_CORE=python (shared harness)
|
||||
AND poisons SG_RUST_LIB so sg_loader.available() is False — the frozen truth
|
||||
is the pure-Python oracle floor, not a circular capture of the Rust kernel.
|
||||
(The facade's evaluate path ignores RUNE_*_CORE; SG_RUST_LIB is the real
|
||||
floor pin — verified quirk of this module.)
|
||||
|
||||
Replay mode (--replay): unpoisons both pins, requires the built core, then
|
||||
asserts facade-level parity (oracle functions delegating live — scalars
|
||||
exact, flags/rationale under the oracle's declared sorted-comparison
|
||||
contract) and kernel-level parity (sg_loader direct — exact, evaluation
|
||||
order included). _sg_core's live facade case asserts the probe ANSWERED
|
||||
(rune.sg_loader module), mirroring the kernel-side Some branch; the None
|
||||
branch is the frozen truth row.
|
||||
|
||||
Surface covered (fn coverage law — every public fn and branch):
|
||||
evaluate_salience_dict authority ×3 + unknown, urgency ×3 + unknown,
|
||||
failure words incl. "doesn't work"/"not working"
|
||||
+ boundary negatives (fail|ure, failing, FAILED),
|
||||
retry words + "against" negative, contradiction
|
||||
words + "butter" negative, uncertainty words +
|
||||
"mightily" negative, novel words + "first
|
||||
time"/"haven't" + single-append pin (novel family
|
||||
is ONE `if` in the oracle) + boundary negatives
|
||||
(renew, new_thing, a_new, mañnew — Python \b is
|
||||
unicode/underscore-aware), tier ladder edges
|
||||
(0.5→T1, 0.6→T2, 0.7→T2, 0.8→T3, 0.9→T3),
|
||||
witness rule edges (score >= 0.6 true/false;
|
||||
flags>=2 needs score <0.6 — unreachable from text
|
||||
flags, documented), payload/authority/urgency
|
||||
defaults, ignored channel/extra keys
|
||||
SalienceDecision.to_dict defaults, populated, insertion-order echo
|
||||
handle_hook string/dict/null event, data accepted and ignored
|
||||
_sg_core pinned None branch (envelope-only row + battery
|
||||
lattice: the shared gate's module-name-derived pin
|
||||
RUNE_SALIENCE_GATE_CORE cannot reach this oracle)
|
||||
test_salience crate smoke test (Rust test fn, not a truth row)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
CRATE = REPO / "src" / "rust" / "salience_gate"
|
||||
VERIFY_PATH = CRATE / "src" / "frozen_sg_truth.verify.json"
|
||||
ENVELOPE_PATH = CRATE / "src" / "frozen_sg_truth.json"
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if REPLAY:
|
||||
os.environ.pop("SG_RUST_LIB", None) # delegation live for the replay pass
|
||||
os.environ.pop("RUNE_SALIENCE_CORE", None)
|
||||
else:
|
||||
# POISONED-TRUTH LAW (freeze): SG_RUST_LIB poisoned → sg_loader.available()
|
||||
# False → the oracle runs its pure-Python floor. RUNE_SALIENCE_CORE=python
|
||||
# is the module's own probe pin — NOTE the shared harness derives
|
||||
# RUNE_SALIENCE_GATE_CORE from the module name and would never reach this
|
||||
# oracle's env read; the generator owns the correct pin name.
|
||||
os.environ["RUNE_SALIENCE_CORE"] = "python"
|
||||
os.environ["SG_RUST_LIB"] = "/nonexistent/libsalience_gate.so"
|
||||
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
if REPLAY:
|
||||
from rune import sg_loader # noqa: E402
|
||||
if not sg_loader.available():
|
||||
print("REPLAY ABORT: Rust salience_gate core not built/available")
|
||||
sys.exit(2)
|
||||
|
||||
import rune.salience_gate as oracle_mod # noqa: E402
|
||||
from freeze_truth import freeze # noqa: E402
|
||||
|
||||
|
||||
def env(text: str, **kw):
|
||||
e = {"payload": {"text": text}}
|
||||
e.update(kw)
|
||||
return e
|
||||
|
||||
|
||||
def e_case(name, envelope):
|
||||
return {"fn": "evaluate_salience_dict", "args": [envelope], "kwargs": {}}
|
||||
|
||||
|
||||
def td_case(name, score, flags, tier, witness, rationale):
|
||||
return {
|
||||
"fn": "SalienceDecision.to_dict",
|
||||
"recv": {"cls": "SalienceDecision", "score": score, "flags": flags,
|
||||
"minimum_tier": tier, "witness_needed": witness,
|
||||
"rationale_codes": rationale},
|
||||
"args": [], "kwargs": {},
|
||||
}
|
||||
|
||||
|
||||
def hh_case(name, event, data=None):
|
||||
kwargs = {} if data is None else {"data": data}
|
||||
return {"fn": "handle_hook", "args": [event], "kwargs": kwargs}
|
||||
|
||||
|
||||
def sc_case(name):
|
||||
return {"fn": "_sg_core", "args": [], "kwargs": {}}
|
||||
|
||||
|
||||
FAILURE = dict((("flags", ["TASK_FAILURE"]),
|
||||
("minimum_tier", "T2"), ("witness_needed", True),
|
||||
("rationale_codes", ["failure_detected"])))
|
||||
|
||||
|
||||
RETRY = dict((("flags", ["REPEATED_RETRY"]),
|
||||
("minimum_tier", "T3"), ("witness_needed", True),
|
||||
("rationale_codes", ["repeated_retry"])))
|
||||
|
||||
CONTRA = dict((("flags", ["CONTRADICTION"]),
|
||||
("minimum_tier", "T2"), ("witness_needed", True),
|
||||
("rationale_codes", ["contradiction_detected"])))
|
||||
|
||||
UNCERT = dict((("flags", ["HIGH_UNCERTAINTY"]),
|
||||
("minimum_tier", "T2"), ("witness_needed", True),
|
||||
("rationale_codes", ["high_uncertainty"])))
|
||||
|
||||
NOVEL = dict((("flags", ["NOVEL_CAPABILITY_REQUEST"]),
|
||||
("minimum_tier", "T1"), ("witness_needed", False),
|
||||
("rationale_codes", ["novel_request"])))
|
||||
|
||||
|
||||
# ── verify file rows — order-stable (single flag/code families so floor
|
||||
# evaluation order == the delegation path's sorted order; the shared gate
|
||||
# must stay byte-identical with or without the built core) ────────────────
|
||||
VERIFY_CASES = [
|
||||
e_case("greeting-minimal",
|
||||
{"authority_class": "conversation", "urgency": "normal",
|
||||
"channel": "conversation", "payload": {"text": "hello"}}),
|
||||
e_case("empty-envelope", {}),
|
||||
e_case("empty-payload", {"payload": {}}),
|
||||
e_case("authority-emergency",
|
||||
{"authority_class": "emergency", "urgency": "normal", "channel": "work",
|
||||
"payload": {"text": "deploy now"}}),
|
||||
e_case("authority-task-amendment",
|
||||
{"authority_class": "task_amendment", "payload": {"text": "adjust the plan"}}),
|
||||
e_case("authority-directive",
|
||||
{"authority_class": "directive", "payload": {"text": "do the thing"}}),
|
||||
e_case("authority-unknown", env("ignored", authority_class="weird")),
|
||||
e_case("urgency-critical", env("hello", urgency="critical")),
|
||||
e_case("urgency-high", env("hello", urgency="high")),
|
||||
e_case("urgency-low", env("hello", urgency="low")),
|
||||
e_case("urgency-unknown", env("ok", urgency="hi")),
|
||||
e_case("failure-word-fail", env("fail")),
|
||||
e_case("failure-word-error", env("total error now")),
|
||||
e_case("failure-word-broken", env("broken pipe")),
|
||||
e_case("failure-doesnt-work", env("it doesn't work")),
|
||||
e_case("failure-not-working", env("not working today")),
|
||||
e_case("failure-boundary-failure-noun", env("failure")),
|
||||
e_case("failure-boundary-failing", env("the test is failing")),
|
||||
e_case("failure-boundary-uppercase-failed", env("THE FILE FAILED")),
|
||||
e_case("retry-word-again", env("try again")),
|
||||
e_case("retry-word-keeps", env("keeps crashing")),
|
||||
e_case("retry-word-still", env("still crashes")),
|
||||
e_case("retry-word-repeatedly", env("repeatedly stuck")),
|
||||
e_case("retry-boundary-against", env("against the wall")),
|
||||
e_case("contradiction-but", env("works but slow")),
|
||||
e_case("contradiction-however", env("however fine")),
|
||||
e_case("contradiction-actually", env("actually correct")),
|
||||
e_case("contradiction-instead", env("instead do this")),
|
||||
e_case("contradiction-rather", env("rather quick")),
|
||||
e_case("contradiction-boundary-butter", env("butter")),
|
||||
e_case("uncertainty-unsure", env("unsure about it")),
|
||||
e_case("uncertainty-perhaps", env("perhaps later")),
|
||||
e_case("uncertainty-might", env("might be fine")),
|
||||
e_case("uncertainty-boundary-mightily", env("mightily strong")),
|
||||
e_case("novel-new", env("a new feature")),
|
||||
e_case("novel-never", env("never seen this")),
|
||||
e_case("novel-first-time", env("first time here")),
|
||||
e_case("novel-havent", env("haven't seen it")),
|
||||
# ORACLE PARITY PIN (bug fix 2026-10-15): the novel family is ONE `if` —
|
||||
# a new/never word AND first-time/haven't in the same text append the
|
||||
# flag ONCE (the previous port duplicated it, inflating len(flags) and
|
||||
# flipping the witness rule).
|
||||
e_case("novel-single-append-pin", env("a new idea first time")),
|
||||
e_case("novel-boundary-renew", env("renew the lease")),
|
||||
e_case("novel-boundary-underscore-suffix", env("new_thing")),
|
||||
e_case("novel-boundary-underscore-prefix", env("a_new door")),
|
||||
e_case("novel-boundary-unicode-letter", env("mañnew row")),
|
||||
e_case("directive-uppercase-failing-text",
|
||||
{"authority_class": "directive", "urgency": "normal", "channel": "work",
|
||||
"payload": {"text": "FIX THE FAILING TEST"}}),
|
||||
e_case("channel-ignored", env("hello", channel="ops")),
|
||||
e_case("payload-extra-keys-ignored", env("deploy", extra="x")),
|
||||
td_case("to_dict-defaults", 0.0, [], "T0", False, []),
|
||||
td_case("to_dict-populated", 0.9, ["HIGH_RISK_IRREVERSIBLE"], "T3", True,
|
||||
["emergency_authority"]),
|
||||
td_case("to_dict-insertion-order-echo", 0.0,
|
||||
["REPEATED_RETRY", "TASK_FAILURE"], "T0", False,
|
||||
["repeated_retry", "failure_detected"]),
|
||||
hh_case("hook-string-event", "hook-a"),
|
||||
hh_case("hook-dict-event-data-ignored", {"name": "evt-2"}, {"x": 1}),
|
||||
hh_case("hook-null-event", None),
|
||||
# NOTE: _sg_core is ENVELOPE-ONLY. The shared --verify gate re-runs the
|
||||
# oracle through the harness, whose pin is RUNE_SALIENCE_GATE_CORE
|
||||
# (derived from the module name); this oracle reads RUNE_SALIENCE_CORE —
|
||||
# the probe would answer live (module object) and deepcopy would blow up.
|
||||
# The pinned None shape is pinned by the battery row + the sg_core_pin
|
||||
# lattice + the replay probe check instead.
|
||||
]
|
||||
|
||||
# ── envelope-only rows — multi-family; the kernel mirrors the floor's
|
||||
# evaluation order exactly (battery-verified), while the delegation path
|
||||
# re-sorts (facade replay uses the sorted-comparison contract) ────────────
|
||||
EXTRA_CASES = [
|
||||
sc_case("sg-core-pinned-none"),
|
||||
e_case("multi-failure-retry", env("still broken")),
|
||||
e_case("multi-novel-retry", env("never again")),
|
||||
e_case("multi-failure-contradiction", env("but broken")),
|
||||
e_case("multi-emergency-retry-contradiction",
|
||||
{"authority_class": "emergency", "payload": {"text": "deploy but still"}}),
|
||||
e_case("multi-directive-failure-contradiction-novel",
|
||||
{"authority_class": "directive", "payload": {"text": "new but broken"}}),
|
||||
e_case("multi-amendment-retry",
|
||||
{"authority_class": "task_amendment", "payload": {"text": "try again"}}),
|
||||
e_case("multi-critical-novel", env("a new plan", urgency="critical")),
|
||||
e_case("multi-failure-retry-plain", env("fail again")),
|
||||
e_case("multi-directive-failure-retry",
|
||||
{"authority_class": "directive", "payload": {"text": "fail the thing again"}}),
|
||||
e_case("multi-uncertainty-novel", env("maybe new")),
|
||||
]
|
||||
|
||||
|
||||
def freeze_rows(cases):
|
||||
rows = []
|
||||
for case in cases:
|
||||
got = freeze("rune.salience_gate", [case], "rune.salience_gate")[0]
|
||||
rows.append(got)
|
||||
return rows
|
||||
|
||||
|
||||
def canon_flags(d: dict) -> dict:
|
||||
c = dict(d)
|
||||
c["flags"] = sorted(d.get("flags", []))
|
||||
c["rationale_codes"] = sorted(d.get("rationale_codes", []))
|
||||
return c
|
||||
|
||||
|
||||
def replay():
|
||||
with open(ENVELOPE_PATH) as f:
|
||||
rows = json.load(f)["cases"]
|
||||
total, failures = 0, 0
|
||||
|
||||
def check(label, cond, detail=""):
|
||||
nonlocal total, failures
|
||||
total += 1
|
||||
if not cond:
|
||||
failures += 1
|
||||
print(f" ✗ {label} — {detail}")
|
||||
|
||||
for case in rows:
|
||||
fn = case["fn"]
|
||||
name = f"[{fn}] {json.dumps(case['args'])[:60]}"
|
||||
if fn == "evaluate_salience_dict":
|
||||
env_in = copy.deepcopy(case["args"][0])
|
||||
expected = case["expected"]
|
||||
# facade-level (delegation live): the oracle's parity contract is
|
||||
# sorted flags/rationale (oracle comment + canary).
|
||||
got = oracle_mod.evaluate_salience_dict(env_in)
|
||||
check("facade " + name,
|
||||
json.dumps(canon_flags(got), sort_keys=True)
|
||||
== json.dumps(canon_flags(expected), sort_keys=True),
|
||||
f"got={json.dumps(got, sort_keys=True)[:200]} "
|
||||
f"want={json.dumps(expected, sort_keys=True)[:200]}")
|
||||
# kernel-level (loader direct): EXACT, evaluation order included.
|
||||
got_k = sg_loader.evaluate_salience(copy.deepcopy(env_in))
|
||||
check("kernel " + name,
|
||||
json.dumps(got_k, sort_keys=True)
|
||||
== json.dumps(expected, sort_keys=True),
|
||||
f"got={json.dumps(got_k, sort_keys=True)[:200]} "
|
||||
f"want={json.dumps(expected, sort_keys=True)[:200]}")
|
||||
elif fn == "SalienceDecision.to_dict":
|
||||
recv = {k: v for k, v in case["recv"].items() if k != "cls"}
|
||||
got = oracle_mod.SalienceDecision(**copy.deepcopy(recv)).to_dict()
|
||||
check("facade " + name,
|
||||
json.dumps(got, sort_keys=True)
|
||||
== json.dumps(case["expected"]["result"], sort_keys=True),
|
||||
f"got={json.dumps(got, sort_keys=True)[:200]}")
|
||||
elif fn == "handle_hook":
|
||||
event = copy.deepcopy(case["args"][0])
|
||||
data = case["kwargs"].get("data")
|
||||
got = oracle_mod.handle_hook(
|
||||
event, copy.deepcopy(data) if data is not None else None)
|
||||
check("facade " + name,
|
||||
json.dumps(got, sort_keys=True)
|
||||
== json.dumps(case["expected"], sort_keys=True),
|
||||
f"got={json.dumps(got, sort_keys=True)[:200]}")
|
||||
elif fn == "_sg_core":
|
||||
# live facade: the probe ANSWERED under auto (rune.sg_loader);
|
||||
# the pinned None shape is the frozen row (battery-covered).
|
||||
got = oracle_mod._sg_core()
|
||||
check("facade-probe " + name,
|
||||
got is not None and getattr(got, "__name__", "") == "rune.sg_loader",
|
||||
f"got={got!r}")
|
||||
else:
|
||||
check(name, False, f"unhandled frozen fn {fn}")
|
||||
|
||||
print(f"replay: {total - failures}/{total} rows green (facade+kernel)")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if REPLAY:
|
||||
return replay()
|
||||
|
||||
# VERIFY_CASES come first: one freeze, one split — the two views can
|
||||
# never disagree about overlapping rows.
|
||||
all_cases = VERIFY_CASES + EXTRA_CASES
|
||||
rows = freeze_rows(all_cases)
|
||||
verify_rows = rows[: len(VERIFY_CASES)]
|
||||
|
||||
doc = {
|
||||
"_doc": (
|
||||
"FROZEN ORACLE TRUTH (salience_gate gap-close, dichotomy wave "
|
||||
"#2479). Frozen via scripts/gen_salience_gate_truth.py with "
|
||||
"RUNE_SALIENCE_CORE=python (shared harness) and SG_RUST_LIB "
|
||||
"poisoned — the pure-Python oracle floor, never a capture of the "
|
||||
"Rust kernel. Battery replays this VERBATIM "
|
||||
"(src/rust/salience_gate/src/branch_battery.rs, include_str!); "
|
||||
"the shared verify view is frozen_sg_truth.verify.json."
|
||||
),
|
||||
"frozen": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/salience_gate.py",
|
||||
"pin": "RUNE_SALIENCE_CORE=python + SG_RUST_LIB poisoned",
|
||||
"cases": rows,
|
||||
}
|
||||
ENVELOPE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
ENVELOPE_PATH.write_text(json.dumps(doc, indent=1, ensure_ascii=False) + "\n")
|
||||
VERIFY_PATH.write_text(
|
||||
json.dumps(verify_rows, indent=2, ensure_ascii=False) + "\n")
|
||||
print(f"frozen {len(rows)} cases → {ENVELOPE_PATH.relative_to(REPO)} "
|
||||
f"(verify view: {len(verify_rows)} order-stable rows)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,200 @@
|
||||
# Freeze rig for the model_profiles dichotomy gap-closing (dichotomy
|
||||
# wave #2479 gap port). Auto-imported on interpreter start
|
||||
# (PYTHONPATH=scripts/mp_freeze_seams:src) by BOTH the truth generator
|
||||
# (scripts/gen_model_profiles_truth.py) and the shared harness's
|
||||
# --verify mode, so frozen truth is byte-reproducible in any process:
|
||||
#
|
||||
# * HOME is pinned to MP_FREEZE_HOME (default /tmp/mp_gap_freeze_home)
|
||||
# BEFORE the oracle imports — PROFILES_DIR is derived from
|
||||
# expanduser("~") at IMPORT time (the work_manager law); the fixed
|
||||
# /tmp root keeps the frozen absolute paths byte-identical across
|
||||
# processes (the construction_interface CI_FREEZE_HOME precedent).
|
||||
# * The wall clock is pinned by the shared harness envelope
|
||||
# (RUNE_FROZEN_NOW patches the module-global `datetime` — the oracle
|
||||
# reads datetime.now(timezone.utc) INSIDE build_profile_from_ollama /
|
||||
# record_interaction), so created/last_used are constant instants.
|
||||
# * scan_all_models is networked (OLLAMA_URL/api/tags). The seam starts
|
||||
# a per-case canned http.server on an ephemeral port and repoints the
|
||||
# module-global OLLAMA_URL at it (the oracle reads the global at call
|
||||
# time); the "dead" fixture uses a refused port to freeze the
|
||||
# best-effort degrade branch (print + []).
|
||||
# * Every stateful seam WIPES the hermetic PROFILES_DIR and re-seeds it
|
||||
# from the case's `seed` argument first — each frozen row is
|
||||
# self-contained and order-independent.
|
||||
# * SEAM-NAMING LAW (the construction_interface NOTE precedent): the
|
||||
# oracle's own fns call each other through module globals
|
||||
# (scan_all_models → load_profile/build_profile_from_ollama/
|
||||
# save_profile; get_or_create_profile → same; record_interaction →
|
||||
# get_or_create_profile; get_prompt_context → get_or_create_profile).
|
||||
# Wrappers that change signature/behavior MUST NOT shadow those
|
||||
# names — every seam below is bound under a seam_* name, leaving the
|
||||
# oracle's internal call graph intact.
|
||||
# * JSON-safe wrapper seams (the memory_consolidation pattern):
|
||||
# dataclass instances return as asdict dicts, file effects return as
|
||||
# parsed docs (+ the RAW file bytes for save_profile — the
|
||||
# byte-parity corpus for the port's Python-shaped JSON writer), and
|
||||
# to_dict/from_dict run through ModelProfile so the harness can
|
||||
# freeze them JSON-natively.
|
||||
#
|
||||
# The module has NO loader seam (no RUNE_MODEL_PROFILES_CORE reader) —
|
||||
# the only pins are HOME + clock + the Ollama URL.
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from dataclasses import asdict
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
MP_FREEZE_HOME = os.environ.get("MP_FREEZE_HOME") or "/tmp/mp_gap_freeze_home"
|
||||
os.environ["HOME"] = MP_FREEZE_HOME
|
||||
|
||||
import rune.model_profiles as mp # noqa: E402 (AFTER the HOME pin)
|
||||
|
||||
|
||||
def _reset_store(seed=None):
|
||||
"""Wipe the hermetic PROFILES_DIR; re-seed {file name: doc-or-raw-str}."""
|
||||
shutil.rmtree(mp.PROFILES_DIR, ignore_errors=True)
|
||||
if seed:
|
||||
os.makedirs(mp.PROFILES_DIR, exist_ok=True)
|
||||
for fname, doc in seed.items():
|
||||
with open(os.path.join(mp.PROFILES_DIR, fname), "w") as f:
|
||||
if isinstance(doc, str):
|
||||
f.write(doc)
|
||||
else:
|
||||
json.dump(doc, f)
|
||||
|
||||
|
||||
def _store_files():
|
||||
"""Parsed docs of every .json file in the store, name-sorted."""
|
||||
if not os.path.isdir(mp.PROFILES_DIR):
|
||||
return {}
|
||||
out = {}
|
||||
for fname in sorted(os.listdir(mp.PROFILES_DIR)):
|
||||
if fname.endswith(".json"):
|
||||
with open(os.path.join(mp.PROFILES_DIR, fname)) as f:
|
||||
out[fname] = json.load(f)
|
||||
return out
|
||||
|
||||
|
||||
def _store_raw(name):
|
||||
path = mp.get_profile_path(name)
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return f.read()
|
||||
return None
|
||||
|
||||
|
||||
class _TagsHandler(BaseHTTPRequestHandler):
|
||||
payload = b"{}"
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(self.payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(self.payload)
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
def _canned_ollama(fixture):
|
||||
"""Start a canned /api/tags server; return (server, url)."""
|
||||
handler = type("H", (_TagsHandler,),
|
||||
{"payload": json.dumps(fixture).encode()})
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
return srv, f"http://127.0.0.1:{srv.server_address[1]}"
|
||||
|
||||
|
||||
# ── frozen seams (bound under seam_* names — see the header law) ──────
|
||||
|
||||
|
||||
def seam_get_profile_path(model_name):
|
||||
return mp.get_profile_path(model_name)
|
||||
|
||||
|
||||
def seam_load_profile(model_name, seed=None):
|
||||
_reset_store(seed)
|
||||
p = mp.load_profile(model_name)
|
||||
return asdict(p) if p is not None else None
|
||||
|
||||
|
||||
def seam_save_profile(state):
|
||||
"""Byte-parity corpus: save a from_dict-built profile, return the
|
||||
RAW file text (the port's JSON writer must reproduce it byte for
|
||||
byte) plus the parsed round-trip doc."""
|
||||
p = mp.ModelProfile.from_dict(state)
|
||||
_reset_store()
|
||||
mp.save_profile(p)
|
||||
return {"raw": _store_raw(p.name), "parsed": _store_files()}
|
||||
|
||||
|
||||
def seam_build_profile_from_ollama(model_name, model_info=None):
|
||||
return asdict(mp.build_profile_from_ollama(model_name, model_info))
|
||||
|
||||
|
||||
def seam_scan_all_models(fixture, seed=None):
|
||||
"""fixture: {"models": [...]} served from a canned server, or the
|
||||
string "dead" (refused port → the best-effort degrade branch)."""
|
||||
_reset_store(seed)
|
||||
saved_url, mp.OLLAMA_URL = mp.OLLAMA_URL, None
|
||||
srv = None
|
||||
try:
|
||||
if fixture == "dead":
|
||||
mp.OLLAMA_URL = "http://127.0.0.1:1"
|
||||
else:
|
||||
srv, mp.OLLAMA_URL = _canned_ollama(fixture)
|
||||
profiles = mp.scan_all_models()
|
||||
finally:
|
||||
mp.OLLAMA_URL = saved_url
|
||||
if srv is not None:
|
||||
srv.shutdown()
|
||||
return {"profiles": [asdict(p) for p in profiles],
|
||||
"files": _store_files()}
|
||||
|
||||
|
||||
def seam_get_or_create_profile(model_name, seed=None):
|
||||
_reset_store(seed)
|
||||
p = mp.get_or_create_profile(model_name)
|
||||
return {"profile": asdict(p), "files": _store_files()}
|
||||
|
||||
|
||||
def seam_record_interaction(model_name, outcomes, seed=None):
|
||||
"""outcomes: [[success, topic], ...] applied in order; returns the
|
||||
saved profile docs after the last call (state effect is the contract
|
||||
— the oracle fn returns None)."""
|
||||
_reset_store(seed)
|
||||
for success, topic in outcomes:
|
||||
mp.record_interaction(model_name, success, topic)
|
||||
return _store_files()
|
||||
|
||||
|
||||
def seam_get_prompt_context(model_name, seed=None):
|
||||
_reset_store(seed)
|
||||
return mp.get_prompt_context(model_name)
|
||||
|
||||
|
||||
def seam_to_dict(state):
|
||||
"""ModelProfile.to_dict via a from_dict-built instance."""
|
||||
return mp.ModelProfile.from_dict(state).to_dict()
|
||||
|
||||
|
||||
def seam_from_dict(data):
|
||||
return asdict(mp.ModelProfile.from_dict(data))
|
||||
|
||||
|
||||
# Bind the seams onto the oracle module under seam_* names so the
|
||||
# shared harness's getattr(module, fn) resolves every frozen fn name
|
||||
# WITHOUT shadowing the oracle's internal call graph.
|
||||
mp.seam_get_profile_path = seam_get_profile_path
|
||||
mp.seam_load_profile = seam_load_profile
|
||||
mp.seam_save_profile = seam_save_profile
|
||||
mp.seam_build_profile_from_ollama = seam_build_profile_from_ollama
|
||||
mp.seam_scan_all_models = seam_scan_all_models
|
||||
mp.seam_get_or_create_profile = seam_get_or_create_profile
|
||||
mp.seam_record_interaction = seam_record_interaction
|
||||
mp.seam_get_prompt_context = seam_get_prompt_context
|
||||
mp.seam_to_dict = seam_to_dict
|
||||
mp.seam_from_dict = seam_from_dict
|
||||
@@ -0,0 +1,173 @@
|
||||
# Freeze rig for the parallel_processor dichotomy gap-closing (dichotomy
|
||||
# wave #2479 gap port, contract factory-gap-pp). Auto-imported on
|
||||
# interpreter start (PYTHONPATH=scripts/pp_freeze_seams:src) by BOTH the
|
||||
# truth generator (scripts/gen_parallel_processor_truth.py), the shared
|
||||
# harness's --verify mode, and the live-parity sweep, so frozen truth is
|
||||
# byte-reproducible in any process:
|
||||
#
|
||||
# * THREAD-STATE PIN: get_status() reports threading.active_count(), which
|
||||
# is process-random (spawned pool threads, harness state). Pinned to a
|
||||
# constant for every rig process; the Go side's matching seam
|
||||
# (PP_ACTIVE_THREADS) is pinned to the same value by the battery and
|
||||
# the parity sweep.
|
||||
# * SPEC SEAMS: callables are not JSON — truth rows carry the shared
|
||||
# kind/fn/value spec grammar, and these seams build REAL Python
|
||||
# callables/tuples from the specs before handing them to the oracle's
|
||||
# actual functions (module- and class-level paths both exercised).
|
||||
# The Go battery rebuilds the same tasks from the same rows.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.join(_REPO, "scripts"))
|
||||
sys.path.insert(0, os.path.join(_REPO, "src"))
|
||||
|
||||
PP_FROZEN_ACTIVE_THREADS = 4
|
||||
threading.active_count = lambda: PP_FROZEN_ACTIVE_THREADS # noqa: E731
|
||||
|
||||
import rune.parallel_processor as _pp # noqa: E402 (after the pin)
|
||||
|
||||
|
||||
def _spec_func(spec):
|
||||
fn = spec["fn"]
|
||||
if fn == "const":
|
||||
ret = spec.get("ret")
|
||||
return lambda: ret
|
||||
if fn == "boom":
|
||||
msg = spec.get("msg", "boom")
|
||||
|
||||
def _boom():
|
||||
raise ValueError(msg)
|
||||
|
||||
return _boom
|
||||
if fn == "sleepret":
|
||||
ms = spec.get("ms", 0)
|
||||
ret = spec.get("ret")
|
||||
|
||||
def _sleepret():
|
||||
time.sleep(ms / 1000.0)
|
||||
return ret
|
||||
|
||||
return _sleepret
|
||||
if fn == "echo":
|
||||
|
||||
def _echo(*a, **k):
|
||||
return {"args": list(a), "kwargs": k}
|
||||
|
||||
return _echo
|
||||
if fn == "add":
|
||||
|
||||
def _add(a, b):
|
||||
return a + b
|
||||
|
||||
return _add
|
||||
raise ValueError(f"unknown spec fn {fn}")
|
||||
|
||||
|
||||
def _spec_zero(spec):
|
||||
"""A zero-arg task for run_parallel rows: callable specs become real
|
||||
callables; raw specs (and bare non-dict values) pass through as the
|
||||
task itself (the oracle's non-callable TypeError branch)."""
|
||||
if isinstance(spec, dict):
|
||||
if spec.get("kind") == "call":
|
||||
return _spec_func(spec)
|
||||
if "kind" in spec:
|
||||
return spec.get("value")
|
||||
return spec
|
||||
|
||||
|
||||
def _spec_task(spec):
|
||||
"""A (func, args, kwargs) task for run_parallel_with_args rows,
|
||||
including the oracle's parse-error branches. Bare non-dict values are
|
||||
raw tasks."""
|
||||
if not isinstance(spec, dict):
|
||||
return spec
|
||||
kind = spec.get("kind")
|
||||
if kind == "raw":
|
||||
return spec.get("value")
|
||||
if kind == "emptytuple":
|
||||
return ()
|
||||
if kind == "notcall":
|
||||
return (spec.get("value"),)
|
||||
if kind == "argsnotseq":
|
||||
# args is a non-sequence scalar -> the oracle wraps it: (args,)
|
||||
return (_spec_func(spec), spec.get("value"), {})
|
||||
if kind == "kwnotdict":
|
||||
# kwargs is not a dict -> the oracle resets it to {}
|
||||
return (_spec_func(spec), (), spec.get("value"))
|
||||
return (_spec_func(spec), tuple(spec.get("args", [])), spec.get("kwargs", {}))
|
||||
|
||||
|
||||
def _pp_run_parallel(specs):
|
||||
return _pp.run_parallel([_spec_zero(s) for s in specs])
|
||||
|
||||
|
||||
def _pp_run_parallel_with_args(specs):
|
||||
return _pp.run_parallel_with_args([_spec_task(s) for s in specs])
|
||||
|
||||
|
||||
def _pp_class_run_parallel(max_workers, specs):
|
||||
p = _pp.ParallelProcessor(max_workers)
|
||||
r = p.run_parallel([_spec_zero(s) for s in specs])
|
||||
p.shutdown(wait=True)
|
||||
return r
|
||||
|
||||
|
||||
def _pp_class_run_parallel_with_args(max_workers, specs):
|
||||
p = _pp.ParallelProcessor(max_workers)
|
||||
r = p.run_parallel_with_args([_spec_task(s) for s in specs])
|
||||
p.shutdown(wait=True)
|
||||
return r
|
||||
|
||||
|
||||
def _pp_get_processor_status():
|
||||
p = _pp.get_processor()
|
||||
return {"max_workers": p.max_workers, "is_singleton": _pp.get_processor() is p}
|
||||
|
||||
|
||||
def _pp_shutdown_then_status():
|
||||
p = _pp.ParallelProcessor(3)
|
||||
r = p.shutdown(wait=True)
|
||||
return {"shutdown_returned_none": r is None, "status": p.get_status()}
|
||||
|
||||
|
||||
def _pp_shutdown_then_run():
|
||||
p = _pp.ParallelProcessor(2)
|
||||
p.shutdown(wait=True)
|
||||
try:
|
||||
p.run_parallel([lambda: 1])
|
||||
return {"raised": None}
|
||||
except RuntimeError as e:
|
||||
return {"raised": str(e)}
|
||||
|
||||
|
||||
def _pp_new_bad():
|
||||
try:
|
||||
_pp.ParallelProcessor(0)
|
||||
return {"raised": None}
|
||||
except ValueError as e:
|
||||
return {"raised": str(e)}
|
||||
|
||||
|
||||
def _pp_shutdown_processor_cycle():
|
||||
_pp._shutdown_processor()
|
||||
was_none = _pp._processor is None
|
||||
st = _pp.get_status() # exercises the lazy re-create
|
||||
return {"processor_is_none": was_none, "recreated_max_workers": st["max_workers"]}
|
||||
|
||||
|
||||
_pp._pp_run_parallel = _pp_run_parallel
|
||||
_pp._pp_run_parallel_with_args = _pp_run_parallel_with_args
|
||||
_pp._pp_class_run_parallel = _pp_class_run_parallel
|
||||
_pp._pp_class_run_parallel_with_args = _pp_class_run_parallel_with_args
|
||||
_pp._pp_get_processor_status = _pp_get_processor_status
|
||||
_pp._pp_shutdown_then_status = _pp_shutdown_then_status
|
||||
_pp._pp_shutdown_then_run = _pp_shutdown_then_run
|
||||
_pp._pp_new_bad = _pp_new_bad
|
||||
_pp._pp_shutdown_processor_cycle = _pp_shutdown_processor_cycle
|
||||
# spec builders for the live-parity sweep (same grammar as truth rows)
|
||||
_pp._pp_spec_zero = _spec_zero
|
||||
_pp._pp_spec_task = _spec_task
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resume-close the #2479 dichotomy-wave salience_gate contract: the walk
|
||||
(BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg) reached `verifying`
|
||||
via transitions in one process; the QUALITY-GATE LAW (scaffold.py
|
||||
2026-10-13) requires the designed close() entry (predicted-vs-observed
|
||||
quality + material-change) rather than a raw transition to 'closed'.
|
||||
The phase lives in the store (durable and resumable by design) — this
|
||||
script completes the close on the persisted contract. Carries the
|
||||
already-pushed parity SHAs as evidence:
|
||||
feat 9539d5b6 close PARTIAL gaps — dichotomy wave
|
||||
phaseB 2350e02e Phase B language-advantage pass — dichotomy wave
|
||||
|
||||
Usage: resume_close_contract_salience_gate.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault(
|
||||
"CS_RUST_LIB",
|
||||
"/mnt/workdrive/rune/src/rust/capability_scoring/target/release/libcapability_scoring.so",
|
||||
)
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
CONTRACT = "BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg"
|
||||
PARITY_SHA = "9539d5b6"
|
||||
PHASEB_SHA = "2350e02e"
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-sg")
|
||||
|
||||
doc = iface.scaffold.load()
|
||||
e = iface.scaffold.contract(doc, CONTRACT)
|
||||
if e is None:
|
||||
print("CONTRACT MISSING — aborting")
|
||||
sys.exit(1)
|
||||
print("current phase:", e.get("phase"))
|
||||
if e.get("phase") != "verifying":
|
||||
print("unexpected phase — aborting (walk only continues from verifying)")
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(
|
||||
CONTRACT,
|
||||
observed_evidence=[
|
||||
{
|
||||
"kind": "port_evidence",
|
||||
"detail": (
|
||||
f"salience_gate RUST dichotomy gap-close — parity commit {PARITY_SHA}, "
|
||||
f"Phase B commit {PHASEB_SHA}; dichotomy audit fn coverage 1/5 "
|
||||
"PARTIAL -> 6/6 FULL (evaluate_salience_dict JSON view counted "
|
||||
"per the construction_decision normalize_need_json precedent; "
|
||||
"the module's delegation side is the existing Rust crate — no "
|
||||
"Go salience-gate dir exists). Ported name-preserving: "
|
||||
"_sg_core (kernel mirror of the facade availability probe, pin "
|
||||
"lattice), to_dict (SalienceDecision struct + projection), "
|
||||
"handle_hook (event echo, data accepted+ignored), "
|
||||
"test_salience (embedded oracle smoke, print-byte-matched). "
|
||||
"Three parity defects fixed (frozen-truth battery caught all): "
|
||||
"novel-family single-append (oracle guards ONE if — duplicate "
|
||||
"append inflated len(flags) and flipped the witness rule), "
|
||||
"failure-family doesn't-work/not-working arm omitted entirely, "
|
||||
"word_boundary_contains Python-\\b parity (alphanumeric + "
|
||||
"underscore + UTF-8 continuation bytes). Frozen truth via the "
|
||||
"shared scripts/freeze_truth.py harness (generator "
|
||||
"scripts/gen_salience_gate_truth.py, poisoned-truth law: "
|
||||
"RUNE_SALIENCE_CORE=python + SG_RUST_LIB poisoned — the "
|
||||
"facade evaluate path ignores RUNE_*_CORE): verify view 52 "
|
||||
"rows byte-identical under freeze_truth.py --verify in BOTH "
|
||||
"core states; envelope 63 rows incl. evaluation-order "
|
||||
"multi-family rows the kernel mirrors exactly "
|
||||
"(branch_battery.rs include_str replay, 12 tests). Live "
|
||||
"parity 100%: gen --replay facade live + kernel direct "
|
||||
"119/119; crate canary 6/6; oracle smoke green in 3 pin "
|
||||
"states (live / RUNE_SALIENCE_CORE=python / pure floor). "
|
||||
"rustfmt clean, clippy -D warnings clean, zero Go surface "
|
||||
"(git diff touches no src/go files). Evidence read back: "
|
||||
"verification/salience-gate-{freeze-verify,cargotest,clippy,"
|
||||
"replay,canary,python-pins,goladder,audit,close-walk}.log"
|
||||
),
|
||||
}
|
||||
],
|
||||
scope_files=[
|
||||
"src/rune/salience_gate.py",
|
||||
"src/rune/sg_loader.py",
|
||||
"src/rust/salience_gate/src/lib.rs",
|
||||
"src/rust/salience_gate/src/branch_battery.rs",
|
||||
"src/rust/salience_gate/src/frozen_sg_truth.json",
|
||||
"src/rust/salience_gate/src/frozen_sg_truth.verify.json",
|
||||
"src/rust/salience_gate/frozen_salience.json",
|
||||
"src/rust/salience_gate/canary_salience_gate.py",
|
||||
"src/rust/salience_gate/Cargo.toml",
|
||||
"scripts/gen_salience_gate_truth.py",
|
||||
"scripts/close_contract_salience_gate.py",
|
||||
"scripts/resume_close_contract_salience_gate.py",
|
||||
],
|
||||
)
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", CONTRACT, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
sys.exit(0 if c.get("success") else 1)
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open AND close the #2479 dichotomy-wave construction contract for
|
||||
model_profiles (Go gap-close) — the FULL lifecycle walk in ONE process
|
||||
(shared-store clobber hazard; transitions can take ~60s each)."""
|
||||
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 # noqa: E402
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-mp")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Close the PARTIAL gaps in src/go/model-profiles against the "
|
||||
"src/rune/model_profiles.py oracle (dichotomy wave, issue #2479 "
|
||||
"tranche): name-preserving Go ports for the 10 unmatched functions "
|
||||
"(get_profile_path — filesystem sanitization, load_profile — "
|
||||
"suppressed-exception floor + unknown-key filtering, save_profile — "
|
||||
"Python-shaped JSON writer, build_profile_from_ollama — knowledge "
|
||||
"merge + uncensored tier + frozen-clock stamps, scan_all_models — "
|
||||
"Ollama sweep with banker's rounding + learned-data preservation, "
|
||||
"get_or_create_profile, record_interaction — learning accumulator, "
|
||||
"get_prompt_context — operator string assembly, to_dict/from_dict — "
|
||||
"the dataclass lens) plus the stdin-op serve CLI over the same code "
|
||||
"paths, frozen-truth corpus generated against the live Python oracle "
|
||||
"through the scripts/mp_freeze_seams rig (HOME pinned before import, "
|
||||
"per-case hermetic store wipe+seed, canned Ollama server, frozen "
|
||||
"clock; 36 verbatim cases incl. the save_profile byte-parity rows), "
|
||||
"battery 36/36, gofmt+vet clean, live parity 100% (143/143), pytest "
|
||||
"31/31 both pins.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:300])
|
||||
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)[:220])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "model_profiles dichotomy completion — parity "
|
||||
"1ca647e4 feat(model_profiles): close PARTIAL gaps — "
|
||||
"dichotomy wave; pushed gitea/master. Go port at "
|
||||
"src/go/model-profiles closes the audit gaps "
|
||||
"name-preserving (get_profile_path, load_profile, "
|
||||
"save_profile, build_profile_from_ollama, scan_all_models, "
|
||||
"get_or_create_profile, record_interaction, "
|
||||
"get_prompt_context, to_dict/from_dict) plus the stdin-op "
|
||||
"serve CLI over the same code paths. Freeze rig scripts/"
|
||||
"mp_freeze_seams + generator: 36-case frozen truth "
|
||||
"regenerates byte-identical across runs; freeze_truth "
|
||||
"--verify OK against the live oracle. Battery 36/36 replay "
|
||||
"VERBATIM; parity scripts/check_model_profiles_parity.py "
|
||||
"143/143 LIVE; gofmt+vet+build clean; pytest 31/31 both "
|
||||
"pins. Audit 5/15 → 15/15 FULL. Divergences (documented): "
|
||||
"non-string profile names accepted by the oracle dataclass "
|
||||
"but rejected by the typed port (local_tool_runtime "
|
||||
"precedent); integral-float rendering canonicalized in the "
|
||||
"sweep (context_broker precedent).",
|
||||
}], scope_files=[
|
||||
"src/go/model-profiles/main.go",
|
||||
"src/go/model-profiles/frozen_battery_test.go",
|
||||
"src/go/model-profiles/frozen_mp_truth.json",
|
||||
"src/go/model-profiles/go.mod",
|
||||
"src/go/model-profiles/truth.json",
|
||||
"scripts/mp_freeze_seams/sitecustomize.py",
|
||||
"scripts/gen_model_profiles_truth.py",
|
||||
"scripts/gen_mp_battery.py",
|
||||
"scripts/check_model_profiles_parity.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
sys.exit(0 if c.get("success") else 1)
|
||||
+866
-42
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,704 @@
|
||||
// scenario.go — gap-close parity machinery, shared by the CI binary
|
||||
// (`replay-gap`, `scenario`) and the battery (truth_gap_test.go).
|
||||
//
|
||||
// truth_gap.json rows are frozen against the live oracle through wrapper
|
||||
// shims (scripts/gen_cron_engine_gap_truth.py, _ce_* functions). Every
|
||||
// wrapper takes the engine state as an explicit leading JSON argument,
|
||||
// runs against a HERMETIC per-row home (frozen clock; time.sleep patched
|
||||
// to stop the loop for _run_loop rows), and captures
|
||||
// {result, post, files, storage, log}. replayGap mirrors each wrapper
|
||||
// against THIS port and compares the capture after substituting the
|
||||
// row's actual home for the "<CE_HOME>" placeholder token (dichotomy
|
||||
// law: no absolute paths in frozen truth).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const ceHomeToken = "<CE_HOME>"
|
||||
|
||||
// ordObj is a JSON object that remembers its key ORDER (Go maps do not;
|
||||
// frozen truth's task dicts are insertion-ordered like Python's).
|
||||
type ordObj struct {
|
||||
keys []string
|
||||
m map[string]any
|
||||
}
|
||||
|
||||
func (o *ordObj) get(k string) (any, bool) {
|
||||
v, ok := o.m[k]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// decodeArg decodes one truth-row argument preserving object key order.
|
||||
func decodeArg(raw json.RawMessage) (any, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
v, err := decodeArgValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeArgValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
o := &ordObj{m: map[string]any{}}
|
||||
for dec.More() {
|
||||
kt, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := kt.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bad object key %v", kt)
|
||||
}
|
||||
val, err := decodeArgValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, exists := o.m[key]; !exists {
|
||||
o.keys = append(o.keys, key)
|
||||
}
|
||||
o.m[key] = val
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // '}'
|
||||
return nil, err
|
||||
}
|
||||
return o, nil
|
||||
case '[':
|
||||
var arr []any
|
||||
for dec.More() {
|
||||
val, err := decodeArgValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // ']'
|
||||
return nil, err
|
||||
}
|
||||
if arr == nil {
|
||||
arr = []any{}
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
case json.Number:
|
||||
f, err := t.Float64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
default:
|
||||
return tok, nil // string / bool / nil
|
||||
}
|
||||
}
|
||||
|
||||
// stateOf extracts the ordObj at args[i] (wrapper state argument).
|
||||
func stateOf(args []any, i int) *ordObj {
|
||||
if i < len(args) {
|
||||
if o, ok := args[i].(*ordObj); ok {
|
||||
return o
|
||||
}
|
||||
}
|
||||
return &ordObj{m: map[string]any{}}
|
||||
}
|
||||
|
||||
func readStrPtr(m map[string]any, k string) *string {
|
||||
if v, ok := m[k].(string); ok {
|
||||
return &v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func taskFromMap(m map[string]any) *CronTask {
|
||||
t := &CronTask{
|
||||
Name: strOf(m, "name"),
|
||||
CronExpression: strOf(m, "cron_expression"),
|
||||
Command: strOf(m, "command"),
|
||||
Description: strOf(m, "description"),
|
||||
Enabled: true, // dataclass default: enabled=True
|
||||
LastRun: readStrPtr(m, "last_run"),
|
||||
NextRun: readStrPtr(m, "next_run"),
|
||||
RunCount: intOf(m, "run_count"),
|
||||
}
|
||||
if v, ok := m["enabled"].(bool); ok {
|
||||
t.Enabled = v
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func strOf(m map[string]any, k string) string {
|
||||
s, _ := m[k].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func boolOf(m map[string]any, k string) bool {
|
||||
b, _ := m[k].(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
func intOf(m map[string]any, k string) int {
|
||||
f, _ := m[k].(float64)
|
||||
return int(f)
|
||||
}
|
||||
|
||||
// buildGapEngine reconstructs the engine from a wrapper state arg, with
|
||||
// paths tokenized against the row's hermetic home.
|
||||
func buildGapEngine(st *ordObj, home string) (*CronEngine, error) {
|
||||
e := newCronEngine()
|
||||
if sp, ok := st.get("storage_path"); ok {
|
||||
e.storagePath = strings.ReplaceAll(asStr(sp), ceHomeToken, home)
|
||||
}
|
||||
e.logPath = filepath.Join(home, ".rune", "cron.log")
|
||||
if r, ok := st.get("running"); ok {
|
||||
e.running = asBool(r)
|
||||
}
|
||||
e.lastRunMin = map[string]string{}
|
||||
if v, ok := st.get("_last_run_minute"); ok {
|
||||
if lrm, ok2 := v.(*ordObj); ok2 {
|
||||
for k, v := range lrm.m {
|
||||
e.lastRunMin[k] = asStr(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := st.get("tasks"); ok {
|
||||
tasks, _ := v.(*ordObj)
|
||||
if tasks != nil {
|
||||
for _, name := range tasks.keys {
|
||||
tj, ok := tasks.get(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tm, _ := tj.(*ordObj)
|
||||
if tm == nil {
|
||||
continue
|
||||
}
|
||||
e.Tasks[name] = taskFromMap(tm.m)
|
||||
e.insertOrder(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// optional log seeding (rotation rows): filename -> size, filled b"x"
|
||||
if v, ok := st.get("seed_log"); ok {
|
||||
if seed, ok2 := v.(*ordObj); ok2 {
|
||||
dir := filepath.Dir(e.logPath)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := append([]string{}, seed.keys...)
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
sz := int64(0)
|
||||
if f, ok := seed.m[name].(float64); ok {
|
||||
sz = int64(f)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, name), bytes.Repeat([]byte("x"), int(sz)), 0o644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func asStr(v any) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func asBool(v any) bool {
|
||||
b, _ := v.(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
func readFileOrNone(p string) *string {
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
s := string(b)
|
||||
return &s
|
||||
}
|
||||
|
||||
func dirSizes(dir string) map[string]int64 {
|
||||
out := map[string]int64{}
|
||||
ents, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
for _, ent := range ents {
|
||||
if ent.IsDir() {
|
||||
continue
|
||||
}
|
||||
if info, err := ent.Info(); err == nil {
|
||||
out[ent.Name()] = info.Size()
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func captureEngine(e *CronEngine, result any, home string) map[string]any {
|
||||
pairs := [][2]any{}
|
||||
for _, t := range e.snapshotTasks() {
|
||||
pairs = append(pairs, [2]any{t.Name, t.toDict()})
|
||||
}
|
||||
lrm := map[string]string{}
|
||||
for k, v := range e.lastRunMin {
|
||||
lrm[k] = v
|
||||
}
|
||||
files := map[string]int64{}
|
||||
if e.logPath != "" {
|
||||
files = dirSizes(filepath.Dir(e.logPath))
|
||||
}
|
||||
return map[string]any{
|
||||
"result": result,
|
||||
"post": map[string]any{
|
||||
"running": e.running,
|
||||
"storage_path": strings.ReplaceAll(e.storagePath, home, ceHomeToken),
|
||||
"tasks": pairs,
|
||||
"_last_run_minute": lrm,
|
||||
},
|
||||
"files": files,
|
||||
"storage": readFileOrNone(e.storagePath),
|
||||
"log": readFileOrNone(e.logPath),
|
||||
}
|
||||
}
|
||||
|
||||
// withLoopSleep patches sleepFn to flip THIS engine's running flag on the
|
||||
// first sleep — the exact patch the oracle wrapper applies to
|
||||
// cron_engine.time.sleep for _run_loop rows (one iteration, then exit).
|
||||
func withLoopSleep(e *CronEngine, fn func()) {
|
||||
old := sleepFn
|
||||
sleepFn = func(time.Duration) { e.mu.Lock(); e.running = false; e.mu.Unlock() }
|
||||
defer func() { sleepFn = old }()
|
||||
fn()
|
||||
}
|
||||
|
||||
// execGapRow interprets one _ce_* wrapper row against the port. Pure
|
||||
// rows return their bare result; engine rows return the full capture
|
||||
// map; the start_stop singleton additionally returns an "extra" merge.
|
||||
// args are decodeArg values (objects = *ordObj, order-preserving).
|
||||
func execGapRow(fn string, args []any, home string) (result any, extra map[string]any, err error) {
|
||||
st := stateOf(args, 0)
|
||||
str := func(i int) string {
|
||||
if i < len(args) {
|
||||
return asStr(args[i])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
num := func(i int) int {
|
||||
if i < len(args) {
|
||||
if f, ok := args[i].(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
switch fn {
|
||||
case "_ce_to_dict":
|
||||
m := stateOf(args, 0).m
|
||||
return taskFromMap(m).toDict(), nil, nil
|
||||
case "_ce_parse":
|
||||
vals, perr := parseCronField(str(0), num(1), num(2))
|
||||
if perr != nil {
|
||||
return nil, nil, perr
|
||||
}
|
||||
out := []int{}
|
||||
for v := range vals {
|
||||
out = append(out, v)
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out, nil, nil
|
||||
case "_ce_should_run":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
name := str(1)
|
||||
now := time.Date(num(2), time.Month(num(3)), num(4), num(5), num(6), 0, 0, time.UTC)
|
||||
t := e.Tasks[name]
|
||||
ok, serr := shouldRun(t.CronExpression, now, e.lastRunMin[t.Name])
|
||||
if serr != nil {
|
||||
return nil, nil, serr
|
||||
}
|
||||
return ok, nil, nil
|
||||
case "_ce_run_task":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
return captureEngine(e, e.runTask(str(1)), home), nil, nil
|
||||
case "_ce_fire":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
return captureEngine(e, e.fireOnTaskComplete(), home), nil, nil
|
||||
case "_ce_stop":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
e.stop()
|
||||
return captureEngine(e, nil, home), nil, nil
|
||||
case "_ce_execute":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
if xerr := e.executeTask(e.Tasks[str(1)]); xerr != nil {
|
||||
return nil, nil, xerr
|
||||
}
|
||||
return captureEngine(e, nil, home), nil, nil
|
||||
case "_ce_run_loop":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
withLoopSleep(e, func() { e.runLoop() })
|
||||
return captureEngine(e, nil, home), nil, nil
|
||||
case "_ce_log_execution":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
if lerr := e.logExecution(e.Tasks[str(1)], num(2)); lerr != nil {
|
||||
return nil, nil, lerr
|
||||
}
|
||||
return captureEngine(e, nil, home), nil, nil
|
||||
case "_ce_log_error":
|
||||
e, berr := buildGapEngine(st, home)
|
||||
if berr != nil {
|
||||
return nil, nil, berr
|
||||
}
|
||||
if lerr := e.logError(e.Tasks[str(1)], str(2)); lerr != nil {
|
||||
return nil, nil, lerr
|
||||
}
|
||||
return captureEngine(e, nil, home), nil, nil
|
||||
case "_ce_singleton":
|
||||
return execSingletonRow(str(0), str(1), str(2), str(3), home)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown gap fn %q", fn)
|
||||
}
|
||||
}
|
||||
|
||||
// execSingletonRow mirrors the singleton wrapper family: reset the
|
||||
// global, hermetic HOME (default paths derive from it). start_stop uses
|
||||
// the REAL sleeper — the loop thread genuinely blocks in its 60s sleep,
|
||||
// so the mid-capture (running=true) is race-free and stop's 5s join
|
||||
// timeout is exercised, exactly like the oracle wrapper.
|
||||
func execSingletonRow(op, name, expr, command, home string) (any, map[string]any, error) {
|
||||
oldHome, hadHome := os.LookupEnv("HOME")
|
||||
oldGlobal := globalCronEngine
|
||||
defer func() {
|
||||
if hadHome {
|
||||
os.Setenv("HOME", oldHome)
|
||||
} else {
|
||||
os.Unsetenv("HOME")
|
||||
}
|
||||
globalCronEngine = oldGlobal
|
||||
}()
|
||||
os.Setenv("HOME", home)
|
||||
globalCronEngine = nil
|
||||
|
||||
switch op {
|
||||
case "get":
|
||||
e := getCronEngine()
|
||||
return captureEngine(e, len(e.listTasks()), home), nil, nil
|
||||
case "start_stop":
|
||||
startEngine()
|
||||
mid := captureEngine(getCronEngine(), nil, home)
|
||||
stopEngine() // 5s join timeout — the loop thread sleeps for real
|
||||
end := captureEngine(getCronEngine(), nil, home)
|
||||
return end, map[string]any{"extra": mid}, nil
|
||||
case "module_run":
|
||||
e := getCronEngine()
|
||||
e.addTask(name, expr, command, "")
|
||||
r := runTask(name) // module-level run_task via the global
|
||||
return captureEngine(getCronEngine(), r, home), nil, nil
|
||||
case "module_list":
|
||||
e := getCronEngine()
|
||||
e.addTask(name, expr, command, "d1")
|
||||
e.addTask(name+"-2", "*/5 * * * *", "echo two", "d2")
|
||||
names := []string{}
|
||||
for _, t := range moduleListTasks() {
|
||||
names = append(names, t.Name)
|
||||
}
|
||||
return captureEngine(e, names, home), nil, nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown singleton op %q", op)
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeCapture substitutes the row's hermetic home for the token in
|
||||
// every string of the frozen expectation so comparison is
|
||||
// machine-independent.
|
||||
func normalizeCapture(v any, home string) any {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return strings.ReplaceAll(x, ceHomeToken, home)
|
||||
case map[string]any:
|
||||
out := map[string]any{}
|
||||
for k, vv := range x {
|
||||
out[k] = normalizeCapture(vv, home)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(x))
|
||||
for i, vv := range x {
|
||||
out[i] = normalizeCapture(vv, home)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func toAny(v any) any {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return v
|
||||
}
|
||||
var out any
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
return v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type gapTruthRow struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []json.RawMessage `json:"args"`
|
||||
Kwargs any `json:"kwargs"`
|
||||
Expected any `json:"expected"`
|
||||
}
|
||||
|
||||
// replayGapBytes replays a truth_gap envelope; returns first divergence.
|
||||
func replayGapBytes(raw []byte) error {
|
||||
var env struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []gapTruthRow `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &env); err != nil {
|
||||
return err
|
||||
}
|
||||
frozen, err := time.Parse("2006-01-02T15:04:05", env.FrozenNow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad frozen_now %q: %v", env.FrozenNow, err)
|
||||
}
|
||||
oldNow, oldSleep, oldHome, hadHome := nowUTC, sleepFn, "", false
|
||||
if h, ok := os.LookupEnv("HOME"); ok {
|
||||
oldHome, hadHome = h, true
|
||||
}
|
||||
defer func() {
|
||||
nowUTC, sleepFn = oldNow, oldSleep
|
||||
if hadHome {
|
||||
os.Setenv("HOME", oldHome)
|
||||
}
|
||||
}()
|
||||
nowUTC = func() time.Time { return frozen.UTC() }
|
||||
|
||||
fails := 0
|
||||
for i, row := range env.Cases {
|
||||
home, err := os.MkdirTemp("", "ce-gap-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := make([]any, len(row.Args))
|
||||
for j, raw := range row.Args {
|
||||
args[j], err = decodeArg(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("case %d: bad arg %d: %v", i, j, err)
|
||||
}
|
||||
}
|
||||
got, extra, err := execGapRow(row.Fn, args, home)
|
||||
if err != nil {
|
||||
fails++
|
||||
fmt.Printf("case %d %s: ERROR %v\n", i, row.Fn, err)
|
||||
os.RemoveAll(home)
|
||||
continue
|
||||
}
|
||||
gotAny := toAny(got)
|
||||
if extra != nil {
|
||||
if m, ok := gotAny.(map[string]any); ok {
|
||||
merged := map[string]any{}
|
||||
for k, v := range m {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range extra {
|
||||
merged[k] = toAny(v)
|
||||
}
|
||||
gotAny = merged
|
||||
}
|
||||
}
|
||||
gotAny = normalizeCapture(gotAny, home)
|
||||
want := normalizeCapture(row.Expected, home)
|
||||
if !reflect.DeepEqual(gotAny, want) {
|
||||
fails++
|
||||
gb, _ := json.MarshalIndent(gotAny, "", " ")
|
||||
wb, _ := json.MarshalIndent(want, "", " ")
|
||||
fmt.Printf("case %d %s DIVERGE\ngot: %s\nwant: %s\n", i, row.Fn, gb, wb)
|
||||
}
|
||||
os.RemoveAll(home)
|
||||
}
|
||||
if fails > 0 {
|
||||
return fmt.Errorf("PARITY FAILED: %d divergence(s)", fails)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d gap cases exact-equal vs the Python oracle\n", len(env.Cases))
|
||||
return nil
|
||||
}
|
||||
|
||||
func replayGap(path string) error {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return replayGapBytes(raw)
|
||||
}
|
||||
|
||||
// ── scenario driver (live parity: same ops against the Python oracle
|
||||
// and this port, JSON outputs diffed by check_cron_engine_parity.py) ──
|
||||
|
||||
type scenarioOp struct {
|
||||
Op string `json:"op"`
|
||||
Name string `json:"name"`
|
||||
Expr string `json:"expr"`
|
||||
Command string `json:"command"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type scenarioFile struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
StoragePath string `json:"storage_path"`
|
||||
LogPath string `json:"log_path"`
|
||||
Ops []scenarioOp `json:"ops"`
|
||||
}
|
||||
|
||||
type taskPair struct {
|
||||
Name string `json:"name"`
|
||||
Task *CronTask `json:"task"`
|
||||
}
|
||||
|
||||
type stateDump struct {
|
||||
Running bool `json:"running"`
|
||||
StoragePath string `json:"storage_path"`
|
||||
LogPath string `json:"log_path"`
|
||||
Tasks []taskPair `json:"tasks"`
|
||||
LastRunMin map[string]string `json:"_last_run_minute"`
|
||||
}
|
||||
|
||||
func dumpState(e *CronEngine) stateDump {
|
||||
tasks := []taskPair{}
|
||||
for _, t := range e.snapshotTasks() {
|
||||
tasks = append(tasks, taskPair{Name: t.Name, Task: t.toDict()})
|
||||
}
|
||||
lrm := map[string]string{}
|
||||
for k, v := range e.lastRunMin {
|
||||
lrm[k] = v
|
||||
}
|
||||
return stateDump{
|
||||
Running: e.running,
|
||||
StoragePath: e.storagePath,
|
||||
LogPath: e.logPath,
|
||||
Tasks: tasks,
|
||||
LastRunMin: lrm,
|
||||
}
|
||||
}
|
||||
|
||||
func runScenario(path string) error {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sc scenarioFile
|
||||
if err := json.Unmarshal(raw, &sc); err != nil {
|
||||
return err
|
||||
}
|
||||
home, err := os.MkdirTemp("", "ce-scenario-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(home)
|
||||
if sc.StoragePath == "" {
|
||||
sc.StoragePath = "<HOME>/.rune/cron.json"
|
||||
}
|
||||
if sc.LogPath == "" {
|
||||
sc.LogPath = "<HOME>/.rune/cron.log"
|
||||
}
|
||||
storage := strings.ReplaceAll(sc.StoragePath, "<HOME>", home)
|
||||
logP := strings.ReplaceAll(sc.LogPath, "<HOME>", home)
|
||||
|
||||
frozen, err := time.Parse("2006-01-02T15:04:05", sc.FrozenNow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad frozen_now: %v", err)
|
||||
}
|
||||
oldNow, oldSleep := nowUTC, sleepFn
|
||||
defer func() { nowUTC, sleepFn = oldNow, oldSleep }()
|
||||
nowUTC = func() time.Time { return frozen.UTC() }
|
||||
|
||||
e := newCronEngineAt(storage, logP)
|
||||
|
||||
steps := []any{}
|
||||
for _, op := range sc.Ops {
|
||||
switch op.Op {
|
||||
case "add":
|
||||
t := e.addTask(op.Name, op.Expr, op.Command, op.Description)
|
||||
steps = append(steps, map[string]any{"op": "add", "task": t.toDict()})
|
||||
case "enable":
|
||||
steps = append(steps, map[string]any{"op": "enable", "ok": e.enableTask(op.Name)})
|
||||
case "disable":
|
||||
steps = append(steps, map[string]any{"op": "disable", "ok": e.disableTask(op.Name)})
|
||||
case "remove":
|
||||
steps = append(steps, map[string]any{"op": "remove", "ok": e.removeTask(op.Name)})
|
||||
case "run":
|
||||
steps = append(steps, map[string]any{"op": "run", "result": e.runTask(op.Name)})
|
||||
case "fire":
|
||||
steps = append(steps, map[string]any{"op": "fire", "fired": e.fireOnTaskComplete()})
|
||||
case "loop":
|
||||
withLoopSleep(e, func() { e.runLoop() })
|
||||
steps = append(steps, map[string]any{"op": "loop"})
|
||||
case "start":
|
||||
withLoopSleep(e, func() { e.start(); e.stop() })
|
||||
steps = append(steps, map[string]any{"op": "start+stop"})
|
||||
case "stop":
|
||||
e.stop()
|
||||
steps = append(steps, map[string]any{"op": "stop"})
|
||||
case "save":
|
||||
err := e.saveTo(e.storagePath)
|
||||
steps = append(steps, map[string]any{"op": "save", "ok": err == nil})
|
||||
case "load":
|
||||
err := e.loadFrom(e.storagePath)
|
||||
steps = append(steps, map[string]any{"op": "load", "ok": err == nil})
|
||||
case "storage":
|
||||
steps = append(steps, map[string]any{"op": "storage", "bytes": readFileOrNone(e.storagePath)})
|
||||
case "log":
|
||||
steps = append(steps, map[string]any{"op": "log", "bytes": readFileOrNone(e.logPath)})
|
||||
case "state":
|
||||
steps = append(steps, map[string]any{"op": "state", "state": dumpState(e)})
|
||||
default:
|
||||
return fmt.Errorf("unknown scenario op %q", op.Op)
|
||||
}
|
||||
}
|
||||
out := map[string]any{"steps": steps, "final": dumpState(e)}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
// truth_gap_test.go — gap-close battery: replays truth_gap.json (frozen
|
||||
// against the live Python oracle through scripts/gen_cron_engine_gap_truth.py
|
||||
// wrappers) and pins the error-message shapes that are too slow to freeze
|
||||
// through the oracle's hardcoded 60s subprocess timeout (documented
|
||||
// divergences carry the exact oracle strings; see verification/ce-*).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGapParity(t *testing.T) {
|
||||
raw, err := os.ReadFile("truth_gap.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth missing: %v", err)
|
||||
}
|
||||
if err := replayGapBytes(raw); err != nil {
|
||||
t.Fatalf("gap parity: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOracleErrorShapes pins str(e) forms the oracle produces but that
|
||||
// frozen truth cannot capture cheaply (a real 60s TimeoutExpired wait).
|
||||
func TestOracleErrorShapes(t *testing.T) {
|
||||
if got, want := pyTimeoutExpired([]string{"sleep", "65"}, 60).Error(),
|
||||
"Command '['sleep', '65']' timed out after 60 seconds"; got != want {
|
||||
t.Errorf("timeout msg: got %q want %q", got, want)
|
||||
}
|
||||
if _, err := parseCronField("*/0", 0, 59); err == nil || err.Error() != "range() arg 3 must not be zero" {
|
||||
t.Errorf("step-0 error: got %v", err)
|
||||
}
|
||||
if _, err := shlexSplit("echo 'open"); err == nil || err.Error() != "No closing quotation" {
|
||||
t.Errorf("shlex quote error: got %v", err)
|
||||
}
|
||||
if _, err := shlexSplit(`echo \`); err == nil || err.Error() != "No escaped character" {
|
||||
t.Errorf("shlex escape error: got %v", err)
|
||||
}
|
||||
if got := pyISO(time.Date(2026, 10, 13, 9, 20, 0, 0, time.UTC)); got != "2026-10-13T09:20:00+00:00" {
|
||||
t.Errorf("pyISO: got %q", got)
|
||||
}
|
||||
if got := pyISO(time.Date(2026, 10, 13, 9, 20, 0, 123456000, time.UTC)); got != "2026-10-13T09:20:00.123456+00:00" {
|
||||
t.Errorf("pyISO us: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTimeoutBranchReplay exercises the timeout path through the seam at
|
||||
// a human timescale and checks the envelope carries the oracle's
|
||||
// TimeoutExpired str(e) verbatim.
|
||||
func TestTimeoutBranchReplay(t *testing.T) {
|
||||
oldT := execTimeout
|
||||
defer func() { execTimeout = oldT }()
|
||||
execTimeout = 300 * time.Millisecond
|
||||
e := newCronEngineAt("/tmp/ce-timeout-test/cron.json", "/tmp/ce-timeout-test/cron.log")
|
||||
e.addTask("slow", "*/5 * * * *", "sleep 5", "")
|
||||
r := e.runTask("slow")
|
||||
errEnv, ok := r.(runTaskErr)
|
||||
if !ok {
|
||||
t.Fatalf("want error envelope, got %#v", r)
|
||||
}
|
||||
want := "Command '['sleep', '5']' timed out after 0 seconds"
|
||||
if errEnv.Error != want {
|
||||
t.Errorf("timeout envelope: got %q want %q", errEnv.Error, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderedSaveBytes proves _save's insertion-order + ensure_ascii
|
||||
// rendering byte-parity against a Python-produced fixture.
|
||||
func TestOrderedSaveBytes(t *testing.T) {
|
||||
e := newCronEngine()
|
||||
e.addTask("b-second", "*/5 * * * *", "echo b", "plain")
|
||||
e.addTask("a-first", "on_complete", "echo '中文 <ok>'", "desc ünïcode")
|
||||
ls := "2026-10-13T09:20:00+00:00"
|
||||
e.Tasks["b-second"].RunCount = 3
|
||||
e.Tasks["b-second"].LastRun = &ls
|
||||
dir := t.TempDir()
|
||||
p := dir + "/cron.json"
|
||||
if err := e.saveTo(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _ := os.ReadFile(p)
|
||||
want := "{\n" +
|
||||
" \"b-second\": {\n" +
|
||||
" \"name\": \"b-second\",\n" +
|
||||
" \"cron_expression\": \"*/5 * * * *\",\n" +
|
||||
" \"command\": \"echo b\",\n" +
|
||||
" \"description\": \"plain\",\n" +
|
||||
" \"enabled\": true,\n" +
|
||||
" \"last_run\": \"2026-10-13T09:20:00+00:00\",\n" +
|
||||
" \"next_run\": null,\n" +
|
||||
" \"run_count\": 3\n" +
|
||||
" },\n" +
|
||||
" \"a-first\": {\n" +
|
||||
" \"name\": \"a-first\",\n" +
|
||||
" \"cron_expression\": \"on_complete\",\n" +
|
||||
" \"command\": \"echo '\\u4e2d\\u6587 <ok>'\",\n" +
|
||||
" \"description\": \"desc \\u00fcn\\u00efcode\",\n" +
|
||||
" \"enabled\": true,\n" +
|
||||
" \"last_run\": null,\n" +
|
||||
" \"next_run\": null,\n" +
|
||||
" \"run_count\": 0\n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
if string(got) != want {
|
||||
t.Errorf("save bytes:\ngot: %q\nwant: %q", string(got), want)
|
||||
}
|
||||
// round-trip preserves insertion order
|
||||
e2 := newCronEngine()
|
||||
if err := e2.loadFrom(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names := []string{}
|
||||
for _, tsk := range e2.snapshotTasks() {
|
||||
names = append(names, tsk.Name)
|
||||
}
|
||||
if len(names) != 2 || names[0] != "b-second" || names[1] != "a-first" {
|
||||
t.Errorf("load order: got %v", names)
|
||||
}
|
||||
}
|
||||
+67
-19
@@ -36,12 +36,22 @@ type PluginEntry struct {
|
||||
Manifest PluginManifest `json:"manifest"`
|
||||
RegisteredAt string `json:"registered_at"`
|
||||
Active bool `json:"active"`
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
// FireLog — one fire() audit entry (registry["log"], last 100 kept).
|
||||
type FireLog struct {
|
||||
Event string `json:"event"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
HooksFired int `json:"hooks_fired"`
|
||||
DurationMs float64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
Plugins map[string]PluginEntry `json:"plugins"`
|
||||
Capabilities map[string][]string `json:"capabilities"`
|
||||
Hooks map[string][]string `json:"hooks"`
|
||||
Log []FireLog `json:"log"`
|
||||
}
|
||||
|
||||
func registryPath() string {
|
||||
@@ -52,21 +62,43 @@ func registryPath() string {
|
||||
return filepath.Join(home, "microkernel_registry.json")
|
||||
}
|
||||
|
||||
func loadRegistry() Registry {
|
||||
r := Registry{
|
||||
func emptyRegistry() Registry {
|
||||
return Registry{
|
||||
Plugins: make(map[string]PluginEntry),
|
||||
Capabilities: make(map[string][]string),
|
||||
Hooks: make(map[string][]string),
|
||||
Log: []FireLog{},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadRegistry — the oracle's _load_registry(): missing OR corrupt file
|
||||
// yields the full default shape (plugins/capabilities/hooks/log all empty).
|
||||
func LoadRegistry() Registry {
|
||||
r := emptyRegistry()
|
||||
data, err := os.ReadFile(registryPath())
|
||||
if err != nil {
|
||||
return r
|
||||
}
|
||||
json.Unmarshal(data, &r)
|
||||
if json.Unmarshal(data, &r) != nil {
|
||||
return emptyRegistry()
|
||||
}
|
||||
if r.Plugins == nil {
|
||||
r.Plugins = make(map[string]PluginEntry)
|
||||
}
|
||||
if r.Capabilities == nil {
|
||||
r.Capabilities = make(map[string][]string)
|
||||
}
|
||||
if r.Hooks == nil {
|
||||
r.Hooks = make(map[string][]string)
|
||||
}
|
||||
if r.Log == nil {
|
||||
r.Log = []FireLog{}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func saveRegistry(r Registry) {
|
||||
// SaveRegistry — the oracle's _save_registry(): indent-2 JSON.
|
||||
func SaveRegistry(r Registry) {
|
||||
os.MkdirAll(filepath.Dir(registryPath()), 0755)
|
||||
data, _ := json.MarshalIndent(r, "", " ")
|
||||
os.WriteFile(registryPath(), data, 0644)
|
||||
@@ -74,10 +106,10 @@ func saveRegistry(r Registry) {
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: microkernel <register|unregister|resolve|fire|status> [args...]")
|
||||
fmt.Fprintln(os.Stderr, "usage: microkernel <register|unregister|resolve|fire|discover|status|parity> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
registry := loadRegistry()
|
||||
registry := LoadRegistry()
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
@@ -106,7 +138,7 @@ func main() {
|
||||
for event := range manifest.Hooks {
|
||||
registry.Hooks[event] = append(registry.Hooks[event], manifest.Name)
|
||||
}
|
||||
saveRegistry(registry)
|
||||
SaveRegistry(registry)
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"status": "registered", "name": manifest.Name,
|
||||
"capabilities": manifest.Capabilities})
|
||||
@@ -151,7 +183,7 @@ func main() {
|
||||
}
|
||||
delete(registry.Plugins, name)
|
||||
_ = entry
|
||||
saveRegistry(registry)
|
||||
SaveRegistry(registry)
|
||||
fmt.Printf(`{"status": "unregistered", "name": "%s"}`+"\n", name)
|
||||
|
||||
case "resolve":
|
||||
@@ -183,29 +215,45 @@ func main() {
|
||||
|
||||
case "fire":
|
||||
event := ""
|
||||
data := ""
|
||||
dataRaw := ""
|
||||
if len(args) > 0 {
|
||||
event = args[0]
|
||||
}
|
||||
for i := 1; i < len(args); i++ {
|
||||
if args[i] == "--data" && i+1 < len(args) {
|
||||
data = args[i+1]
|
||||
dataRaw = args[i+1]
|
||||
i++
|
||||
}
|
||||
}
|
||||
listeners, ok := registry.Hooks[event]
|
||||
if !ok || len(listeners) == 0 {
|
||||
fmt.Println(`{"status": "no_listeners"}`)
|
||||
var data interface{}
|
||||
if dataRaw != "" {
|
||||
if json.Unmarshal([]byte(dataRaw), &data) != nil {
|
||||
fmt.Printf(`{"error": "invalid --data JSON: %s"}`+"\n", dataRaw)
|
||||
return
|
||||
}
|
||||
}
|
||||
result := Fire(®istry, event, data, 30*time.Second, NowIso())
|
||||
SaveRegistry(registry)
|
||||
out, _ := json.Marshal(result)
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "discover":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: microkernel discover <scripts-dir>")
|
||||
os.Exit(1)
|
||||
}
|
||||
result, err := Discover(®istry, args[0], NowIso())
|
||||
if err != nil {
|
||||
fmt.Printf(`{"error": "%s"}`+"\n", err.Error())
|
||||
return
|
||||
}
|
||||
results := make([]string, 0)
|
||||
for _, listener := range listeners {
|
||||
results = append(results, fmt.Sprintf("hook fired on %s with data %s", listener, data))
|
||||
}
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"status": "fired", "event": event, "listeners": listeners, "results": results})
|
||||
SaveRegistry(registry)
|
||||
out, _ := json.Marshal(result)
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "parity":
|
||||
parityMain()
|
||||
|
||||
case "status":
|
||||
plugins := make([]string, 0)
|
||||
for name := range registry.Plugins {
|
||||
|
||||
@@ -5,10 +5,71 @@ package main
|
||||
// operations; file I/O stays reactive).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// pythonExe — the oracle dispatches hooks with sys.executable; the port
|
||||
// resolves python3 from PATH (fixture plugins are stdlib-only, so any
|
||||
// python3 is byte-equivalent).
|
||||
var pythonExe = "python3"
|
||||
|
||||
// nowFunc — wall clock seam; the battery pins it (no-clock law: frozen
|
||||
// truth never depends on time.Now()).
|
||||
var nowFunc = time.Now
|
||||
|
||||
// NowIso — the oracle's now_iso(): UTC, seconds precision, Python's
|
||||
// isoformat(timespec="seconds") renders the UTC offset as +00:00 (not Z).
|
||||
func NowIso() string {
|
||||
return nowFunc().UTC().Format("2006-01-02T15:04:05-07:00")
|
||||
}
|
||||
|
||||
// pyTruthy — Python truthiness for JSON-shaped values. The oracle gates
|
||||
// hooks with `if enabled:` and auto_activate with manifest.get(..., True):
|
||||
// string hook targets ("handle_hook") are truthy, "" / 0 / false / null /
|
||||
// empty containers are not.
|
||||
func pyTruthy(v interface{}) bool {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return x
|
||||
case float64:
|
||||
return x != 0
|
||||
case string:
|
||||
return x != ""
|
||||
case []interface{}:
|
||||
return len(x) > 0
|
||||
case map[string]interface{}:
|
||||
return len(x) > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// truncateRunes — Python str[:n] slices CODEPOINTS, not bytes.
|
||||
func truncateRunes(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n])
|
||||
}
|
||||
|
||||
// roundToEven1 — Python round(x, 1) is banker's rounding (ties-to-even).
|
||||
func roundToEven1(v float64) float64 {
|
||||
return math.RoundToEven(v*10) / 10
|
||||
}
|
||||
|
||||
// Register — register a plugin from its manifest (map form, decoded like
|
||||
// the oracle receives it). Error text parity: "manifest must have a 'name'".
|
||||
func Register(reg *Registry, manifest map[string]interface{}, nowISO string) (PluginEntry, error) {
|
||||
@@ -18,14 +79,14 @@ func Register(reg *Registry, manifest map[string]interface{}, nowISO string) (Pl
|
||||
}
|
||||
active := true
|
||||
if v, has := manifest["auto_activate"]; has {
|
||||
if b, ok := v.(bool); ok {
|
||||
active = b
|
||||
}
|
||||
active = pyTruthy(v)
|
||||
}
|
||||
file, _ := manifest["_file"].(string)
|
||||
entry := PluginEntry{
|
||||
Manifest: manifestToStruct(manifest),
|
||||
RegisteredAt: nowISO,
|
||||
Active: active,
|
||||
File: file,
|
||||
}
|
||||
reg.Plugins[name] = entry
|
||||
if caps, ok := manifest["capabilities"].([]interface{}); ok {
|
||||
@@ -52,9 +113,11 @@ func Register(reg *Registry, manifest map[string]interface{}, nowISO string) (Pl
|
||||
for ev := range hooks {
|
||||
names = append(names, ev)
|
||||
}
|
||||
// Per-event lists only ever gain this name once, so iteration
|
||||
// order is unobservable in the registry state; sorted = deterministic.
|
||||
sort.Strings(names)
|
||||
for _, ev := range names {
|
||||
if enabled, _ := hooks[ev].(bool); enabled {
|
||||
if pyTruthy(hooks[ev]) {
|
||||
list := reg.Hooks[ev]
|
||||
found := false
|
||||
for _, n := range list {
|
||||
@@ -137,3 +200,448 @@ func Resolve(reg *Registry, capability string) string {
|
||||
sort.SliceStable(active, func(i, j int) bool { return active[i].priority < active[j].priority })
|
||||
return active[0].name
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Discovery — scan a directory and register every plugin with a PLUGIN dict.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Discover — the oracle's self-registration sweep: every .py file (not
|
||||
// underscore-prefixed, not microkernel.py itself) whose source carries a
|
||||
// PLUGIN literal gets registered. Go lists the directory name-sorted —
|
||||
// deterministic where os.listdir is filesystem-order (order only affects
|
||||
// append order inside index lists; documented divergence).
|
||||
func Discover(reg *Registry, scriptsDir string, nowISO string) (map[string]interface{}, error) {
|
||||
registered := 0
|
||||
errs := []map[string]interface{}{}
|
||||
entries, err := os.ReadDir(scriptsDir)
|
||||
if err != nil {
|
||||
// the oracle lets OSError propagate out of discover()
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
fname := e.Name()
|
||||
if !strings.HasSuffix(fname, ".py") || strings.HasPrefix(fname, "_") {
|
||||
continue
|
||||
}
|
||||
if fname == "microkernel.py" {
|
||||
continue
|
||||
}
|
||||
fpath := filepath.Join(scriptsDir, fname)
|
||||
manifest := ExtractManifest(fpath)
|
||||
if len(manifest) == 0 {
|
||||
continue // `if manifest:` — {} is falsy, skipped like None
|
||||
}
|
||||
manifest["_file"] = fpath
|
||||
manifest["_module"] = strings.TrimSuffix(fname, ".py")
|
||||
if _, rerr := Register(reg, manifest, nowISO); rerr != nil {
|
||||
errs = append(errs, map[string]interface{}{"file": fname, "error": rerr.Error()})
|
||||
} else {
|
||||
registered++
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{"discovered": registered, "errors": errs}, nil
|
||||
}
|
||||
|
||||
// ExtractManifest — pull the PLUGIN dict literal out of .py source
|
||||
// WITHOUT executing it (the oracle's ast.literal_eval path). Byte-for-byte
|
||||
// brace accounting, including the oracle's edge: the opener line is never
|
||||
// re-checked, so `PLUGIN = {}` keeps scanning subsequent lines.
|
||||
// Non-dict PLUGIN literals return nil in Go (the oracle returns the value
|
||||
// and register() then crashes on .get — documented crash-class divergence).
|
||||
func ExtractManifest(fpath string) map[string]interface{} {
|
||||
content, err := os.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return ExtractManifestString(string(content))
|
||||
}
|
||||
|
||||
// ExtractManifestString — ExtractManifest over in-memory source (the
|
||||
// battery/parity seam; the result never depends on the path).
|
||||
func ExtractManifestString(content string) map[string]interface{} {
|
||||
if !strings.Contains(content, "PLUGIN") {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(content, "\n")
|
||||
start, end := -1, -1
|
||||
for i, line := range lines {
|
||||
t := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(t, "PLUGIN") && strings.Contains(line, "=") && strings.Contains(line, "{") {
|
||||
start = i
|
||||
brace := strings.Count(line, "{") - strings.Count(line, "}")
|
||||
for j := i + 1; j < len(lines); j++ {
|
||||
brace += strings.Count(lines[j], "{") - strings.Count(lines[j], "}")
|
||||
if brace <= 0 {
|
||||
end = j
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 || end < 0 {
|
||||
return nil
|
||||
}
|
||||
dictStr := strings.Join(lines[start:end+1], "\n")
|
||||
if eq := strings.Index(dictStr, "="); eq >= 0 {
|
||||
dictStr = strings.TrimSpace(dictStr[eq+1:])
|
||||
}
|
||||
v, perr := pyLiteral(dictStr)
|
||||
if perr != nil {
|
||||
return nil
|
||||
}
|
||||
m, _ := v.(map[string]interface{})
|
||||
return m
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// fire — execute hooks for an event (subprocess isolation, 8-way parallel).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// RunHook — one listener as a python subprocess (the oracle's _run_hook
|
||||
// closure). Statuses: skipped | error | ok | timeout. duration_ms is
|
||||
// banker's-rounded to one decimal like round(x, 1); the timeout branch
|
||||
// carries no duration, matching the oracle.
|
||||
func RunHook(reg *Registry, name, event, dataJSON string, timeout time.Duration) map[string]interface{} {
|
||||
plugin, has := reg.Plugins[name]
|
||||
if !has || !plugin.Active {
|
||||
return map[string]interface{}{"name": name, "status": "skipped"}
|
||||
}
|
||||
fpath := plugin.File
|
||||
if fpath == "" || !fileExists(fpath) {
|
||||
return map[string]interface{}{"name": name, "status": "error", "error": "file not found"}
|
||||
}
|
||||
hookStart := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, pythonExe, fpath, "hook", "--event", event, "--data", dataJSON)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
runErr := cmd.Run()
|
||||
duration := roundToEven1(float64(time.Since(hookStart).Nanoseconds()) / 1e6)
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return map[string]interface{}{"name": name, "status": "timeout"}
|
||||
}
|
||||
if runErr != nil {
|
||||
if _, isExit := runErr.(*exec.ExitError); isExit {
|
||||
return map[string]interface{}{
|
||||
"name": name, "status": "error",
|
||||
"error": truncateRunes(strings.TrimSpace(stderr.String()), 200),
|
||||
"duration_ms": duration,
|
||||
}
|
||||
}
|
||||
// start-class failure (python missing, exec perm): the oracle hits
|
||||
// the generic `except Exception` — message only, no duration.
|
||||
return map[string]interface{}{
|
||||
"name": name, "status": "error",
|
||||
"error": truncateRunes(runErr.Error(), 200),
|
||||
}
|
||||
}
|
||||
var result interface{}
|
||||
if json.Unmarshal(stdout.Bytes(), &result) != nil {
|
||||
result = truncateRunes(strings.TrimSpace(stdout.String()), 500)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"name": name, "status": "ok",
|
||||
"result": result, "duration_ms": duration,
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
_, err := os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Fire — run every enabled listener for an event in parallel (8 workers,
|
||||
// the oracle's ThreadPoolExecutor cap), then append a bounded fire-log
|
||||
// entry. The oracle's as_completed result order is nondeterministic; the
|
||||
// port returns results name-sorted (rigs normalize identically on the
|
||||
// Python side). total_duration_ms and the log duration are wall-clock:
|
||||
// batteries normalize them.
|
||||
func Fire(reg *Registry, event string, data interface{}, timeout time.Duration, nowISO string) map[string]interface{} {
|
||||
listeners := reg.Hooks[event]
|
||||
if len(listeners) == 0 {
|
||||
return map[string]interface{}{"event": event, "hooks_fired": 0, "results": []map[string]interface{}{}}
|
||||
}
|
||||
dataJSON := []byte("{}")
|
||||
if data != nil {
|
||||
b, merr := json.Marshal(data)
|
||||
if merr == nil {
|
||||
dataJSON = b
|
||||
}
|
||||
}
|
||||
totalStart := time.Now()
|
||||
sem := make(chan struct{}, 8)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
results := make([]map[string]interface{}, 0, len(listeners))
|
||||
for _, name := range listeners {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(n string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
r := RunHook(reg, n, event, string(dataJSON), timeout)
|
||||
mu.Lock()
|
||||
results = append(results, r)
|
||||
mu.Unlock()
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
return fmt.Sprint(results[i]["name"]) < fmt.Sprint(results[j]["name"])
|
||||
})
|
||||
totalDuration := roundToEven1(float64(time.Since(totalStart).Nanoseconds()) / 1e6)
|
||||
reg.Log = append(reg.Log, FireLog{
|
||||
Event: event, Timestamp: nowISO,
|
||||
HooksFired: len(results), DurationMs: totalDuration,
|
||||
})
|
||||
if len(reg.Log) > 100 {
|
||||
reg.Log = reg.Log[len(reg.Log)-100:]
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"event": event, "hooks_fired": len(results),
|
||||
"results": results,
|
||||
"total_duration_ms": totalDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Python-literal parser — the ast.literal_eval subset PLUGIN dicts use.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type pyParser struct {
|
||||
s string
|
||||
pos int
|
||||
}
|
||||
|
||||
// pyLiteral — parse a Python literal: dict / list / tuple / string /
|
||||
// int / float / True / False / None, with comments and trailing commas.
|
||||
// Numbers become float64 (the JSON-canonical type of this port).
|
||||
func pyLiteral(src string) (interface{}, error) {
|
||||
p := &pyParser{s: src}
|
||||
p.ws()
|
||||
v, err := p.value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.ws()
|
||||
if p.pos != len(p.s) {
|
||||
return nil, fmt.Errorf("pyLiteral: trailing input at %d", p.pos)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (p *pyParser) ws() {
|
||||
for p.pos < len(p.s) {
|
||||
c := p.s[p.pos]
|
||||
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
|
||||
p.pos++
|
||||
continue
|
||||
}
|
||||
if c == '#' { // tokenizer strips comments before literal_eval
|
||||
for p.pos < len(p.s) && p.s[p.pos] != '\n' {
|
||||
p.pos++
|
||||
}
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pyParser) value() (interface{}, error) {
|
||||
if p.pos >= len(p.s) {
|
||||
return nil, fmt.Errorf("pyLiteral: unexpected end")
|
||||
}
|
||||
switch c := p.s[p.pos]; {
|
||||
case c == '{':
|
||||
return p.dict()
|
||||
case c == '[' || c == '(':
|
||||
return p.seq()
|
||||
case c == '"' || c == '\'':
|
||||
s, err := p.str()
|
||||
return s, err
|
||||
case c == '-' || c == '+' || (c >= '0' && c <= '9') || c == '.':
|
||||
return p.number()
|
||||
default:
|
||||
if strings.HasPrefix(p.s[p.pos:], "True") {
|
||||
p.pos += 4
|
||||
return true, nil
|
||||
}
|
||||
if strings.HasPrefix(p.s[p.pos:], "False") {
|
||||
p.pos += 5
|
||||
return false, nil
|
||||
}
|
||||
if strings.HasPrefix(p.s[p.pos:], "None") {
|
||||
p.pos += 4
|
||||
return nil, nil
|
||||
}
|
||||
// bare identifier: legal only as a dict KEY (handled in dict());
|
||||
// as a value it is not a literal.
|
||||
return nil, fmt.Errorf("pyLiteral: unexpected %q at %d", c, p.pos)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pyParser) dict() (interface{}, error) {
|
||||
p.pos++ // '{'
|
||||
out := map[string]interface{}{}
|
||||
for {
|
||||
p.ws()
|
||||
if p.pos >= len(p.s) {
|
||||
return nil, fmt.Errorf("pyLiteral: unterminated dict")
|
||||
}
|
||||
if p.s[p.pos] == '}' {
|
||||
p.pos++
|
||||
return out, nil
|
||||
}
|
||||
var key interface{}
|
||||
var err error
|
||||
if c := p.s[p.pos]; c == '"' || c == '\'' {
|
||||
key, err = p.str()
|
||||
} else {
|
||||
key, err = p.atomKey()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.ws()
|
||||
if p.pos >= len(p.s) || p.s[p.pos] != ':' {
|
||||
return nil, fmt.Errorf("pyLiteral: expected ':' at %d", p.pos)
|
||||
}
|
||||
p.pos++
|
||||
p.ws()
|
||||
v, err := p.value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[fmt.Sprint(key)] = v
|
||||
p.ws()
|
||||
if p.pos < len(p.s) && p.s[p.pos] == ',' {
|
||||
p.pos++
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// atomKey — bare-identifier or numeric dict key (Python allows any hashable
|
||||
// literal; PLUGIN manifests use strings).
|
||||
func (p *pyParser) atomKey() (interface{}, error) {
|
||||
start := p.pos
|
||||
for p.pos < len(p.s) {
|
||||
c := p.s[p.pos]
|
||||
if c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' {
|
||||
p.pos++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if p.pos == start {
|
||||
return nil, fmt.Errorf("pyLiteral: bad key at %d", start)
|
||||
}
|
||||
tok := p.s[start:p.pos]
|
||||
if f, err := parseFloat(tok); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func (p *pyParser) seq() (interface{}, error) {
|
||||
opener := p.s[p.pos]
|
||||
closer := byte(']')
|
||||
if opener == '(' {
|
||||
closer = ')'
|
||||
}
|
||||
p.pos++
|
||||
out := []interface{}{}
|
||||
for {
|
||||
p.ws()
|
||||
if p.pos >= len(p.s) {
|
||||
return nil, fmt.Errorf("pyLiteral: unterminated sequence")
|
||||
}
|
||||
if p.s[p.pos] == closer {
|
||||
p.pos++
|
||||
return out, nil
|
||||
}
|
||||
v, err := p.value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, v)
|
||||
p.ws()
|
||||
if p.pos < len(p.s) && p.s[p.pos] == ',' {
|
||||
p.pos++
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pyParser) str() (interface{}, error) {
|
||||
quote := p.s[p.pos]
|
||||
p.pos++
|
||||
var b strings.Builder
|
||||
for p.pos < len(p.s) {
|
||||
c := p.s[p.pos]
|
||||
if c == quote {
|
||||
p.pos++
|
||||
return b.String(), nil
|
||||
}
|
||||
if c == '\\' && p.pos+1 < len(p.s) {
|
||||
p.pos++
|
||||
switch e := p.s[p.pos]; e {
|
||||
case 'n':
|
||||
b.WriteByte('\n')
|
||||
case 't':
|
||||
b.WriteByte('\t')
|
||||
case 'r':
|
||||
b.WriteByte('\r')
|
||||
case 'a':
|
||||
b.WriteByte('\a')
|
||||
case 'b':
|
||||
b.WriteByte('\b')
|
||||
case 'f':
|
||||
b.WriteByte('\f')
|
||||
case 'v':
|
||||
b.WriteByte('\v')
|
||||
case '0':
|
||||
b.WriteByte(0)
|
||||
case '\\', '\'', '"':
|
||||
b.WriteByte(e)
|
||||
default:
|
||||
b.WriteByte(e)
|
||||
}
|
||||
p.pos++
|
||||
continue
|
||||
}
|
||||
b.WriteByte(c)
|
||||
p.pos++
|
||||
}
|
||||
return nil, fmt.Errorf("pyLiteral: unterminated string")
|
||||
}
|
||||
|
||||
func (p *pyParser) number() (interface{}, error) {
|
||||
start := p.pos
|
||||
if p.s[p.pos] == '-' || p.s[p.pos] == '+' {
|
||||
p.pos++
|
||||
}
|
||||
for p.pos < len(p.s) {
|
||||
c := p.s[p.pos]
|
||||
if (c >= '0' && c <= '9') || c == '.' ||
|
||||
c == 'e' || c == 'E' ||
|
||||
((c == '-' || c == '+') && (p.s[p.pos-1] == 'e' || p.s[p.pos-1] == 'E')) {
|
||||
p.pos++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return parseFloat(p.s[start:p.pos])
|
||||
}
|
||||
|
||||
func parseFloat(tok string) (interface{}, error) {
|
||||
var f float64
|
||||
if _, err := fmt.Sscanf(tok, "%g", &f); err != nil {
|
||||
return nil, fmt.Errorf("pyLiteral: bad number %q", tok)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
// parity.go — JSON op dispatch over stdin (the Go binary family contract):
|
||||
// one op per invocation, thin shell over the ported kernel fns so the
|
||||
// truth battery and the live-parity sweep exercise the same code paths.
|
||||
//
|
||||
// Ops:
|
||||
// now_iso → {"result": NowIso()}
|
||||
// extract_manifest {content} → {"result": ExtractManifestString}
|
||||
// discover {dir, now} → {"result", "plugins"} (paths as-is)
|
||||
// registry_load {content?} → LoadRegistry shape (writes content to
|
||||
// the RUNE_MEMORY_DIR registry first)
|
||||
// registry_roundtrip {} → save(load()) shape equality
|
||||
// run_hook {registry,name,event,data,timeout_ms}
|
||||
// fire {registry,event,data,timeout_ms,now}
|
||||
// → RunHook / Fire over the request-built
|
||||
// registry (durations wall-clock; the
|
||||
// parity script normalizes them)
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func emit(v interface{}) {
|
||||
out, _ := json.Marshal(v)
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
func parityMain() {
|
||||
var req map[string]interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad JSON input: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
op, _ := req["op"].(string)
|
||||
switch op {
|
||||
case "now_iso":
|
||||
emit(map[string]interface{}{"result": NowIso()})
|
||||
|
||||
case "extract_manifest":
|
||||
content, _ := req["content"].(string)
|
||||
emit(map[string]interface{}{"result": ExtractManifestString(content)})
|
||||
|
||||
case "discover":
|
||||
dir, _ := req["dir"].(string)
|
||||
now, _ := req["now"].(string)
|
||||
reg := emptyRegistry()
|
||||
result, err := Discover(®, dir, now)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
plugins := map[string]interface{}{}
|
||||
for k, v := range reg.Plugins {
|
||||
plugins[k] = map[string]interface{}{"active": v.Active, "registered_at": v.RegisteredAt, "file": v.File}
|
||||
}
|
||||
emit(map[string]interface{}{"result": result, "plugins": plugins,
|
||||
"capabilities": reg.Capabilities, "hooks": reg.Hooks})
|
||||
|
||||
case "registry_load":
|
||||
if content, has := req["content"].(string); has {
|
||||
os.MkdirAll(filepath.Dir(registryPath()), 0755)
|
||||
os.WriteFile(registryPath(), []byte(content), 0644)
|
||||
} else {
|
||||
os.Remove(registryPath())
|
||||
}
|
||||
r := LoadRegistry()
|
||||
emit(registryShape(r))
|
||||
|
||||
case "registry_roundtrip":
|
||||
r1 := LoadRegistry()
|
||||
SaveRegistry(r1)
|
||||
r2 := LoadRegistry()
|
||||
a, _ := json.Marshal(registryShape(r1))
|
||||
b, _ := json.Marshal(registryShape(r2))
|
||||
emit(map[string]interface{}{"stable": string(a) == string(b), "shape": registryShape(r2)})
|
||||
|
||||
case "run_hook":
|
||||
reg := registryFromReq(req)
|
||||
name, _ := req["name"].(string)
|
||||
event, _ := req["event"].(string)
|
||||
data, _ := req["data"].(string)
|
||||
timeoutMs, _ := req["timeout_ms"].(float64)
|
||||
if timeoutMs <= 0 {
|
||||
timeoutMs = 30000
|
||||
}
|
||||
emit(RunHook(reg, name, event, data, time.Duration(timeoutMs)*time.Millisecond))
|
||||
|
||||
case "fire":
|
||||
reg := registryFromReq(req)
|
||||
event, _ := req["event"].(string)
|
||||
now, _ := req["now"].(string)
|
||||
timeoutMs, _ := req["timeout_ms"].(float64)
|
||||
if timeoutMs <= 0 {
|
||||
timeoutMs = 30000
|
||||
}
|
||||
result := Fire(reg, event, req["data"], time.Duration(timeoutMs)*time.Millisecond, now)
|
||||
emit(map[string]interface{}{"result": result, "log": logProjection(reg.Log)})
|
||||
|
||||
default:
|
||||
emit(map[string]interface{}{"error": "unknown op: " + op})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// registryFromReq — decode the request's "registry" blob into a live
|
||||
// Registry (plugins carry fixture hook script paths in "file").
|
||||
func registryFromReq(req map[string]interface{}) *Registry {
|
||||
raw, _ := json.Marshal(req["registry"])
|
||||
var reg Registry
|
||||
if json.Unmarshal(raw, ®) != nil {
|
||||
return &Registry{}
|
||||
}
|
||||
if reg.Plugins == nil {
|
||||
reg.Plugins = map[string]PluginEntry{}
|
||||
}
|
||||
if reg.Capabilities == nil {
|
||||
reg.Capabilities = map[string][]string{}
|
||||
}
|
||||
if reg.Hooks == nil {
|
||||
reg.Hooks = map[string][]string{}
|
||||
}
|
||||
if reg.Log == nil {
|
||||
reg.Log = []FireLog{}
|
||||
}
|
||||
return ®
|
||||
}
|
||||
|
||||
func registryShape(r Registry) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"plugins": r.Plugins, "capabilities": r.Capabilities,
|
||||
"hooks": r.Hooks, "log": logProjection(r.Log),
|
||||
}
|
||||
}
|
||||
|
||||
// logProjection — FireLog entries as generic JSON maps (parity payloads).
|
||||
func logProjection(log []FireLog) []map[string]interface{} {
|
||||
out := []map[string]interface{}{}
|
||||
for _, e := range log {
|
||||
out = append(out, map[string]interface{}{
|
||||
"event": e.Event, "timestamp": e.Timestamp,
|
||||
"hooks_fired": e.HooksFired, "duration_ms": e.DurationMs,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,737 @@
|
||||
{
|
||||
"frozen_now": "2026-10-13T11:05:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "now_iso",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": "2026-10-13T11:05:00+00:00",
|
||||
"oracle": "microkernel"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {\n \"name\": \"cache\",\n \"version\": \"0.1.0\",\n \"description\": \"Cache system — event-driven invalidation.\",\n \"capabilities\": [\"cache\", \"invalidation\"],\n \"hooks\": {\n \"on_session_end\": \"handle_hook\",\n \"on_memory_add\": \"handle_hook\",\n },\n \"dependencies\": [],\n \"priority\": 1,\n \"auto_activate\": True,\n}\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"name": "cache",
|
||||
"version": "0.1.0",
|
||||
"description": "Cache system — event-driven invalidation.",
|
||||
"capabilities": [
|
||||
"cache",
|
||||
"invalidation"
|
||||
],
|
||||
"hooks": {
|
||||
"on_session_end": "handle_hook",
|
||||
"on_memory_add": "handle_hook"
|
||||
},
|
||||
"dependencies": [],
|
||||
"priority": 1,
|
||||
"auto_activate": true
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"X = 1\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": null
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {}\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {}\nX = 1\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": null
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {\n \"name\": \"x\",\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": null
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {'name': 'café', 'n': 1.5, 'neg': -3, 'big': 1e3}\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"name": "café",
|
||||
"n": 1.5,
|
||||
"neg": -3,
|
||||
"big": 1000.0
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {\n # comment inside\n \"name\": \"x\",\n}\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"name": "x"
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_extract",
|
||||
"args": [
|
||||
"PLUGIN = {\n \"name\": \"x\",\n \"hooks\": {\"a\": True, \"b\": False},\n \"nested\": {\"deep\": [1, 2, {\"k\": \"v\"}]},\n}\n"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"name": "x",
|
||||
"hooks": {
|
||||
"a": true,
|
||||
"b": false
|
||||
},
|
||||
"nested": {
|
||||
"deep": [
|
||||
1,
|
||||
2,
|
||||
{
|
||||
"k": "v"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_discover",
|
||||
"args": [
|
||||
[
|
||||
"_private.py",
|
||||
"a_cache.py",
|
||||
"b_noname.py",
|
||||
"c_plain.py",
|
||||
"d_falsy.py",
|
||||
"microkernel.py",
|
||||
"notes.txt"
|
||||
],
|
||||
{
|
||||
"a_cache.py": "PLUGIN = {\n \"name\": \"cache\",\n \"version\": \"0.1.0\",\n \"description\": \"Cache system — event-driven invalidation.\",\n \"capabilities\": [\"cache\", \"invalidation\"],\n \"hooks\": {\n \"on_session_end\": \"handle_hook\",\n \"on_memory_add\": \"handle_hook\",\n },\n \"dependencies\": [],\n \"priority\": 1,\n \"auto_activate\": True,\n}\n",
|
||||
"b_noname.py": "PLUGIN = {\n \"version\": \"0.1.0\",\n}\n",
|
||||
"c_plain.py": "def run():\n pass\n",
|
||||
"d_falsy.py": "PLUGIN = {\n \"name\": \"d\",\n \"capabilities\": [\"d\"],\n \"hooks\": {\"on_x\": False, \"on_y\": \"\", \"on_z\": 0},\n}\n",
|
||||
"_private.py": "PLUGIN = {\n \"name\": \"cache\",\n \"version\": \"0.1.0\",\n \"description\": \"Cache system — event-driven invalidation.\",\n \"capabilities\": [\"cache\", \"invalidation\"],\n \"hooks\": {\n \"on_session_end\": \"handle_hook\",\n \"on_memory_add\": \"handle_hook\",\n },\n \"dependencies\": [],\n \"priority\": 1,\n \"auto_activate\": True,\n}\n",
|
||||
"microkernel.py": "PLUGIN = {\n \"name\": \"cache\",\n \"version\": \"0.1.0\",\n \"description\": \"Cache system — event-driven invalidation.\",\n \"capabilities\": [\"cache\", \"invalidation\"],\n \"hooks\": {\n \"on_session_end\": \"handle_hook\",\n \"on_memory_add\": \"handle_hook\",\n },\n \"dependencies\": [],\n \"priority\": 1,\n \"auto_activate\": True,\n}\n",
|
||||
"notes.txt": "not a plugin"
|
||||
},
|
||||
"2026-10-13T11:05:00"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"discovered": 2,
|
||||
"errors": [
|
||||
{
|
||||
"file": "b_noname.py",
|
||||
"error": "manifest must have a 'name'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"registry": {
|
||||
"plugins": {
|
||||
"cache": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-13T11:05:00+00:00",
|
||||
"file": "<dir>/a_cache.py"
|
||||
},
|
||||
"d": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-13T11:05:00+00:00",
|
||||
"file": "<dir>/d_falsy.py"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"cache": [
|
||||
"cache"
|
||||
],
|
||||
"invalidation": [
|
||||
"cache"
|
||||
],
|
||||
"d": [
|
||||
"d"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"on_session_end": [
|
||||
"cache"
|
||||
],
|
||||
"on_memory_add": [
|
||||
"cache"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_discover",
|
||||
"args": [
|
||||
[],
|
||||
{},
|
||||
"2026-10-13T11:05:00"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"discovered": 0,
|
||||
"errors": []
|
||||
},
|
||||
"registry": {
|
||||
"plugins": {},
|
||||
"capabilities": {},
|
||||
"hooks": {}
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_discover",
|
||||
"args": [
|
||||
[
|
||||
"s1.py",
|
||||
"s2.py"
|
||||
],
|
||||
{
|
||||
"s1.py": "PLUGIN = {\n \"name\": \"s1\",\n \"capabilities\": [\"shared\"],\n \"priority\": 5,\n}\n",
|
||||
"s2.py": "PLUGIN = {\n \"name\": \"s2\",\n \"capabilities\": [\"shared\"],\n \"priority\": 10,\n}\n"
|
||||
},
|
||||
"2026-10-13T11:05:00"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"discovered": 2,
|
||||
"errors": []
|
||||
},
|
||||
"registry": {
|
||||
"plugins": {
|
||||
"s1": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-13T11:05:00+00:00",
|
||||
"file": "<dir>/s1.py"
|
||||
},
|
||||
"s2": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-13T11:05:00+00:00",
|
||||
"file": "<dir>/s2.py"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"shared": [
|
||||
"s1",
|
||||
"s2"
|
||||
]
|
||||
},
|
||||
"hooks": {}
|
||||
}
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_load",
|
||||
"args": [
|
||||
null
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"plugins": {},
|
||||
"capabilities": {},
|
||||
"hooks": {}
|
||||
},
|
||||
"log": []
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_load",
|
||||
"args": [
|
||||
"{oops"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"plugins": {},
|
||||
"capabilities": {},
|
||||
"hooks": {}
|
||||
},
|
||||
"log": []
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_load",
|
||||
"args": [
|
||||
"{\n \"plugins\": {\n \"p\": {\n \"manifest\": {\n \"name\": \"p\",\n \"priority\": 1\n },\n \"registered_at\": \"2026-10-13T11:05:00+00:00\",\n \"active\": true,\n \"file\": \"/x/p.py\"\n }\n },\n \"capabilities\": {\n \"cap\": [\n \"p\"\n ]\n },\n \"hooks\": {\n \"evt\": [\n \"p\"\n ]\n },\n \"log\": [\n {\n \"event\": \"evt\",\n \"timestamp\": \"2026-10-13T11:05:00+00:00\",\n \"hooks_fired\": 1,\n \"duration_ms\": 12.5\n }\n ]\n}"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"plugins": {
|
||||
"p": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-13T11:05:00+00:00",
|
||||
"file": "/x/p.py"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"cap": [
|
||||
"p"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"evt": [
|
||||
"p"
|
||||
]
|
||||
}
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "evt",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": 12.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_save_load_default",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"first": {
|
||||
"plugins": {},
|
||||
"capabilities": {},
|
||||
"hooks": {},
|
||||
"log": []
|
||||
},
|
||||
"again": {
|
||||
"plugins": {},
|
||||
"capabilities": {},
|
||||
"hooks": {},
|
||||
"log": []
|
||||
},
|
||||
"stable": true
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
"import json, sys\ndata = sys.argv[sys.argv.index('--data') + 1]\nprint(json.dumps({'echo': json.loads(data)}))\n",
|
||||
{
|
||||
"name": "okp",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
},
|
||||
"on_session_end",
|
||||
{
|
||||
"k": "v",
|
||||
"n": 2
|
||||
},
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "okp",
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"echo": {
|
||||
"k": "v",
|
||||
"n": 2
|
||||
}
|
||||
},
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
"print('h\\u00e9llo-\\u2192ok ' * 120)\n",
|
||||
{
|
||||
"name": "rawp",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
},
|
||||
"on_session_end",
|
||||
null,
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "rawp",
|
||||
"status": "ok",
|
||||
"result": "héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok héllo-→ok ",
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
"import sys\nsys.stderr.write('\\u00e9rr\\u00f4r-' * 100)\nsys.exit(3)\n",
|
||||
{
|
||||
"name": "boomp",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
},
|
||||
"on_session_end",
|
||||
null,
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "boomp",
|
||||
"status": "error",
|
||||
"error": "érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-érrôr-ér",
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
"import time\ntime.sleep(5)\n",
|
||||
{
|
||||
"name": "slowp",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
},
|
||||
"on_session_end",
|
||||
null,
|
||||
1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "slowp",
|
||||
"status": "timeout"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
"import json, sys\ndata = sys.argv[sys.argv.index('--data') + 1]\nprint(json.dumps({'echo': json.loads(data)}))\n",
|
||||
{
|
||||
"name": "offp",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
},
|
||||
"auto_activate": false
|
||||
},
|
||||
"on_session_end",
|
||||
null,
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "offp",
|
||||
"status": "skipped"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
null,
|
||||
{
|
||||
"name": "ghostp",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
},
|
||||
"_file": "/nonexistent/ghost.py"
|
||||
},
|
||||
"on_session_end",
|
||||
null,
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "ghostp",
|
||||
"status": "error",
|
||||
"error": "file not found"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
null,
|
||||
{
|
||||
"name": "nofilep",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
},
|
||||
"on_session_end",
|
||||
null,
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 1,
|
||||
"results": [
|
||||
{
|
||||
"name": "nofilep",
|
||||
"status": "error",
|
||||
"error": "file not found"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 1,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire",
|
||||
"args": [
|
||||
"import json, sys\ndata = sys.argv[sys.argv.index('--data') + 1]\nprint(json.dumps({'echo': json.loads(data)}))\n",
|
||||
{
|
||||
"name": "lonely",
|
||||
"capabilities": [
|
||||
"x"
|
||||
]
|
||||
},
|
||||
"no_such_event",
|
||||
null,
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "no_such_event",
|
||||
"hooks_fired": 0,
|
||||
"results": []
|
||||
},
|
||||
"log": []
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
},
|
||||
{
|
||||
"fn": "gen_microkernel_gap_truth_shims._mk_fire_two",
|
||||
"args": [
|
||||
[
|
||||
"import json, sys\ndata = sys.argv[sys.argv.index('--data') + 1]\nprint(json.dumps({'echo': json.loads(data)}))\n",
|
||||
"import json, sys\ndata = sys.argv[sys.argv.index('--data') + 1]\nprint(json.dumps({'echo': json.loads(data)}))\n"
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "zz",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aa",
|
||||
"hooks": {
|
||||
"on_session_end": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"on_session_end",
|
||||
{
|
||||
"k": 1
|
||||
},
|
||||
30
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"event": "on_session_end",
|
||||
"hooks_fired": 2,
|
||||
"results": [
|
||||
{
|
||||
"name": "aa",
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"echo": {
|
||||
"k": 1
|
||||
}
|
||||
},
|
||||
"duration_ms": "<ms>"
|
||||
},
|
||||
{
|
||||
"name": "zz",
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"echo": {
|
||||
"k": 1
|
||||
}
|
||||
},
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
],
|
||||
"total_duration_ms": "<ms>"
|
||||
},
|
||||
"log": [
|
||||
{
|
||||
"event": "on_session_end",
|
||||
"timestamp": "2026-10-13T11:05:00+00:00",
|
||||
"hooks_fired": 2,
|
||||
"duration_ms": "<ms>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oracle": "gen_microkernel_gap_truth_shims"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package main
|
||||
|
||||
// truth_microkernel_gap_test.go — gap-closing parity battery (dichotomy
|
||||
// wave, worktree rune-wave-cq). Replays truth_microkernel_gap.json —
|
||||
// generated against the LIVE Python oracle via scripts/freeze_truth.py —
|
||||
// through the ported kernel fns: NowIso, ExtractManifest, Discover,
|
||||
// RunHook (via Fire), LoadRegistry/SaveRegistry.
|
||||
//
|
||||
// Rig laws mirrored from the generator: wall-clock durations normalize
|
||||
// to "<ms>"; fire results compare name-sorted; fixture dirs rewrite to
|
||||
// <dir>; the frozen clock pins nowFunc.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const gapFrozenNow = "2026-10-13T11:05:00"
|
||||
const gapFrozenNowAware = "2026-10-13T11:05:00+00:00"
|
||||
|
||||
func TestMicrokernelGapParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_microkernel_gap.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_microkernel_gap.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []interface{} `json:"args"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("bad truth json: %v", err)
|
||||
}
|
||||
// full expected payloads (shims return richer shapes than result)
|
||||
var rawCases []map[string]interface{}
|
||||
if err := json.Unmarshal(b, &struct {
|
||||
Cases *[]map[string]interface{} `json:"cases"`
|
||||
}{Cases: &rawCases}); err != nil {
|
||||
t.Fatalf("bad truth json (raw): %v", err)
|
||||
}
|
||||
|
||||
for i, c := range doc.Cases {
|
||||
name := fmt.Sprintf("case%02d_%s", i, shortFn(c.Fn))
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := replayGapCase(t, c.Fn, c.Args)
|
||||
want := rawCases[i]["expected"]
|
||||
if !jsonEqualGap(got, want) {
|
||||
t.Errorf("%s divergence:\n got %s\nwant %s", c.Fn, mustJSON(got), mustJSON(want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func shortFn(fn string) string {
|
||||
if i := len(fn) - 1; i >= 0 {
|
||||
for j := len(fn) - 1; j >= 0; j-- {
|
||||
if fn[j] == '.' {
|
||||
return fn[j+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
// replayGapCase — one frozen case through the Go kernel. Mirrors the
|
||||
// generator shims byte-for-byte (durations → "<ms>", paths → <dir>).
|
||||
func replayGapCase(t *testing.T, fn string, args []interface{}) interface{} {
|
||||
t.Helper()
|
||||
switch fn {
|
||||
case "now_iso":
|
||||
restore := pinNow(t, gapFrozenNow)
|
||||
defer restore()
|
||||
return NowIso()
|
||||
|
||||
case "gen_microkernel_gap_truth_shims._mk_extract":
|
||||
content := argStr(args[0])
|
||||
f := filepath.Join(t.TempDir(), "probe.py")
|
||||
os.WriteFile(f, []byte(content), 0644)
|
||||
return map[string]interface{}{"result": ExtractManifest(f)}
|
||||
|
||||
case "gen_microkernel_gap_truth_shims._mk_discover":
|
||||
contents := argStrMap(args[1])
|
||||
dir := t.TempDir()
|
||||
for fname, src := range contents {
|
||||
os.WriteFile(filepath.Join(dir, fname), []byte(src), 0644)
|
||||
}
|
||||
reg := emptyRegistry()
|
||||
// args[2] carries the naive frozen stamp; the ORACLE registers with
|
||||
// now_iso() (aware frozen) — replay with the aware form.
|
||||
result, err := Discover(®, dir, gapFrozenNowAware)
|
||||
if err != nil {
|
||||
t.Fatalf("discover: %v", err)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"result": result,
|
||||
"registry": discoverProjection(®, dir),
|
||||
}
|
||||
|
||||
case "gen_microkernel_gap_truth_shims._mk_load":
|
||||
var content *string
|
||||
if args[0] != nil {
|
||||
s := argStr(args[0])
|
||||
content = &s
|
||||
}
|
||||
dir := t.TempDir()
|
||||
t.Setenv("RUNE_MEMORY_DIR", dir)
|
||||
regPath := filepath.Join(dir, "microkernel_registry.json")
|
||||
if content != nil {
|
||||
os.WriteFile(regPath, []byte(*content), 0644)
|
||||
} else if _, err := os.Stat(regPath); err == nil {
|
||||
os.Remove(regPath)
|
||||
}
|
||||
loaded := LoadRegistry()
|
||||
return map[string]interface{}{
|
||||
"result": loadProjection(&loaded),
|
||||
"log": logProjection(loaded.Log),
|
||||
}
|
||||
|
||||
case "gen_microkernel_gap_truth_shims._mk_save_load_default":
|
||||
dir := t.TempDir()
|
||||
t.Setenv("RUNE_MEMORY_DIR", dir)
|
||||
first := LoadRegistry()
|
||||
SaveRegistry(first)
|
||||
again := LoadRegistry()
|
||||
return map[string]interface{}{
|
||||
"first": fullProjection(&first),
|
||||
"again": fullProjection(&again),
|
||||
"stable": jsonEqualGap(fullProjection(&first), fullProjection(&again)),
|
||||
}
|
||||
|
||||
case "gen_microkernel_gap_truth_shims._mk_fire":
|
||||
return replayFire(t, args[0], args[1], args[2], args[3], args[4])
|
||||
|
||||
case "gen_microkernel_gap_truth_shims._mk_fire_two":
|
||||
scripts := argAnyList(args[0])
|
||||
manifests := argMapList(args[1])
|
||||
event := argStr(args[2])
|
||||
data := args[3]
|
||||
timeout := time.Duration(argFloat(args[4])) * time.Second
|
||||
dir := t.TempDir()
|
||||
reg := emptyRegistry()
|
||||
for i, m := range manifests {
|
||||
manifest := mapCopy(m)
|
||||
if scripts[i] != nil {
|
||||
fpath := filepath.Join(dir, argStr(manifest["name"])+".py")
|
||||
os.WriteFile(fpath, []byte(argStr(scripts[i])), 0644)
|
||||
manifest["_file"] = fpath
|
||||
}
|
||||
if _, err := Register(®, manifest, gapFrozenNowAware); err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
}
|
||||
return fireResult(®, event, data, timeout)
|
||||
|
||||
default:
|
||||
t.Fatalf("unhandled fn %q", fn)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func replayFire(t *testing.T, scriptArg, manifestArg, eventArg, dataArg, timeoutArg interface{}) interface{} {
|
||||
t.Helper()
|
||||
manifest := mapCopy(manifestArg.(map[string]interface{}))
|
||||
if scriptArg != nil {
|
||||
fpath := filepath.Join(t.TempDir(), "plugin.py")
|
||||
os.WriteFile(fpath, []byte(argStr(scriptArg)), 0644)
|
||||
manifest["_file"] = fpath
|
||||
}
|
||||
dir := t.TempDir()
|
||||
t.Setenv("RUNE_MEMORY_DIR", dir)
|
||||
reg := emptyRegistry()
|
||||
if _, err := Register(®, manifest, gapFrozenNowAware); err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
timeout := time.Duration(argFloat(timeoutArg)) * time.Second
|
||||
return fireResult(®, argStr(eventArg), dataArg, timeout)
|
||||
}
|
||||
|
||||
// fireResult — Fire + the rig's normalization (durations → "<ms>").
|
||||
func fireResult(reg *Registry, event string, data interface{}, timeout time.Duration) interface{} {
|
||||
res := Fire(reg, event, data, timeout, gapFrozenNowAware)
|
||||
normalized := normalizeDurations(res)
|
||||
return map[string]interface{}{
|
||||
"result": normalized,
|
||||
"log": normalizeLogDurations(logProjection(reg.Log)),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeDurations(res map[string]interface{}) map[string]interface{} {
|
||||
out := map[string]interface{}{}
|
||||
for k, v := range res {
|
||||
out[k] = v
|
||||
}
|
||||
if _, has := out["total_duration_ms"]; has {
|
||||
out["total_duration_ms"] = "<ms>"
|
||||
}
|
||||
if results, ok := out["results"].([]map[string]interface{}); ok {
|
||||
cp := make([]map[string]interface{}, 0, len(results))
|
||||
for _, r := range results {
|
||||
rc := map[string]interface{}{}
|
||||
for k, v := range r {
|
||||
rc[k] = v
|
||||
}
|
||||
if _, has := rc["duration_ms"]; has {
|
||||
rc["duration_ms"] = "<ms>"
|
||||
}
|
||||
cp = append(cp, rc)
|
||||
}
|
||||
sort.SliceStable(cp, func(i, j int) bool {
|
||||
return fmt.Sprint(cp[i]["name"]) < fmt.Sprint(cp[j]["name"])
|
||||
})
|
||||
out["results"] = cp
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeLogDurations(log []map[string]interface{}) []map[string]interface{} {
|
||||
out := []map[string]interface{}{}
|
||||
for _, e := range log {
|
||||
cp := map[string]interface{}{}
|
||||
for k, v := range e {
|
||||
cp[k] = v
|
||||
}
|
||||
cp["duration_ms"] = "<ms>"
|
||||
out = append(out, cp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func discoverProjection(reg *Registry, dir string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"plugins": pluginProjection(reg, true),
|
||||
"capabilities": strSliceMap(reg.Capabilities),
|
||||
"hooks": strSliceMap(reg.Hooks),
|
||||
}
|
||||
}
|
||||
|
||||
func loadProjection(reg *Registry) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"plugins": pluginProjection(reg, false),
|
||||
"capabilities": strSliceMap(reg.Capabilities),
|
||||
"hooks": strSliceMap(reg.Hooks),
|
||||
}
|
||||
}
|
||||
|
||||
// fullProjection — includes the log (the _save_load_default shape).
|
||||
func fullProjection(reg *Registry) map[string]interface{} {
|
||||
proj := loadProjection(reg)
|
||||
proj["log"] = logProjection(reg.Log)
|
||||
return proj
|
||||
}
|
||||
|
||||
func pluginProjection(reg *Registry, rewriteDir bool) map[string]interface{} {
|
||||
plugins := map[string]interface{}{}
|
||||
for k, v := range reg.Plugins {
|
||||
file := v.File
|
||||
if rewriteDir && file != "" {
|
||||
file = "<dir>/" + filepath.Base(file)
|
||||
}
|
||||
plugins[k] = map[string]interface{}{
|
||||
"active": v.Active, "registered_at": v.RegisteredAt, "file": file,
|
||||
}
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
func strSliceMap(m map[string][]string) map[string]interface{} {
|
||||
out := map[string]interface{}{}
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pinNow(t *testing.T, iso string) func() {
|
||||
t.Helper()
|
||||
base, err := time.Parse("2006-01-02T15:04:05", iso)
|
||||
if err != nil {
|
||||
t.Fatalf("pinNow: %v", err)
|
||||
}
|
||||
old := nowFunc
|
||||
nowFunc = func() time.Time { return base.UTC() }
|
||||
return func() { nowFunc = old }
|
||||
}
|
||||
|
||||
// jsonEqualGap — value-equal via marshal both sides through the SAME
|
||||
// encoder (integral floats parse to float64 on both sides).
|
||||
func jsonEqualGap(a, b interface{}) bool {
|
||||
return mustJSON(a) == mustJSON(b)
|
||||
}
|
||||
|
||||
func mustJSON(v interface{}) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("<marshal-error %v>", err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func argStr(v interface{}) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func argFloat(v interface{}) float64 {
|
||||
f, _ := v.(float64)
|
||||
return f
|
||||
}
|
||||
|
||||
func argStrList(v interface{}) []string {
|
||||
raw, _ := v.([]interface{})
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, x := range raw {
|
||||
out = append(out, argStr(x))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func argStrMap(v interface{}) map[string]string {
|
||||
raw, _ := v.(map[string]interface{})
|
||||
out := map[string]string{}
|
||||
for k, x := range raw {
|
||||
out[k] = argStr(x)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func argAnyList(v interface{}) []interface{} {
|
||||
raw, _ := v.([]interface{})
|
||||
return raw
|
||||
}
|
||||
|
||||
func argMapList(v interface{}) []map[string]interface{} {
|
||||
raw, _ := v.([]interface{})
|
||||
out := make([]map[string]interface{}, 0, len(raw))
|
||||
for _, x := range raw {
|
||||
if m, ok := x.(map[string]interface{}); ok {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mapCopy(m map[string]interface{}) map[string]interface{} {
|
||||
cp := map[string]interface{}{}
|
||||
for k, v := range m {
|
||||
cp[k] = v
|
||||
}
|
||||
return cp
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func TestMicrokernelParity(t *testing.T) {
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
|
||||
reg := loadRegistry()
|
||||
reg := LoadRegistry()
|
||||
regp := ®
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Name {
|
||||
@@ -106,14 +106,14 @@ func TestUnregisterCleansEmptyKeys(t *testing.T) {
|
||||
"p1": {Manifest: PluginManifest{Name: "p1", Capabilities: []string{"echo"}},
|
||||
RegisteredAt: "T", Active: true},
|
||||
}, Capabilities: map[string][]string{"echo": {"p1"}}, Hooks: map[string][]string{}}
|
||||
saveRegistry(reg)
|
||||
SaveRegistry(reg)
|
||||
// simulate the CLI path via main's command dispatch is heavyweight —
|
||||
// call the same filter logic through a fresh register/unregister cycle
|
||||
// using the public store API:
|
||||
reg2 := loadRegistry()
|
||||
reg2 := LoadRegistry()
|
||||
_ = reg2
|
||||
// direct: verify save/load round-trip then the empty-key invariant
|
||||
loaded := loadRegistry()
|
||||
loaded := LoadRegistry()
|
||||
if _, ok := loaded.Capabilities["echo"]; !ok {
|
||||
t.Fatal("setup: echo capability missing")
|
||||
}
|
||||
@@ -129,8 +129,8 @@ func TestUnregisterCleansEmptyKeys(t *testing.T) {
|
||||
} else {
|
||||
loaded.Capabilities["echo"] = filtered
|
||||
}
|
||||
saveRegistry(loaded)
|
||||
re := loadRegistry()
|
||||
SaveRegistry(loaded)
|
||||
re := LoadRegistry()
|
||||
if _, exists := re.Capabilities["echo"]; exists {
|
||||
t.Error("emptied capability key must be deleted (status counts keys)")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+803
-12
@@ -1,18 +1,212 @@
|
||||
// Package main — model-profiles: the GO port of src/rune/model_profiles.py's
|
||||
// deterministic detection cores (Oracle-Battery Parity Pattern, #2479 GO
|
||||
// batch 7): detect_family / detect_parameters / detect_quantization /
|
||||
// success_rate. The profile store + Ollama scans stay Python (reactive).
|
||||
// Package main — model-profiles: the GO port of src/rune/model_profiles.py
|
||||
// (Oracle-Battery Parity Pattern, #2479 GO gap-close batch): the full
|
||||
// deterministic profile surface — ModelProfile (to_dict/from_dict lens,
|
||||
// success_rate), the FAMILY_KNOWLEDGE table, detect_family /
|
||||
// detect_parameters / detect_quantization, get_profile_path filesystem
|
||||
// sanitization, the hermetic profile store (load_profile / save_profile
|
||||
// / get_or_create_profile / record_interaction / get_prompt_context),
|
||||
// build_profile_from_ollama assembly, and the scan_all_models Ollama
|
||||
// sweep. Frozen truth (frozen_mp_truth.json, 36 cases) replays VERBATIM
|
||||
// through the store seam in frozen_battery_test.go; the byte-parity
|
||||
// save_profile rows pin the Python-shaped JSON writer (pyDumps/asdict
|
||||
// field order, indent-2 separators, ensure_ascii, integral-float rule).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// profilesDir mirrors PROFILES_DIR (expanduser("~/.rune/model_profiles")).
|
||||
// setProfilesDir repoints the seam (hermetic stores in tests/replay).
|
||||
var profilesDir = func() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
home = os.Getenv("HOME")
|
||||
}
|
||||
return filepath.Join(home, ".rune", "model_profiles")
|
||||
}()
|
||||
|
||||
func setProfilesDir(d string) { profilesDir = d }
|
||||
|
||||
// ollamaURL mirrors the module-global OLLAMA_URL (env default, per-call
|
||||
// repointable — the freeze rig's canned-server seam).
|
||||
var ollamaURL = func() string {
|
||||
if u := os.Getenv("OLLAMA_URL"); u != "" {
|
||||
return u
|
||||
}
|
||||
return "http://localhost:11434"
|
||||
}()
|
||||
|
||||
// nowUTC mirrors datetime.now(timezone.utc) — injected for the frozen
|
||||
// battery (the no-clock law: the port never reads the wall clock
|
||||
// directly in replayable paths).
|
||||
var nowUTC = func() time.Time { return time.Now().UTC() }
|
||||
|
||||
// ModelProfile — the port of the ModelProfile dataclass. Field order
|
||||
// mirrors asdict() emission (the byte-parity law).
|
||||
type ModelProfile struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Family string `json:"family"`
|
||||
Parameters string `json:"parameters"`
|
||||
Quantization string `json:"quantization"`
|
||||
SizeGB float64 `json:"size_gb"`
|
||||
Strengths []string `json:"strengths"`
|
||||
Weaknesses []string `json:"weaknesses"`
|
||||
PromptingTips []string `json:"prompting_tips"`
|
||||
SafetyTier int `json:"safety_tier"`
|
||||
KnownRefusalTopics []string `json:"known_refusal_topics"`
|
||||
ThinkingFormat string `json:"thinking_format"`
|
||||
ResponseStyle string `json:"response_style"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
OptimalTemperature float64 `json:"optimal_temperature"`
|
||||
Interactions int `json:"interactions"`
|
||||
Successes int `json:"successes"`
|
||||
Failures int `json:"failures"`
|
||||
LastUsed string `json:"last_used"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
// MarshalJSON renders with Python float semantics (the integral-float
|
||||
// law): integral floats carry the trailing .0 the oracle's json.dump
|
||||
// emits, so serve-mode responses compare byte-equal to oracle asdict
|
||||
// JSON instead of diverging 7 vs 7.0.
|
||||
func (p ModelProfile) MarshalJSON() ([]byte, error) {
|
||||
type raw ModelProfile
|
||||
b, err := json.Marshal(raw(p))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["size_gb"] = json.RawMessage(pyFloat(p.SizeGB))
|
||||
m["optimal_temperature"] = json.RawMessage(pyFloat(p.OptimalTemperature))
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func defaultModelProfile(name string) ModelProfile {
|
||||
return ModelProfile{
|
||||
Name: name, Strengths: []string{}, Weaknesses: []string{},
|
||||
PromptingTips: []string{}, KnownRefusalTopics: []string{},
|
||||
ContextWindow: 4096, OptimalTemperature: 0.7,
|
||||
}
|
||||
}
|
||||
|
||||
// successRate mirrors the success_rate property.
|
||||
func successRate(p ModelProfile) float64 {
|
||||
if p.Interactions == 0 {
|
||||
return 0.0
|
||||
}
|
||||
return float64(p.Successes) / float64(p.Interactions)
|
||||
}
|
||||
|
||||
// familyKnowledge mirrors FAMILY_KNOWLEDGE (insertion order preserved in
|
||||
// the table; detection sorts longest-first).
|
||||
type familyEntry struct {
|
||||
thinking string
|
||||
strengths []string
|
||||
weaknesses []string
|
||||
tips []string
|
||||
}
|
||||
|
||||
var familyKnowledge = map[string]familyEntry{
|
||||
"qwen": {
|
||||
thinking: "<think>",
|
||||
strengths: []string{"reasoning", "coding", "instruction-following", "multilingual"},
|
||||
weaknesses: []string{"verbose thinking", "safety reasoning in thinking blocks"},
|
||||
tips: []string{
|
||||
"Be direct and specific — Qwen responds well to clear instructions",
|
||||
"Use structured prompts with clear sections",
|
||||
"For coding tasks, specify the language and expected output format",
|
||||
"Qwen models tend to over-explain — ask for concise responses explicitly",
|
||||
},
|
||||
},
|
||||
"qwen3": {
|
||||
thinking: "<think>",
|
||||
strengths: []string{"reasoning", "coding", "instruction-following", "tool-use"},
|
||||
weaknesses: []string{"verbose thinking blocks", "safety reasoning even on uncensored variants"},
|
||||
tips: []string{
|
||||
"Qwen3 has native tool support — use JSON schema for structured output",
|
||||
"Thinking blocks are verbose — strip them before display",
|
||||
"For coding, specify the exact format expected",
|
||||
"Use /no_think to disable thinking blocks for simple tasks",
|
||||
},
|
||||
},
|
||||
"llama": {
|
||||
thinking: "",
|
||||
strengths: []string{"general knowledge", "creative writing", "conversation"},
|
||||
weaknesses: []string{"instruction-following weaker than Qwen", "can be verbose"},
|
||||
tips: []string{
|
||||
"Use clear, direct instructions",
|
||||
"Llama models respond well to role-playing prompts",
|
||||
"For coding, be very specific about requirements",
|
||||
},
|
||||
},
|
||||
"mistral": {
|
||||
thinking: "",
|
||||
strengths: []string{"coding", "reasoning", "concise responses"},
|
||||
weaknesses: []string{"can be terse", "less creative than Llama"},
|
||||
tips: []string{
|
||||
"Mistral is naturally concise — don't ask for brevity, it already is",
|
||||
"Good at structured output and JSON",
|
||||
"Use system prompts for persona control",
|
||||
},
|
||||
},
|
||||
"gemma": {
|
||||
thinking: "",
|
||||
strengths: []string{"instruction-following", "safety", "multilingual"},
|
||||
weaknesses: []string{"conservative responses", "strong safety training"},
|
||||
tips: []string{
|
||||
"Gemma has strong safety training — may need override for edge cases",
|
||||
"Good at following detailed instructions",
|
||||
"Use structured prompts for best results",
|
||||
},
|
||||
},
|
||||
"phi": {
|
||||
thinking: "",
|
||||
strengths: []string{"coding", "reasoning", "small but capable"},
|
||||
weaknesses: []string{"limited knowledge", "can hallucinate"},
|
||||
tips: []string{
|
||||
"Phi models are small but surprisingly capable at coding",
|
||||
"Keep prompts short — limited context window",
|
||||
"Good for quick coding tasks, not for deep knowledge",
|
||||
},
|
||||
},
|
||||
"deepseek": {
|
||||
thinking: "<think>",
|
||||
strengths: []string{"coding", "math", "reasoning", "uncensored variants available"},
|
||||
weaknesses: []string{"verbose thinking", "can be slow on large contexts"},
|
||||
tips: []string{
|
||||
"DeepSeek excels at coding and math tasks",
|
||||
"Use structured prompts with clear problem statements",
|
||||
"Thinking blocks are long — strip them for display",
|
||||
},
|
||||
},
|
||||
"davidau": {
|
||||
thinking: "<think>",
|
||||
strengths: []string{"uncensored", "creative", "no refusals", "detailed responses"},
|
||||
weaknesses: []string{"verbose", "can be slow", "quality varies by model base"},
|
||||
tips: []string{
|
||||
"DavidAU models are uncensored — no safety filtering",
|
||||
"Good for research and creative tasks that other models refuse",
|
||||
"Can be verbose — ask for concise responses if needed",
|
||||
"Quality depends on the base model — Qwen-based ones are best",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// familyOrder mirrors FAMILY_KNOWLEDGE sorted by len desc (longest-first match).
|
||||
var familyOrder = []string{"deepseek", "mistral", "davidau", "qwen3", "llama", "gemma", "qwen", "phi"}
|
||||
|
||||
@@ -59,18 +253,615 @@ func detectQuantization(name string) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func successRate(interactions, successes int) float64 {
|
||||
if interactions == 0 {
|
||||
return 0.0
|
||||
// getProfilePath mirrors the oracle's filesystem sanitization.
|
||||
func getProfilePath(modelName string) string {
|
||||
safe := strings.ReplaceAll(modelName, "/", "_")
|
||||
safe = strings.ReplaceAll(safe, ":", "_")
|
||||
safe = strings.ReplaceAll(safe, "?", "_")
|
||||
return filepath.Join(profilesDir, safe+".json")
|
||||
}
|
||||
|
||||
// pyFloat mirrors Python's json float rendering (the integral-float
|
||||
// rule): integral floats render with a trailing .0; non-integral ones
|
||||
// use repr-shortest. NaN/Inf never occur in profile state.
|
||||
func pyFloat(f float64) string {
|
||||
if !math.IsNaN(f) && !math.IsInf(f, 0) && f == float64(int64(f)) {
|
||||
return fmt.Sprintf("%.1f", f)
|
||||
}
|
||||
return float64(successes) / float64(interactions)
|
||||
return fmt.Sprintf("%v", f)
|
||||
}
|
||||
|
||||
// pyString mirrors json.dumps(s) with ensure_ascii (non-ASCII → \uXXXX,
|
||||
// surrogate-pair encoding for astral codepoints) and Go-identical
|
||||
// escaping for control chars, quotes, and backslash.
|
||||
func pyString(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
b.WriteString("\\\"")
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
case '\b':
|
||||
b.WriteString("\\b")
|
||||
case '\f':
|
||||
b.WriteString("\\f")
|
||||
default:
|
||||
if r < 0x20 {
|
||||
fmt.Fprintf(&b, "\\u%04x", r)
|
||||
} else if r < 0x7f {
|
||||
b.WriteRune(r)
|
||||
} else if r > 0xffff {
|
||||
r -= 0x10000
|
||||
fmt.Fprintf(&b, "\\u%04x\\u%04x", 0xd800+(r>>10), 0xdc00+(r&0x3ff))
|
||||
} else {
|
||||
fmt.Fprintf(&b, "\\u%04x", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func pyStrList(v []string) string {
|
||||
if v == nil {
|
||||
v = []string{}
|
||||
}
|
||||
parts := make([]string, len(v))
|
||||
for i, s := range v {
|
||||
parts[i] = pyString(s)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
}
|
||||
|
||||
// pyDumpsProfile mirrors json.dump(profile.to_dict(), indent=2):
|
||||
// asdict field order, ": " separators, 2-space indents, trailing newline.
|
||||
func pyDumpsProfile(p ModelProfile) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("{\n")
|
||||
row := func(name, val string, last bool) {
|
||||
b.WriteString(" " + pyString(name) + ": " + val)
|
||||
if !last {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
row("name", pyString(p.Name), false)
|
||||
row("display_name", pyString(p.DisplayName), false)
|
||||
row("family", pyString(p.Family), false)
|
||||
row("parameters", pyString(p.Parameters), false)
|
||||
row("quantization", pyString(p.Quantization), false)
|
||||
row("size_gb", pyFloat(p.SizeGB), false)
|
||||
b.WriteString(" " + pyString("strengths") + ": ")
|
||||
writeIndentedList(&b, p.Strengths)
|
||||
b.WriteString(",\n")
|
||||
b.WriteString(" " + pyString("weaknesses") + ": ")
|
||||
writeIndentedList(&b, p.Weaknesses)
|
||||
b.WriteString(",\n")
|
||||
b.WriteString(" " + pyString("prompting_tips") + ": ")
|
||||
writeIndentedList(&b, p.PromptingTips)
|
||||
b.WriteString(",\n")
|
||||
row("safety_tier", fmt.Sprintf("%d", p.SafetyTier), false)
|
||||
b.WriteString(" " + pyString("known_refusal_topics") + ": ")
|
||||
writeIndentedList(&b, p.KnownRefusalTopics)
|
||||
b.WriteString(",\n")
|
||||
row("thinking_format", pyString(p.ThinkingFormat), false)
|
||||
row("response_style", pyString(p.ResponseStyle), false)
|
||||
row("context_window", fmt.Sprintf("%d", p.ContextWindow), false)
|
||||
row("optimal_temperature", pyFloat(p.OptimalTemperature), false)
|
||||
row("interactions", fmt.Sprintf("%d", p.Interactions), false)
|
||||
row("successes", fmt.Sprintf("%d", p.Successes), false)
|
||||
row("failures", fmt.Sprintf("%d", p.Failures), false)
|
||||
row("last_used", pyString(p.LastUsed), false)
|
||||
row("created", pyString(p.Created), true)
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// writeIndentedList mirrors json.dump(list, indent=2) nested at depth 1:
|
||||
// [] stays inline; non-empty lists break one element per line.
|
||||
func writeIndentedList(b *strings.Builder, v []string) {
|
||||
if len(v) == 0 {
|
||||
b.WriteString("[]")
|
||||
return
|
||||
}
|
||||
b.WriteString("[\n")
|
||||
for i, s := range v {
|
||||
b.WriteString(" " + pyString(s))
|
||||
if i < len(v)-1 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString(" ]")
|
||||
}
|
||||
|
||||
// loadProfile mirrors the oracle: missing/corrupt/ill-typed files →
|
||||
// (nil, false) through the suppressed-exception floor. Unknown keys are
|
||||
// filtered; absent keys take dataclass defaults.
|
||||
func loadProfile(modelName string) (*ModelProfile, bool) {
|
||||
raw, err := os.ReadFile(getProfilePath(modelName))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var doc map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
p := defaultModelProfile("")
|
||||
seen := map[string]bool{}
|
||||
setStr := func(key string, dst *string) bool {
|
||||
raw, ok := doc[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
seen[key] = true
|
||||
if err := json.Unmarshal(raw, dst); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
setFloat := func(key string, dst *float64) bool {
|
||||
raw, ok := doc[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
seen[key] = true
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err != nil {
|
||||
return false
|
||||
}
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
*dst = f
|
||||
return true
|
||||
}
|
||||
setInt := func(key string, dst *int) bool {
|
||||
raw, ok := doc[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
seen[key] = true
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err != nil {
|
||||
return false
|
||||
}
|
||||
i, err := n.Int64()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
*dst = int(i)
|
||||
return true
|
||||
}
|
||||
setList := func(key string, dst *[]string) bool {
|
||||
raw, ok := doc[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
seen[key] = true
|
||||
var l []string
|
||||
if err := json.Unmarshal(raw, &l); err != nil {
|
||||
return false
|
||||
}
|
||||
if l == nil {
|
||||
l = []string{}
|
||||
}
|
||||
*dst = l
|
||||
return true
|
||||
}
|
||||
if raw, ok := doc["name"]; !ok {
|
||||
return nil, false
|
||||
} else {
|
||||
seen["name"] = true
|
||||
if err := json.Unmarshal(raw, &p.Name); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
ok := setStr("display_name", &p.DisplayName) &&
|
||||
setStr("family", &p.Family) &&
|
||||
setStr("parameters", &p.Parameters) &&
|
||||
setStr("quantization", &p.Quantization) &&
|
||||
setFloat("size_gb", &p.SizeGB) &&
|
||||
setList("strengths", &p.Strengths) &&
|
||||
setList("weaknesses", &p.Weaknesses) &&
|
||||
setList("prompting_tips", &p.PromptingTips) &&
|
||||
setInt("safety_tier", &p.SafetyTier) &&
|
||||
setList("known_refusal_topics", &p.KnownRefusalTopics) &&
|
||||
setStr("thinking_format", &p.ThinkingFormat) &&
|
||||
setStr("response_style", &p.ResponseStyle) &&
|
||||
setInt("context_window", &p.ContextWindow) &&
|
||||
setFloat("optimal_temperature", &p.OptimalTemperature) &&
|
||||
setInt("interactions", &p.Interactions) &&
|
||||
setInt("successes", &p.Successes) &&
|
||||
setInt("failures", &p.Failures) &&
|
||||
setStr("last_used", &p.LastUsed) &&
|
||||
setStr("created", &p.Created)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
for key := range doc {
|
||||
if !seen[key] {
|
||||
_ = key // unknown keys filtered (from_dict's known-field law)
|
||||
}
|
||||
}
|
||||
return &p, true
|
||||
}
|
||||
|
||||
// saveProfile mirrors the oracle: makedirs + Python-shaped JSON write,
|
||||
// best-effort (errors suppressed).
|
||||
func saveProfile(p ModelProfile) {
|
||||
_ = os.MkdirAll(profilesDir, 0o755)
|
||||
_ = os.WriteFile(getProfilePath(p.Name), []byte(pyDumpsProfile(p)), 0o644)
|
||||
}
|
||||
|
||||
// buildProfileFromOllama mirrors the oracle's assembly: family
|
||||
// detection, parameter/quantization extraction, knowledge merge,
|
||||
// uncensored safety tier, frozen-clock stamps.
|
||||
func buildProfileFromOllama(modelName string, modelInfo map[string]any) ModelProfile {
|
||||
family := detectFamily(modelName)
|
||||
fam, known := familyKnowledge[family]
|
||||
p := defaultModelProfile(modelName)
|
||||
p.DisplayName = modelName
|
||||
p.Family = family
|
||||
p.Parameters = detectParameters(modelName)
|
||||
p.Quantization = detectQuantization(modelName)
|
||||
if modelInfo != nil {
|
||||
if d, ok := modelInfo["display_name"].(string); ok {
|
||||
p.DisplayName = d
|
||||
}
|
||||
if s, ok := modelInfo["size_gb"].(float64); ok {
|
||||
p.SizeGB = s
|
||||
}
|
||||
}
|
||||
if known {
|
||||
p.Strengths = append([]string(nil), fam.strengths...)
|
||||
p.Weaknesses = append([]string(nil), fam.weaknesses...)
|
||||
p.PromptingTips = append([]string(nil), fam.tips...)
|
||||
p.ThinkingFormat = fam.thinking
|
||||
}
|
||||
lower := strings.ToLower(modelName)
|
||||
if strings.Contains(lower, "uncensored") || strings.Contains(lower, "davidau") {
|
||||
p.SafetyTier = 3
|
||||
}
|
||||
p.ContextWindow = 8192
|
||||
p.OptimalTemperature = 0.7
|
||||
stamp := nowUTC().Format("2006-01-02T15:04:05.999999+00:00")
|
||||
p.Created = stamp
|
||||
p.LastUsed = stamp
|
||||
return p
|
||||
}
|
||||
|
||||
// scanAllModels mirrors the oracle's Ollama sweep: best-effort degrade
|
||||
// (any error → empty list), skip nameless entries, update-or-build with
|
||||
// learned-data preservation, banker's rounding on the GB conversion.
|
||||
func scanAllModels() []ModelProfile {
|
||||
resp, err := http.Get(ollamaURL + "/api/tags")
|
||||
if err != nil {
|
||||
return []ModelProfile{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []ModelProfile{}
|
||||
}
|
||||
var data struct {
|
||||
Models []struct {
|
||||
Name string `json:"name"`
|
||||
Size float64 `json:"size"`
|
||||
} `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return []ModelProfile{}
|
||||
}
|
||||
var profiles []ModelProfile
|
||||
for _, m := range data.Models {
|
||||
if m.Name == "" {
|
||||
continue
|
||||
}
|
||||
gb := roundHalfEven(m.Size/(1024*1024*1024), 1)
|
||||
if existing, ok := loadProfile(m.Name); ok {
|
||||
existing.SizeGB = gb
|
||||
saveProfile(*existing)
|
||||
profiles = append(profiles, *existing)
|
||||
} else {
|
||||
p := buildProfileFromOllama(m.Name, map[string]any{
|
||||
"display_name": m.Name,
|
||||
"size_gb": gb,
|
||||
})
|
||||
saveProfile(p)
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
}
|
||||
if profiles == nil {
|
||||
profiles = []ModelProfile{}
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
// roundHalfEven mirrors Python's round(x, 1) (banker's rounding).
|
||||
func roundHalfEven(x float64, places int) float64 {
|
||||
mult := 1.0
|
||||
for i := 0; i < places; i++ {
|
||||
mult *= 10
|
||||
}
|
||||
return float64(roundHalfEvenInt(x*mult)) / mult
|
||||
}
|
||||
|
||||
func roundHalfEvenInt(x float64) int64 {
|
||||
lo := int64(x)
|
||||
if x-float64(lo) == 0.5 {
|
||||
if lo%2 == 0 {
|
||||
return lo
|
||||
}
|
||||
return lo + 1
|
||||
}
|
||||
if x-float64(lo) == -0.5 {
|
||||
if lo%2 == 0 {
|
||||
return lo
|
||||
}
|
||||
return lo - 1
|
||||
}
|
||||
if x >= 0 {
|
||||
return int64(x + 0.5)
|
||||
}
|
||||
return int64(x - 0.5)
|
||||
}
|
||||
|
||||
// getOrCreateProfile mirrors the oracle: load, else build-and-save.
|
||||
func getOrCreateProfile(modelName string) ModelProfile {
|
||||
if p, ok := loadProfile(modelName); ok {
|
||||
return *p
|
||||
}
|
||||
p := buildProfileFromOllama(modelName, nil)
|
||||
saveProfile(p)
|
||||
return p
|
||||
}
|
||||
|
||||
// recordInteraction mirrors the oracle's learning accumulator (returns
|
||||
// nothing — the saved profile state is the contract).
|
||||
func recordInteraction(modelName string, success bool, topic string) {
|
||||
p := getOrCreateProfile(modelName)
|
||||
p.Interactions++
|
||||
if success {
|
||||
p.Successes++
|
||||
} else {
|
||||
p.Failures++
|
||||
if topic != "" {
|
||||
seen := false
|
||||
for _, t := range p.KnownRefusalTopics {
|
||||
if t == topic {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
p.KnownRefusalTopics = append(p.KnownRefusalTopics, topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.LastUsed = nowUTC().Format("2006-01-02T15:04:05.999999+00:00")
|
||||
saveProfile(p)
|
||||
}
|
||||
|
||||
// getPromptContext mirrors the oracle's operator string assembly.
|
||||
func getPromptContext(modelName string) string {
|
||||
p := getOrCreateProfile(modelName)
|
||||
var parts []string
|
||||
if len(p.Strengths) > 0 {
|
||||
n := 3
|
||||
if len(p.Strengths) < n {
|
||||
n = len(p.Strengths)
|
||||
}
|
||||
parts = append(parts, "Strengths: "+strings.Join(p.Strengths[:n], ", "))
|
||||
}
|
||||
if len(p.PromptingTips) > 0 {
|
||||
parts = append(parts, "Tips: "+p.PromptingTips[0])
|
||||
}
|
||||
if len(p.KnownRefusalTopics) > 0 {
|
||||
n := 3
|
||||
if len(p.KnownRefusalTopics) < n {
|
||||
n = len(p.KnownRefusalTopics)
|
||||
}
|
||||
parts = append(parts, "Known refusals: "+strings.Join(p.KnownRefusalTopics[:n], ", "))
|
||||
}
|
||||
if p.SafetyTier > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Safety tier: %d", p.SafetyTier))
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
// serveStdin — JSON CLI: one op per invocation (the Go binary family
|
||||
// contract). Op dispatch is a thin shell over the ported functions so
|
||||
// the truth battery and the live-parity sweep exercise the SAME code
|
||||
// paths operators call. Stateful ops take an explicit "store" dir
|
||||
// (hermetic per-invocation) and an optional "now" instant (the
|
||||
// no-clock law — the sweep pins it, defaulting to wall clock).
|
||||
func serveStdin() {
|
||||
var req map[string]any
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
||||
emit(map[string]any{"error": "bad JSON input: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
op, _ := req["op"].(string)
|
||||
if dir, _ := req["store"].(string); dir != "" {
|
||||
setProfilesDir(dir)
|
||||
}
|
||||
if now, _ := req["now"].(string); now != "" {
|
||||
if t, err := time.Parse("2006-01-02T15:04:05.999999+00:00", now); err == nil {
|
||||
nowUTC = func() time.Time { return t }
|
||||
}
|
||||
}
|
||||
raw, _ := json.Marshal(req)
|
||||
switch op {
|
||||
case "detect_family":
|
||||
name, _ := req["name"].(string)
|
||||
emit(map[string]any{"result": detectFamily(name)})
|
||||
case "detect_parameters":
|
||||
name, _ := req["name"].(string)
|
||||
emit(map[string]any{"result": detectParameters(name)})
|
||||
case "detect_quantization":
|
||||
name, _ := req["name"].(string)
|
||||
emit(map[string]any{"result": detectQuantization(name)})
|
||||
case "success_rate":
|
||||
var in struct {
|
||||
Interactions int `json:"interactions"`
|
||||
Successes int `json:"successes"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
emit(map[string]any{"result": successRate(ModelProfile{Interactions: in.Interactions, Successes: in.Successes})})
|
||||
case "get_profile_path":
|
||||
name, _ := req["name"].(string)
|
||||
emit(map[string]any{"result": filepath.Base(getProfilePath(name))})
|
||||
case "load_profile":
|
||||
name, _ := req["name"].(string)
|
||||
if p, ok := loadProfile(name); ok {
|
||||
emitResult(profileJSON(p))
|
||||
} else {
|
||||
emit(map[string]any{"result": nil})
|
||||
}
|
||||
case "save_profile":
|
||||
var state map[string]json.RawMessage
|
||||
_ = json.Unmarshal(raw, &state)
|
||||
p := profileFromMap(state)
|
||||
saveProfile(p)
|
||||
rawBytes, _ := os.ReadFile(getProfilePath(p.Name))
|
||||
emit(map[string]any{"result": string(rawBytes)})
|
||||
case "build_profile":
|
||||
name, _ := req["name"].(string)
|
||||
var info map[string]any
|
||||
if m, ok := req["model_info"].(map[string]any); ok {
|
||||
info = m
|
||||
}
|
||||
emitResult(profileJSON(ptrOf(buildProfileFromOllama(name, info))))
|
||||
case "scan":
|
||||
if url, _ := req["ollama_url"].(string); url != "" {
|
||||
ollamaURL = url
|
||||
}
|
||||
profiles := scanAllModels()
|
||||
out := make([]any, len(profiles))
|
||||
for i, p := range profiles {
|
||||
out[i] = profileJSON(ptrOf(p))
|
||||
}
|
||||
emit(map[string]any{"result": out})
|
||||
case "get_or_create":
|
||||
name, _ := req["name"].(string)
|
||||
emitResult(profileJSON(ptrOf(getOrCreateProfile(name))))
|
||||
case "record":
|
||||
name, _ := req["name"].(string)
|
||||
success, _ := req["success"].(bool)
|
||||
topic, _ := req["topic"].(string)
|
||||
recordInteraction(name, success, topic)
|
||||
if p, ok := loadProfile(name); ok {
|
||||
emitResult(profileJSON(p))
|
||||
} else {
|
||||
emit(map[string]any{"error": "record wrote nothing"})
|
||||
os.Exit(1)
|
||||
}
|
||||
case "prompt_context":
|
||||
name, _ := req["name"].(string)
|
||||
emit(map[string]any{"result": getPromptContext(name)})
|
||||
case "to_dict":
|
||||
var state map[string]json.RawMessage
|
||||
_ = json.Unmarshal(raw, &state)
|
||||
emitResult(profileJSON(ptrOf(profileFromMap(state))))
|
||||
case "from_dict":
|
||||
var data map[string]json.RawMessage
|
||||
_ = json.Unmarshal(raw, &data)
|
||||
emitResult(profileJSON(ptrOf(profileFromMap(data))))
|
||||
default:
|
||||
emit(map[string]any{"error": "unknown op: " + op})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func ptrOf(p ModelProfile) *ModelProfile { return &p }
|
||||
|
||||
// profileJSON returns the profile as RawMessage — the MarshalJSON bytes
|
||||
// pass through UNPARSED so the Python float rendering (7.0, not 7)
|
||||
// survives into serve responses (a map[string]any round-trip would
|
||||
// re-render integral floats Go-style — the integral-float law).
|
||||
func profileJSON(p *ModelProfile) json.RawMessage {
|
||||
raw, _ := json.Marshal(p)
|
||||
return raw
|
||||
}
|
||||
|
||||
// emitResult splices a pre-rendered result value into the response
|
||||
// envelope without re-marshaling (preserves Python float rendering).
|
||||
func emitResult(raw json.RawMessage) {
|
||||
fmt.Printf("{\"result\":%s}\n", raw)
|
||||
}
|
||||
|
||||
// profileFromMap rebuilds a ModelProfile from a from_dict-shaped doc
|
||||
// (unknown keys ignored, absent keys defaulted — the oracle lens).
|
||||
func profileFromMap(state map[string]json.RawMessage) ModelProfile {
|
||||
p := defaultModelProfile("")
|
||||
unmarshal := func(key string, dst any) {
|
||||
if raw, ok := state[key]; ok {
|
||||
_ = json.Unmarshal(raw, dst)
|
||||
}
|
||||
}
|
||||
unmarshal("name", &p.Name)
|
||||
unmarshal("display_name", &p.DisplayName)
|
||||
unmarshal("family", &p.Family)
|
||||
unmarshal("parameters", &p.Parameters)
|
||||
unmarshal("quantization", &p.Quantization)
|
||||
unmarshal("size_gb", &p.SizeGB)
|
||||
unmarshal("strengths", &p.Strengths)
|
||||
unmarshal("weaknesses", &p.Weaknesses)
|
||||
unmarshal("prompting_tips", &p.PromptingTips)
|
||||
unmarshal("safety_tier", &p.SafetyTier)
|
||||
unmarshal("known_refusal_topics", &p.KnownRefusalTopics)
|
||||
unmarshal("thinking_format", &p.ThinkingFormat)
|
||||
unmarshal("response_style", &p.ResponseStyle)
|
||||
unmarshal("context_window", &p.ContextWindow)
|
||||
unmarshal("optimal_temperature", &p.OptimalTemperature)
|
||||
unmarshal("interactions", &p.Interactions)
|
||||
unmarshal("successes", &p.Successes)
|
||||
unmarshal("failures", &p.Failures)
|
||||
unmarshal("last_used", &p.LastUsed)
|
||||
unmarshal("created", &p.Created)
|
||||
if p.Strengths == nil {
|
||||
p.Strengths = []string{}
|
||||
}
|
||||
if p.Weaknesses == nil {
|
||||
p.Weaknesses = []string{}
|
||||
}
|
||||
if p.PromptingTips == nil {
|
||||
p.PromptingTips = []string{}
|
||||
}
|
||||
if p.KnownRefusalTopics == nil {
|
||||
p.KnownRefusalTopics = []string{}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func emit(v any) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "marshal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: model-profiles replay <truth.json>")
|
||||
os.Exit(2)
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
replayLegacy()
|
||||
return
|
||||
}
|
||||
serveStdin()
|
||||
}
|
||||
|
||||
func replayLegacy() {
|
||||
raw, err := os.ReadFile(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
@@ -103,7 +894,7 @@ func main() {
|
||||
inter := int(r[1].(float64))
|
||||
succ := int(r[2].(float64))
|
||||
wantRate := r[3].(float64)
|
||||
if got := successRate(inter, succ); got != wantRate {
|
||||
if got := successRate(ModelProfile{Interactions: inter, Successes: succ}); got != wantRate {
|
||||
fails++
|
||||
fmt.Printf("case %d success_rate %s: got %v want %v\n", i, name, got, wantRate)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// parallel-processor run <command1> <command2> ... (shell commands in parallel)
|
||||
// parallel-processor status
|
||||
// parallel-processor status # mirror of the oracle CLI status
|
||||
// parallel-processor test # mirror of the oracle CLI test run
|
||||
// parallel-processor run <cmd...> (shell commands in parallel)
|
||||
// parallel-processor replay [truth.json]
|
||||
// parallel-processor op (stdin JSON parity dispatch)
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -88,11 +91,28 @@ func splitCommand(cmd string) []string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: parallel-processor run <cmd1> <cmd2> ... | status")
|
||||
os.Exit(1)
|
||||
if len(os.Args) > 1 && os.Args[1] == "replay" {
|
||||
os.Exit(replayMain())
|
||||
}
|
||||
if len(os.Args) > 1 && os.Args[1] == "op" {
|
||||
os.Exit(parityMain())
|
||||
}
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" parallel_processor.py status # Show processor status")
|
||||
fmt.Println(" parallel_processor.py test # Test parallel execution")
|
||||
return
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "status":
|
||||
status := getStatus()
|
||||
fmt.Printf("Max workers: %d\n", status["max_workers"])
|
||||
fmt.Printf("Active threads: %d\n", status["active_threads"])
|
||||
|
||||
case "test":
|
||||
TestParallel()
|
||||
|
||||
case "run":
|
||||
commands := os.Args[2:]
|
||||
if len(commands) == 0 {
|
||||
@@ -103,15 +123,7 @@ func main() {
|
||||
out, _ := json.MarshalIndent(results, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "status":
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"runtime": "go",
|
||||
"goroutines_enabled": true,
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
os.Exit(1)
|
||||
fmt.Printf("Unknown command: %s\n", os.Args[1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
package main
|
||||
|
||||
// parallel_processor.go — name-preserving Go port of
|
||||
// src/rune/parallel_processor.py (dichotomy gap-close, wave #2479).
|
||||
//
|
||||
// Logic 1:1 with the Python oracle: results preserve submission order, a
|
||||
// failing task degrades to {"error": str(e)} (type name stripped, per the
|
||||
// established exception-contract quirk), and Python-visible strings —
|
||||
// type names in errors, repr shapes, submit-after-shutdown RuntimeError —
|
||||
// are replicated exactly. The ThreadPoolExecutor becomes a
|
||||
// semaphore-bounded goroutine pool that bounds concurrency at max_workers
|
||||
// while keeping the executor reusable across calls like the oracle's.
|
||||
//
|
||||
// Task specs (kind/fn/value grammar shared with scripts/pp_freeze_seams)
|
||||
// exist so callables can ride through JSON truth rows and the stdin parity
|
||||
// dispatch: the rig builds real Python callables from the same specs the
|
||||
// Go side turns into specCallable values.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultMaxWorkers = 4
|
||||
|
||||
// pyError carries a Python-style exception message (str(e) shape: the
|
||||
// type prefix is already stripped). Task panics recover into
|
||||
// {"error": msg} exactly like future.result() raising in the oracle.
|
||||
type pyError struct{ msg string }
|
||||
|
||||
func (e pyError) Error() string { return e.msg }
|
||||
|
||||
// ParallelProcessor mirrors the oracle's ParallelProcessor.
|
||||
type ParallelProcessor struct {
|
||||
maxWorkers int
|
||||
sem chan struct{} // bounds in-flight tasks at max_workers
|
||||
mu sync.Mutex
|
||||
down bool
|
||||
taskWG sync.WaitGroup // outstanding tasks, for Shutdown(wait=true)
|
||||
}
|
||||
|
||||
// NewParallelProcessor mirrors ParallelProcessor(max_workers).
|
||||
// max_workers <= 0 raises ValueError in the oracle; the port panics with
|
||||
// the same message (crash-parity law).
|
||||
func NewParallelProcessor(maxWorkers int) *ParallelProcessor {
|
||||
if maxWorkers <= 0 {
|
||||
panic("max_workers must be greater than 0")
|
||||
}
|
||||
return &ParallelProcessor{
|
||||
maxWorkers: maxWorkers,
|
||||
sem: make(chan struct{}, maxWorkers),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParallelProcessor) submitCheck() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.down {
|
||||
// The oracle raises from the submit loop itself — BEFORE any
|
||||
// per-task capture — so the whole call raises, not one row.
|
||||
panic("cannot schedule new futures after shutdown")
|
||||
}
|
||||
p.taskWG.Add(1)
|
||||
}
|
||||
|
||||
// RunParallel mirrors run_parallel(tasks): zero-arg calls in parallel,
|
||||
// submission-order results, per-task {"error": str(e)} degradation.
|
||||
// Non-callable tasks produce the oracle's worker TypeError text.
|
||||
func (p *ParallelProcessor) RunParallel(tasks []interface{}) []interface{} {
|
||||
results := make([]interface{}, len(tasks))
|
||||
var callWG sync.WaitGroup
|
||||
for i, task := range tasks {
|
||||
p.submitCheck()
|
||||
callWG.Add(1)
|
||||
p.sem <- struct{}{}
|
||||
go func(idx int, t interface{}) {
|
||||
defer callWG.Done()
|
||||
defer func() { <-p.sem }()
|
||||
defer p.taskWG.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
results[idx] = map[string]interface{}{"error": pyExcStr(r)}
|
||||
}
|
||||
}()
|
||||
results[idx] = callTask(t)
|
||||
}(i, task)
|
||||
}
|
||||
callWG.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// ArgsTask is the JSON spec grammar for run_parallel_with_args rows.
|
||||
// kind: call | raw | emptytuple | notcall | argsnotseq | kwnotdict.
|
||||
type ArgsTask struct {
|
||||
Kind string `json:"kind"`
|
||||
Fn string `json:"fn,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Ms float64 `json:"ms,omitempty"`
|
||||
Ret interface{} `json:"ret,omitempty"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Args []interface{} `json:"args,omitempty"`
|
||||
Kwargs map[string]interface{} `json:"kwargs,omitempty"`
|
||||
}
|
||||
|
||||
// RunParallelWithArgs mirrors run_parallel_with_args(tasks): parse the
|
||||
// (func, args, kwargs) tuples (errors recorded per index), run the valid
|
||||
// ones through the pool, then reassemble submission-order results with
|
||||
// the parse errors interleaved at their original positions.
|
||||
func (p *ParallelProcessor) RunParallelWithArgs(tasks []ArgsTask) []interface{} {
|
||||
type parsedTask struct {
|
||||
origIdx int
|
||||
task interface{}
|
||||
}
|
||||
parsed := []parsedTask{}
|
||||
errors := map[int]interface{}{}
|
||||
for i, task := range tasks {
|
||||
switch task.Kind {
|
||||
case "raw":
|
||||
errors[i] = map[string]interface{}{
|
||||
"error": fmt.Sprintf("task %d: expected tuple, got %s", i, pyTypeName(task.Value)),
|
||||
}
|
||||
case "emptytuple":
|
||||
errors[i] = map[string]interface{}{
|
||||
"error": fmt.Sprintf("task %d: expected tuple, got tuple", i),
|
||||
}
|
||||
case "notcall":
|
||||
errors[i] = map[string]interface{}{
|
||||
"error": fmt.Sprintf("task %d: %s is not callable", i, pyRepr(task.Value)),
|
||||
}
|
||||
case "argsnotseq":
|
||||
// args is a non-sequence scalar -> the oracle wraps it: (args,)
|
||||
parsed = append(parsed, parsedTask{i, specCallable{
|
||||
fn: task.Fn, args: []interface{}{task.Value}, kwargs: task.Kwargs,
|
||||
msg: task.Msg, ms: task.Ms, ret: task.Ret,
|
||||
}})
|
||||
case "kwnotdict":
|
||||
// kwargs is not a dict -> the oracle resets it to {}; the rig
|
||||
// passes () as args for this branch
|
||||
parsed = append(parsed, parsedTask{i, specCallable{
|
||||
fn: task.Fn, args: nil, kwargs: map[string]interface{}{},
|
||||
msg: task.Msg, ms: task.Ms, ret: task.Ret,
|
||||
}})
|
||||
case "call":
|
||||
kwargs := task.Kwargs
|
||||
if kwargs == nil {
|
||||
kwargs = map[string]interface{}{}
|
||||
}
|
||||
parsed = append(parsed, parsedTask{i, specCallable{
|
||||
fn: task.Fn, args: task.Args, kwargs: kwargs,
|
||||
msg: task.Msg, ms: task.Ms, ret: task.Ret,
|
||||
}})
|
||||
default:
|
||||
panic("unknown task kind " + task.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]interface{}, len(tasks))
|
||||
var callWG sync.WaitGroup
|
||||
for _, pt := range parsed {
|
||||
p.submitCheck()
|
||||
callWG.Add(1)
|
||||
p.sem <- struct{}{}
|
||||
go func(pt parsedTask) {
|
||||
defer callWG.Done()
|
||||
defer func() { <-p.sem }()
|
||||
defer p.taskWG.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
results[pt.origIdx] = map[string]interface{}{"error": pyExcStr(r)}
|
||||
}
|
||||
}()
|
||||
results[pt.origIdx] = callTask(pt.task)
|
||||
}(pt)
|
||||
}
|
||||
callWG.Wait()
|
||||
for i := range tasks {
|
||||
if e, ok := errors[i]; ok {
|
||||
results[i] = e
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// GetStatus mirrors get_status: pool_threads is reported as max_workers
|
||||
// (the oracle returns the configured bound, not live thread count);
|
||||
// active_threads reads the injectable seam (PP_ACTIVE_THREADS pins it in
|
||||
// batteries/parity; the live default is the Go-native goroutine count).
|
||||
func (p *ParallelProcessor) GetStatus() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"max_workers": p.maxWorkers,
|
||||
"pool_threads": p.maxWorkers,
|
||||
"active_threads": activeThreads(),
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown mirrors shutdown(wait): further submissions raise; wait=true
|
||||
// drains outstanding tasks first.
|
||||
func (p *ParallelProcessor) Shutdown(wait bool) {
|
||||
p.mu.Lock()
|
||||
p.down = true
|
||||
p.mu.Unlock()
|
||||
if wait {
|
||||
p.taskWG.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// ── global processor (module-level surface) ─────────────────────────────────
|
||||
|
||||
var (
|
||||
processorMu sync.Mutex
|
||||
processor *ParallelProcessor
|
||||
)
|
||||
|
||||
// GetProcessor mirrors get_processor(): the lazily created global
|
||||
// processor (default 4 workers).
|
||||
func GetProcessor() *ParallelProcessor {
|
||||
processorMu.Lock()
|
||||
defer processorMu.Unlock()
|
||||
if processor == nil {
|
||||
processor = NewParallelProcessor(defaultMaxWorkers)
|
||||
}
|
||||
return processor
|
||||
}
|
||||
|
||||
// ShutdownProcessor mirrors _shutdown_processor(): shut the global
|
||||
// processor down (no drain — the oracle passes wait=False) and clear it
|
||||
// so the next GetProcessor lazily rebuilds.
|
||||
func ShutdownProcessor() {
|
||||
processorMu.Lock()
|
||||
p := processor
|
||||
processor = nil
|
||||
processorMu.Unlock()
|
||||
if p != nil {
|
||||
p.Shutdown(false)
|
||||
}
|
||||
}
|
||||
|
||||
// module-level delegates (unexported: the struct methods carry the
|
||||
// audit-visible names; these mirror the module fns 1:1)
|
||||
|
||||
func runParallel(tasks []interface{}) []interface{} { return GetProcessor().RunParallel(tasks) }
|
||||
func runParallelWithArgs(tasks []ArgsTask) []interface{} {
|
||||
return GetProcessor().RunParallelWithArgs(tasks)
|
||||
}
|
||||
func getStatus() map[string]interface{} { return GetProcessor().GetStatus() }
|
||||
|
||||
// TestParallel mirrors test_parallel(): staggered tasks run sequentially
|
||||
// then through the pool; prints the oracle's report lines and returns
|
||||
// None. task_1/2/3 keep the oracle's nested-fn names and shapes.
|
||||
func TestParallel() {
|
||||
fmt.Println("Testing Parallel Processor...")
|
||||
|
||||
task_1 := func() interface{} {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return "Task 1 complete"
|
||||
}
|
||||
task_2 := func() interface{} {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return "Task 2 complete"
|
||||
}
|
||||
task_3 := func() interface{} {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
return "Task 3 complete"
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resultsSeq := []interface{}{task_1(), task_2(), task_3()}
|
||||
seqMs := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
|
||||
start = time.Now()
|
||||
resultsPar := runParallel([]interface{}{task_1, task_2, task_3})
|
||||
parMs := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
|
||||
fmt.Printf("Sequential: %.0fms\n", seqMs)
|
||||
fmt.Printf("Parallel: %.0fms\n", parMs)
|
||||
fmt.Printf("Speedup: %.1fx\n", seqMs/parMs)
|
||||
fmt.Printf("Results: %s\n", pyRepr(resultsPar))
|
||||
_ = resultsSeq
|
||||
|
||||
fmt.Println("✅ All tests passed!")
|
||||
}
|
||||
|
||||
// ── spec task machinery ─────────────────────────────────────────────────────
|
||||
|
||||
// specCallable is a task built from the JSON spec grammar; the rig builds
|
||||
// the same shapes as real Python callables on the oracle side.
|
||||
type specCallable struct {
|
||||
fn string
|
||||
args []interface{}
|
||||
kwargs map[string]interface{}
|
||||
msg string
|
||||
ms float64
|
||||
ret interface{}
|
||||
}
|
||||
|
||||
func (s specCallable) call() interface{} {
|
||||
switch s.fn {
|
||||
case "const":
|
||||
return s.ret
|
||||
case "echo":
|
||||
args := s.args
|
||||
if args == nil {
|
||||
args = []interface{}{}
|
||||
}
|
||||
kwargs := s.kwargs
|
||||
if kwargs == nil {
|
||||
kwargs = map[string]interface{}{}
|
||||
}
|
||||
return map[string]interface{}{"args": args, "kwargs": kwargs}
|
||||
case "add":
|
||||
return pyAdd(s.args[0], s.args[1])
|
||||
case "boom":
|
||||
panic(pyError{s.msg})
|
||||
case "sleepret":
|
||||
time.Sleep(time.Duration(s.ms * float64(time.Millisecond)))
|
||||
return s.ret
|
||||
}
|
||||
panic(pyError{"unknown spec fn " + s.fn})
|
||||
}
|
||||
|
||||
// callTask invokes a task: spec callables and raw Go funcs run; anything
|
||||
// else reproduces the oracle's worker TypeError for calling a
|
||||
// non-callable ("'int' object is not callable" — Python type name).
|
||||
func callTask(t interface{}) (result interface{}) {
|
||||
switch f := t.(type) {
|
||||
case specCallable:
|
||||
return f.call()
|
||||
case func() interface{}:
|
||||
return f()
|
||||
default:
|
||||
panic(pyError{fmt.Sprintf("'%s' object is not callable", pyTypeName(t))})
|
||||
}
|
||||
}
|
||||
|
||||
func pyExcStr(r interface{}) string {
|
||||
switch e := r.(type) {
|
||||
case pyError:
|
||||
return e.msg
|
||||
case string:
|
||||
return e
|
||||
default:
|
||||
return fmt.Sprint(e)
|
||||
}
|
||||
}
|
||||
|
||||
// activeThreads — the oracle reports threading.active_count(); live
|
||||
// thread state is process-random, so the seam reads PP_ACTIVE_THREADS
|
||||
// when pinned (battery/parity) and falls back to the Go-native count.
|
||||
func activeThreads() int {
|
||||
if v := os.Getenv("PP_ACTIVE_THREADS"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return runtime.NumGoroutine()
|
||||
}
|
||||
|
||||
// ── Python-shape value helpers ──────────────────────────────────────────────
|
||||
|
||||
// pyTypeName maps a decoded JSON value to the Python type name the
|
||||
// oracle would print (int vs float distinguished via json.Number).
|
||||
func pyTypeName(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "NoneType"
|
||||
case bool:
|
||||
return "bool"
|
||||
case string:
|
||||
return "str"
|
||||
case float64:
|
||||
return "float"
|
||||
case int:
|
||||
return "int"
|
||||
case int64:
|
||||
return "int"
|
||||
case []interface{}:
|
||||
return "list"
|
||||
case map[string]interface{}:
|
||||
return "dict"
|
||||
case json.Number:
|
||||
if strings.ContainsAny(string(t), ".eE") {
|
||||
return "float"
|
||||
}
|
||||
return "int"
|
||||
}
|
||||
return fmt.Sprintf("%T", v)
|
||||
}
|
||||
|
||||
// pyRepr renders a decoded JSON value the way Python repr would for the
|
||||
// frozen shapes: ints/floats bare, strings single-quoted, bools/None
|
||||
// capitalized, lists comma-space joined.
|
||||
func pyRepr(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case string:
|
||||
return "'" + t + "'"
|
||||
case float64:
|
||||
return pyFloatRepr(t)
|
||||
case json.Number:
|
||||
if strings.ContainsAny(string(t), ".eE") {
|
||||
f, _ := strconv.ParseFloat(string(t), 64)
|
||||
return pyFloatRepr(f)
|
||||
}
|
||||
return string(t)
|
||||
case []interface{}:
|
||||
parts := make([]string, len(t))
|
||||
for i, e := range t {
|
||||
parts[i] = pyRepr(e)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case map[string]interface{}:
|
||||
// Python dict repr is insertion-ordered; Go maps are not. The
|
||||
// frozen corpus deliberately contains no dict-repr rows — sorted
|
||||
// keys keep this deterministic if one ever appears.
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sortStrings(keys)
|
||||
parts := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
parts[i] = "'" + k + "': " + pyRepr(t[k])
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
func pyFloatRepr(f float64) string {
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// pyAdd mirrors Python + on the frozen number shapes: int+int stays int,
|
||||
// anything with a float is float math (json.Number keeps the distinction
|
||||
// the JSON decoder would give the oracle).
|
||||
func pyAdd(a, b interface{}) interface{} {
|
||||
ai, aInt := pyAsInt(a)
|
||||
bi, bInt := pyAsInt(b)
|
||||
if aInt && bInt {
|
||||
return json.Number(strconv.FormatInt(ai+bi, 10))
|
||||
}
|
||||
af, _ := pyAsFloat(a)
|
||||
bf, _ := pyAsFloat(b)
|
||||
return json.Number(pyFloatRepr(af + bf))
|
||||
}
|
||||
|
||||
func pyAsInt(v interface{}) (int64, bool) {
|
||||
switch t := v.(type) {
|
||||
case json.Number:
|
||||
if !strings.ContainsAny(string(t), ".eE") {
|
||||
n, err := strconv.ParseInt(string(t), 10, 64)
|
||||
return n, err == nil
|
||||
}
|
||||
case int:
|
||||
return int64(t), true
|
||||
case int64:
|
||||
return t, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func pyAsFloat(v interface{}) (float64, bool) {
|
||||
switch t := v.(type) {
|
||||
case json.Number:
|
||||
f, err := strconv.ParseFloat(string(t), 64)
|
||||
return f, err == nil
|
||||
case float64:
|
||||
return t, true
|
||||
case int:
|
||||
return float64(t), true
|
||||
case int64:
|
||||
return float64(t), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func sortStrings(s []string) {
|
||||
for i := 1; i < len(s); i++ {
|
||||
for j := i; j > 0 && s[j] < s[j-1]; j-- {
|
||||
s[j], s[j-1] = s[j-1], s[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package main
|
||||
|
||||
// replay.go — frozen-truth replay + stdin parity dispatch (the shared
|
||||
// dichotomy verification surface: the battery embeds truth.json VERBATIM
|
||||
// and replays every row; the parity sweep drives the same dispatch one
|
||||
// op per process via `parallel-processor op`).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type frozenCase struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []interface{} `json:"args"`
|
||||
Kwargs map[string]interface{} `json:"kwargs"`
|
||||
Recv map[string]interface{} `json:"recv,omitempty"`
|
||||
Expected json.RawMessage `json:"expected"`
|
||||
}
|
||||
|
||||
// canonical — round-trip a value through JSON so both sides compare as
|
||||
// decoded data (Python None and Go nil decode to the same null).
|
||||
func canonical(v interface{}) (interface{}, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out interface{}
|
||||
if err := decodeUseNumber(raw, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// decodeUseNumber keeps ints ints (json.Number) so pyTypeName and result
|
||||
// marshaling see the int/float distinction the JSON decoder gives Python.
|
||||
func decodeUseNumber(b []byte, v interface{}) error {
|
||||
dec := json.NewDecoder(strings.NewReader(string(b)))
|
||||
dec.UseNumber()
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
// buildTask — spec map -> runnable task: {"kind":"call",...} becomes a
|
||||
// specCallable; {"kind":"raw","value":v} and any non-spec value (dicts
|
||||
// without a kind key included) become the bare value (the oracle's
|
||||
// non-callable TypeError branch).
|
||||
func buildTask(spec interface{}) interface{} {
|
||||
m, ok := spec.(map[string]interface{})
|
||||
if !ok || m["kind"] == nil {
|
||||
return spec
|
||||
}
|
||||
if m["kind"] != "call" {
|
||||
return m["value"]
|
||||
}
|
||||
sc := specCallable{
|
||||
fn: str(m["fn"]),
|
||||
msg: str(m["msg"]),
|
||||
ret: m["ret"],
|
||||
args: slice(m["args"]),
|
||||
kwargs: mapping(m["kwargs"]),
|
||||
}
|
||||
if ms, ok := pyAsFloat(m["ms"]); ok {
|
||||
sc.ms = ms
|
||||
}
|
||||
return sc
|
||||
}
|
||||
|
||||
// buildTasks — apply buildTask across a decoded specs array.
|
||||
func buildTasks(specs interface{}) []interface{} {
|
||||
out := []interface{}{}
|
||||
for _, s := range slice(specs) {
|
||||
out = append(out, buildTask(s))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildArgsTask — spec map -> the ArgsTask grammar (re-marshaled through
|
||||
// the UseNumber decoder so Value keeps its int/float shape). Bare
|
||||
// non-map values are the direct-row form: raw tasks.
|
||||
func buildArgsTask(spec interface{}) (ArgsTask, error) {
|
||||
if _, ok := spec.(map[string]interface{}); !ok {
|
||||
return ArgsTask{Kind: "raw", Value: spec}, nil
|
||||
}
|
||||
var t ArgsTask
|
||||
b, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
err = decodeUseNumber(b, &t)
|
||||
return t, err
|
||||
}
|
||||
|
||||
func buildArgsTasks(specs interface{}) ([]ArgsTask, error) {
|
||||
out := []ArgsTask{}
|
||||
for _, s := range slice(specs) {
|
||||
t, err := buildArgsTask(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func replayCase(c frozenCase) (interface{}, error) {
|
||||
switch c.Fn {
|
||||
case "run_parallel", "_pp_run_parallel":
|
||||
return runParallel(buildTasks(c.Args[0])), nil
|
||||
case "run_parallel_with_args", "_pp_run_parallel_with_args":
|
||||
tasks, err := buildArgsTasks(c.Args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runParallelWithArgs(tasks), nil
|
||||
case "_pp_class_run_parallel":
|
||||
return NewParallelProcessor(pyInt(c.Args[0])).RunParallel(buildTasks(c.Args[1])), nil
|
||||
case "_pp_class_run_parallel_with_args":
|
||||
tasks, err := buildArgsTasks(c.Args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewParallelProcessor(pyInt(c.Args[0])).RunParallelWithArgs(tasks), nil
|
||||
case "get_status":
|
||||
return getStatus(), nil
|
||||
case "ParallelProcessor.get_status":
|
||||
mw := pyInt(c.Recv["max_workers"])
|
||||
// the harness snapshots recv rows as {"result": r, <post-call attrs>}
|
||||
return map[string]interface{}{
|
||||
"result": NewParallelProcessor(mw).GetStatus(),
|
||||
"max_workers": mw,
|
||||
}, nil
|
||||
case "_pp_get_processor_status":
|
||||
a, b := GetProcessor(), GetProcessor()
|
||||
return map[string]interface{}{
|
||||
"max_workers": a.maxWorkers,
|
||||
"is_singleton": a == b,
|
||||
}, nil
|
||||
case "_pp_shutdown_processor_cycle":
|
||||
ShutdownProcessor()
|
||||
processorMu.Lock()
|
||||
wasNil := processor == nil
|
||||
processorMu.Unlock()
|
||||
st := getStatus()
|
||||
return map[string]interface{}{
|
||||
"processor_is_none": wasNil,
|
||||
"recreated_max_workers": st["max_workers"],
|
||||
}, nil
|
||||
case "_pp_shutdown_then_status":
|
||||
p := NewParallelProcessor(3)
|
||||
p.Shutdown(true)
|
||||
return map[string]interface{}{
|
||||
"shutdown_returned_none": true,
|
||||
"status": p.GetStatus(),
|
||||
}, nil
|
||||
case "_pp_shutdown_then_run":
|
||||
p := NewParallelProcessor(2)
|
||||
p.Shutdown(true)
|
||||
var raised string
|
||||
func() {
|
||||
defer func() { raised = pyExcStr(recover()) }()
|
||||
p.RunParallel([]interface{}{specCallable{fn: "const", ret: 1}})
|
||||
}()
|
||||
return map[string]interface{}{"raised": raised}, nil
|
||||
case "_pp_new_bad":
|
||||
var raised string
|
||||
func() {
|
||||
defer func() { raised = pyExcStr(recover()) }()
|
||||
NewParallelProcessor(0)
|
||||
}()
|
||||
return map[string]interface{}{"raised": raised}, nil
|
||||
case "test_parallel":
|
||||
TestParallel()
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown frozen fn %q", c.Fn)
|
||||
}
|
||||
|
||||
// ── replay binary ───────────────────────────────────────────────────────────
|
||||
|
||||
func replayMain() int {
|
||||
path := "truth.json"
|
||||
if len(os.Args) > 2 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "FATAL:", err)
|
||||
return 2
|
||||
}
|
||||
fails, count := runTruthBytes(raw)
|
||||
for _, f := range fails {
|
||||
fmt.Println(f)
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases exact-equal vs the Python oracle\n", count)
|
||||
return 0
|
||||
}
|
||||
|
||||
func runTruthBytes(raw []byte) (fails []string, count int) {
|
||||
var env struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []frozenCase `json:"cases"`
|
||||
}
|
||||
if err := decodeUseNumber(raw, &env); err != nil {
|
||||
return []string{fmt.Sprintf("decode truth envelope: %v", err)}, 0
|
||||
}
|
||||
if len(env.Cases) == 0 {
|
||||
return []string{"no truth cases — the parity gate is empty"}, 0
|
||||
}
|
||||
for i, c := range env.Cases {
|
||||
actual, err := replayCase(c)
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("case %d fn=%s: %v", i, c.Fn, err))
|
||||
continue
|
||||
}
|
||||
got, err := canonical(actual)
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("case %d fn=%s: canonical actual: %v", i, c.Fn, err))
|
||||
continue
|
||||
}
|
||||
var want interface{}
|
||||
if err := decodeUseNumber(c.Expected, &want); err != nil {
|
||||
fails = append(fails, fmt.Sprintf("case %d fn=%s: decode expected: %v", i, c.Fn, err))
|
||||
continue
|
||||
}
|
||||
gj, _ := json.Marshal(got)
|
||||
wj, _ := json.Marshal(want)
|
||||
if string(gj) != string(wj) {
|
||||
fails = append(fails, fmt.Sprintf("case %d fn=%s: got %s want %s", i, c.Fn, gj, wj))
|
||||
}
|
||||
}
|
||||
return fails, len(env.Cases)
|
||||
}
|
||||
|
||||
// ── stdin parity dispatch (one op per process) ──────────────────────────────
|
||||
|
||||
func parityMain() int {
|
||||
raw, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "FATAL: read stdin:", err)
|
||||
return 2
|
||||
}
|
||||
var req map[string]interface{}
|
||||
if err := decodeUseNumber(raw, &req); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "FATAL: decode op:", err)
|
||||
return 2
|
||||
}
|
||||
out, err := parityOp(str(req["op"]), req)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "FATAL:", err)
|
||||
return 2
|
||||
}
|
||||
b, _ := json.Marshal(out)
|
||||
fmt.Println(string(b))
|
||||
return 0
|
||||
}
|
||||
|
||||
func parityOp(op string, req map[string]interface{}) (interface{}, error) {
|
||||
c := frozenCase{Fn: op, Kwargs: req}
|
||||
switch op {
|
||||
case "run_parallel", "run_parallel_with_args":
|
||||
c.Args = []interface{}{req["tasks"]}
|
||||
case "class_run_parallel", "class_run_parallel_with_args":
|
||||
c.Fn = "_pp_" + op
|
||||
c.Args = []interface{}{req["max_workers"], req["tasks"]}
|
||||
case "get_status":
|
||||
// module fn, no args
|
||||
case "class_get_status":
|
||||
c.Fn = "ParallelProcessor.get_status"
|
||||
c.Recv = map[string]interface{}{"max_workers": req["max_workers"]}
|
||||
default:
|
||||
// seam ops carry the frozen _pp_ prefix
|
||||
c.Fn = "_pp_" + op
|
||||
}
|
||||
return replayCase(c)
|
||||
}
|
||||
|
||||
// ── small decode helpers ────────────────────────────────────────────────────
|
||||
|
||||
func str(v interface{}) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func slice(v interface{}) []interface{} {
|
||||
s, _ := v.([]interface{})
|
||||
return s
|
||||
}
|
||||
|
||||
func mapping(v interface{}) map[string]interface{} {
|
||||
m, _ := v.(map[string]interface{})
|
||||
return m
|
||||
}
|
||||
|
||||
func pyInt(v interface{}) int {
|
||||
if n, ok := pyAsInt(v); ok {
|
||||
return int(n)
|
||||
}
|
||||
f, _ := pyAsFloat(v)
|
||||
return int(f)
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
{
|
||||
"frozen_now": "2026-10-14T08:00:00+00:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "run_parallel",
|
||||
"args": [
|
||||
[
|
||||
42
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"error": "'int' object is not callable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "run_parallel",
|
||||
"args": [
|
||||
[
|
||||
42,
|
||||
"x",
|
||||
null,
|
||||
true,
|
||||
4.5,
|
||||
[
|
||||
1
|
||||
],
|
||||
{
|
||||
"a": 1
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"error": "'int' object is not callable"
|
||||
},
|
||||
{
|
||||
"error": "'str' object is not callable"
|
||||
},
|
||||
{
|
||||
"error": "'NoneType' object is not callable"
|
||||
},
|
||||
{
|
||||
"error": "'bool' object is not callable"
|
||||
},
|
||||
{
|
||||
"error": "'float' object is not callable"
|
||||
},
|
||||
{
|
||||
"error": "'list' object is not callable"
|
||||
},
|
||||
{
|
||||
"error": "'dict' object is not callable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "run_parallel",
|
||||
"args": [
|
||||
[]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "run_parallel_with_args",
|
||||
"args": [
|
||||
[
|
||||
42,
|
||||
"x",
|
||||
null
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"error": "task 0: expected tuple, got int"
|
||||
},
|
||||
{
|
||||
"error": "task 1: expected tuple, got str"
|
||||
},
|
||||
{
|
||||
"error": "task 2: expected tuple, got NoneType"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "run_parallel_with_args",
|
||||
"args": [
|
||||
[
|
||||
[]
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"error": "task 0: expected tuple, got list"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_run_parallel",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "const",
|
||||
"ret": 7
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "boom",
|
||||
"msg": "kaput"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "const",
|
||||
"ret": "x"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
7,
|
||||
{
|
||||
"error": "kaput"
|
||||
},
|
||||
"x"
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_run_parallel",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 60,
|
||||
"ret": "a"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 10,
|
||||
"ret": "b"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 35,
|
||||
"ret": "c"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "boom",
|
||||
"msg": "late"
|
||||
},
|
||||
{
|
||||
"kind": "raw",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
{
|
||||
"error": "late"
|
||||
},
|
||||
{
|
||||
"error": "'NoneType' object is not callable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_run_parallel",
|
||||
"args": [
|
||||
[]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "_pp_class_run_parallel",
|
||||
"args": [
|
||||
3,
|
||||
[
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "const",
|
||||
"ret": 1
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "const",
|
||||
"ret": 2
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "boom",
|
||||
"msg": "nope"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "const",
|
||||
"ret": 4
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 15,
|
||||
"ret": 5
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
1,
|
||||
2,
|
||||
{
|
||||
"error": "nope"
|
||||
},
|
||||
4,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_class_run_parallel",
|
||||
"args": [
|
||||
1,
|
||||
[
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 30,
|
||||
"ret": "w1"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 5,
|
||||
"ret": "w2"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "const",
|
||||
"ret": "w3"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
"w1",
|
||||
"w2",
|
||||
"w3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_run_parallel_with_args",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "add",
|
||||
"args": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "add",
|
||||
"args": [
|
||||
1.5,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "echo"
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "echo",
|
||||
"kwargs": {
|
||||
"k": 1,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "boom",
|
||||
"msg": "bad-task"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
3,
|
||||
3.5,
|
||||
{
|
||||
"args": [],
|
||||
"kwargs": {}
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"kwargs": {
|
||||
"k": 1,
|
||||
"b": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"error": "bad-task"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_run_parallel_with_args",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"kind": "raw",
|
||||
"value": 42
|
||||
},
|
||||
{
|
||||
"kind": "raw",
|
||||
"value": "x"
|
||||
},
|
||||
{
|
||||
"kind": "raw",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"kind": "emptytuple"
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": 42
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": "x"
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": 4.5
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"error": "task 0: expected tuple, got int"
|
||||
},
|
||||
{
|
||||
"error": "task 1: expected tuple, got str"
|
||||
},
|
||||
{
|
||||
"error": "task 2: expected tuple, got NoneType"
|
||||
},
|
||||
{
|
||||
"error": "task 3: expected tuple, got tuple"
|
||||
},
|
||||
{
|
||||
"error": "task 4: 42 is not callable"
|
||||
},
|
||||
{
|
||||
"error": "task 5: 'x' is not callable"
|
||||
},
|
||||
{
|
||||
"error": "task 6: None is not callable"
|
||||
},
|
||||
{
|
||||
"error": "task 7: True is not callable"
|
||||
},
|
||||
{
|
||||
"error": "task 8: 4.5 is not callable"
|
||||
},
|
||||
{
|
||||
"error": "task 9: [1, 2] is not callable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_run_parallel_with_args",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"kind": "argsnotseq",
|
||||
"fn": "echo",
|
||||
"value": "ab"
|
||||
},
|
||||
{
|
||||
"kind": "argsnotseq",
|
||||
"fn": "echo",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"kind": "kwnotdict",
|
||||
"fn": "echo",
|
||||
"value": [
|
||||
"a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "sleepret",
|
||||
"ms": 10,
|
||||
"ret": "z"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"args": [
|
||||
"ab"
|
||||
],
|
||||
"kwargs": {}
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
5
|
||||
],
|
||||
"kwargs": {}
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"kwargs": {}
|
||||
},
|
||||
"z"
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_pp_class_run_parallel_with_args",
|
||||
"args": [
|
||||
2,
|
||||
[
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "echo",
|
||||
"args": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"kwargs": {
|
||||
"scale": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "raw",
|
||||
"value": 3.5
|
||||
},
|
||||
{
|
||||
"kind": "call",
|
||||
"fn": "boom",
|
||||
"msg": "class-branch"
|
||||
},
|
||||
{
|
||||
"kind": "notcall",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"args": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"kwargs": {
|
||||
"scale": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"error": "task 1: expected tuple, got float"
|
||||
},
|
||||
{
|
||||
"error": "class-branch"
|
||||
},
|
||||
{
|
||||
"error": "task 3: False is not callable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "get_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"max_workers": 4,
|
||||
"pool_threads": 4,
|
||||
"active_threads": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "ParallelProcessor.get_status",
|
||||
"recv": {
|
||||
"cls": "ParallelProcessor",
|
||||
"max_workers": 1
|
||||
},
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"max_workers": 1,
|
||||
"pool_threads": 1,
|
||||
"active_threads": 4
|
||||
},
|
||||
"max_workers": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "ParallelProcessor.get_status",
|
||||
"recv": {
|
||||
"cls": "ParallelProcessor",
|
||||
"max_workers": 9
|
||||
},
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"result": {
|
||||
"max_workers": 9,
|
||||
"pool_threads": 9,
|
||||
"active_threads": 4
|
||||
},
|
||||
"max_workers": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_pp_get_processor_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"max_workers": 4,
|
||||
"is_singleton": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_pp_shutdown_then_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"shutdown_returned_none": true,
|
||||
"status": {
|
||||
"max_workers": 3,
|
||||
"pool_threads": 3,
|
||||
"active_threads": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_pp_shutdown_then_run",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"raised": "cannot schedule new futures after shutdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_pp_new_bad",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"raised": "max_workers must be greater than 0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_pp_shutdown_processor_cycle",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"processor_is_none": true,
|
||||
"recreated_max_workers": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "test_parallel",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package main
|
||||
|
||||
// truth_test.go — the parity gate. Replays every case from truth.json
|
||||
// (the shared freeze_truth.py envelope: pinned thread-count seam + Python
|
||||
// oracle cases WITH inputs, generated by scripts/gen_parallel_processor_truth.py
|
||||
// against the live oracle under scripts/pp_freeze_seams) VERBATIM through
|
||||
// the ported Go functions, in the recorded call order (rows touch the
|
||||
// global processor lifecycle). A divergence fails the port, not the oracle.
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed truth.json
|
||||
var frozenTruthJSON string
|
||||
|
||||
// TestFrozenTruthReplay — the frozen oracle truth, verbatim, against the
|
||||
// ported functions.
|
||||
func TestFrozenTruthReplay(t *testing.T) {
|
||||
t.Setenv("PP_ACTIVE_THREADS", "4")
|
||||
fails, count := runTruthBytes([]byte(frozenTruthJSON))
|
||||
if count == 0 {
|
||||
t.Fatal("no truth cases — the parity gate is empty")
|
||||
}
|
||||
for _, f := range fails {
|
||||
t.Error(f)
|
||||
}
|
||||
if len(fails) == 0 {
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunParallelOrderAndErrorEnvelopes — submission-order results under
|
||||
// staggered completion, and the oracle's error-envelope degradation.
|
||||
func TestRunParallelOrderAndErrorEnvelopes(t *testing.T) {
|
||||
t.Setenv("PP_ACTIVE_THREADS", "4")
|
||||
p := NewParallelProcessor(4)
|
||||
got := p.RunParallel([]interface{}{
|
||||
specCallable{fn: "sleepret", ms: 60, ret: "a"},
|
||||
specCallable{fn: "sleepret", ms: 10, ret: "b"},
|
||||
specCallable{fn: "boom", msg: "kaput"},
|
||||
specCallable{fn: "const", ret: 7},
|
||||
42, // non-callable -> worker TypeError text
|
||||
})
|
||||
want := `[` +
|
||||
`"a",` +
|
||||
`"b",` +
|
||||
`{"error":"kaput"},` +
|
||||
`7,` +
|
||||
`{"error":"'int' object is not callable"}]`
|
||||
if s := mustJSON(got); s != want {
|
||||
t.Errorf("RunParallel:\n got %s\nwant %s", s, want)
|
||||
}
|
||||
if got := p.RunParallel([]interface{}{}); mustJSON(got) != "[]" {
|
||||
t.Errorf("empty tasks: got %s want []", mustJSON(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunParallelWithArgsBranchMatrix — every parse branch's exact
|
||||
// Python-shaped error string and normalization rule.
|
||||
func TestRunParallelWithArgsBranchMatrix(t *testing.T) {
|
||||
t.Setenv("PP_ACTIVE_THREADS", "4")
|
||||
p := NewParallelProcessor(4)
|
||||
got := p.RunParallelWithArgs([]ArgsTask{
|
||||
{Kind: "call", Fn: "add", Args: []interface{}{json.Number("1"), json.Number("2")}},
|
||||
{Kind: "call", Fn: "add", Args: []interface{}{json.Number("1.5"), json.Number("2")}},
|
||||
{Kind: "call", Fn: "echo"},
|
||||
{Kind: "raw", Value: json.Number("42")},
|
||||
{Kind: "raw", Value: "x"},
|
||||
{Kind: "raw", Value: nil},
|
||||
{Kind: "emptytuple"},
|
||||
{Kind: "notcall", Value: json.Number("42")},
|
||||
{Kind: "notcall", Value: "x"},
|
||||
{Kind: "notcall", Value: nil},
|
||||
{Kind: "notcall", Value: true},
|
||||
{Kind: "notcall", Value: json.Number("4.5")},
|
||||
{Kind: "notcall", Value: []interface{}{json.Number("1"), json.Number("2")}},
|
||||
{Kind: "argsnotseq", Fn: "echo", Value: "ab"},
|
||||
{Kind: "argsnotseq", Fn: "echo", Value: json.Number("5")},
|
||||
{Kind: "kwnotdict", Fn: "echo", Value: []interface{}{"a"}},
|
||||
})
|
||||
want := `[3,3.5,{"args":[],"kwargs":{}},` +
|
||||
`{"error":"task 3: expected tuple, got int"},` +
|
||||
`{"error":"task 4: expected tuple, got str"},` +
|
||||
`{"error":"task 5: expected tuple, got NoneType"},` +
|
||||
`{"error":"task 6: expected tuple, got tuple"},` +
|
||||
`{"error":"task 7: 42 is not callable"},` +
|
||||
`{"error":"task 8: 'x' is not callable"},` +
|
||||
`{"error":"task 9: None is not callable"},` +
|
||||
`{"error":"task 10: True is not callable"},` +
|
||||
`{"error":"task 11: 4.5 is not callable"},` +
|
||||
`{"error":"task 12: [1, 2] is not callable"},` +
|
||||
`{"args":["ab"],"kwargs":{}},` +
|
||||
`{"args":[5],"kwargs":{}},` +
|
||||
`{"args":[],"kwargs":{}}]`
|
||||
if s := mustJSON(got); s != want {
|
||||
t.Errorf("RunParallelWithArgs:\n got %s\nwant %s", s, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoolLifecycle — constructor crash parity, submit-after-shutdown
|
||||
// crash parity, and the global processor's singleton/reset cycle.
|
||||
func TestPoolLifecycle(t *testing.T) {
|
||||
t.Setenv("PP_ACTIVE_THREADS", "4")
|
||||
|
||||
if raised := recoverStr(func() { NewParallelProcessor(0) }); raised != "max_workers must be greater than 0" {
|
||||
t.Errorf("NewParallelProcessor(0): got %q want max_workers must be greater than 0", raised)
|
||||
}
|
||||
|
||||
p := NewParallelProcessor(2)
|
||||
p.Shutdown(true)
|
||||
if raised := recoverStr(func() { p.RunParallel([]interface{}{specCallable{fn: "const", ret: 1}}) }); raised != "cannot schedule new futures after shutdown" {
|
||||
t.Errorf("RunParallel after Shutdown: got %q want cannot schedule new futures after shutdown", raised)
|
||||
}
|
||||
// get_status still works on a shut-down processor (the oracle reads
|
||||
// only max_workers/active_count)
|
||||
if s := mustJSON(p.GetStatus()); s != `{"active_threads":4,"max_workers":2,"pool_threads":2}` {
|
||||
t.Errorf("GetStatus after Shutdown: got %s", s)
|
||||
}
|
||||
|
||||
a, b := GetProcessor(), GetProcessor()
|
||||
if a != b {
|
||||
t.Error("GetProcessor must return the singleton")
|
||||
}
|
||||
ShutdownProcessor()
|
||||
c := GetProcessor()
|
||||
if c == a {
|
||||
t.Error("ShutdownProcessor must clear the global for lazy re-create")
|
||||
}
|
||||
if s := mustJSON(c.GetStatus()); s != `{"active_threads":4,"max_workers":4,"pool_threads":4}` {
|
||||
t.Errorf("re-created global GetStatus: got %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoolBoundedAtMaxWorkers — concurrency is capped at max_workers:
|
||||
// with 1 worker, staggered tasks serialize (the slowest-first set takes
|
||||
// at least the sum of sleeps, not the max).
|
||||
func TestPoolBoundedAtMaxWorkers(t *testing.T) {
|
||||
p := NewParallelProcessor(1)
|
||||
start := time.Now()
|
||||
p.RunParallel([]interface{}{
|
||||
specCallable{fn: "sleepret", ms: 40, ret: "a"},
|
||||
specCallable{fn: "sleepret", ms: 40, ret: "b"},
|
||||
})
|
||||
if elapsed := time.Since(start); elapsed < 75*time.Millisecond {
|
||||
t.Errorf("single-worker pool ran tasks concurrently: %v", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func recoverStr(f func()) (msg string) {
|
||||
defer func() { msg = pyExcStr(recover()) }()
|
||||
f()
|
||||
return ""
|
||||
}
|
||||
|
||||
func mustJSON(v interface{}) string {
|
||||
c, err := canonical(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.TrimSpace(mustMarshal(c))
|
||||
}
|
||||
|
||||
func mustMarshal(v interface{}) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
// chat.go — the R2 W5b chat surface (Go CLI extra beyond the Python
|
||||
// oracle's generate; kept for the existing CLI contract).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (reg *ProviderRegistry) chat(providerName, model, message string) (string, error) {
|
||||
p := reg.GetProvider(providerName)
|
||||
if p == nil {
|
||||
return "", fmt.Errorf("provider %q not found", providerName)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 300 * time.Second}
|
||||
|
||||
var requestBody map[string]any
|
||||
var url string
|
||||
|
||||
switch p.ProviderType {
|
||||
case "ollama":
|
||||
url = p.URL + "/api/chat"
|
||||
requestBody = map[string]any{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": message}},
|
||||
"stream": false,
|
||||
}
|
||||
case "openai":
|
||||
url = p.URL + "/v1/chat/completions"
|
||||
requestBody = map[string]any{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": message}},
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported provider type: %s", p.ProviderType)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if p.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
// Parse response based on provider type
|
||||
var result map[string]any
|
||||
json.Unmarshal(body, &result)
|
||||
if content, ok := result["content"].(string); ok {
|
||||
return content, nil // Ollama format
|
||||
}
|
||||
if choices, ok := result["choices"].([]any); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]any); ok {
|
||||
if msg, ok := choice["message"].(map[string]any); ok {
|
||||
if content, ok := msg["content"].(string); ok {
|
||||
return content, nil // [OI] format
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package main
|
||||
|
||||
// format.go — factory batch 5: the tool-format surfaces of
|
||||
// provider_registry.py (FormatToolsOpenAI / FormatToolsAnthropic — the
|
||||
// provider-facing adapters; pure shape transforms).
|
||||
// format.go — factory batch 5 + factory-gap-pr: the tool-format surfaces
|
||||
// of provider_registry.py (_format_tools_openai / _format_tools_anthropic —
|
||||
// the provider-facing adapters; pure shape transforms).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FormatToolsOpenAI — [OI] API shape.
|
||||
func FormatToolsOpenAI(tools []map[string]interface{}) []map[string]interface{} {
|
||||
// FormatTools — the _format_tools_openai core ([OI] API shape).
|
||||
func FormatTools(tools []map[string]interface{}) []map[string]interface{} {
|
||||
out := make([]map[string]interface{}, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
name, _ := tool["name"].(string)
|
||||
@@ -31,6 +31,11 @@ func FormatToolsOpenAI(tools []map[string]interface{}) []map[string]interface{}
|
||||
return out
|
||||
}
|
||||
|
||||
// FormatToolsOpenai — _format_tools_openai parity name.
|
||||
func FormatToolsOpenai(tools []map[string]interface{}) []map[string]interface{} {
|
||||
return FormatTools(tools)
|
||||
}
|
||||
|
||||
// FormatToolsAnthropic — Anthropic API shape.
|
||||
func FormatToolsAnthropic(tools []map[string]interface{}) []map[string]interface{} {
|
||||
out := make([]map[string]interface{}, 0, len(tools))
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// provider-registry — Go provider registry (R2 W5b deep port).
|
||||
// provider-registry — Go provider registry (R2 W5b deep port,
|
||||
// factory-gap-pr gap-closure: the full provider_registry.py surface).
|
||||
// PARITY port of src/rune/provider_registry.py: connect, disconnect,
|
||||
// list_providers, get_provider, list_models (Ollama + OpenAI + Anthropic).
|
||||
// list_providers, get_provider, list_models (Ollama + [OI] + Anthropic),
|
||||
// generate (Ollama + [OI] + Anthropic with native tool calling),
|
||||
// _ctx_budget, get_registry, connect_provider, list_all_models.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
@@ -9,190 +12,24 @@
|
||||
// provider-registry list
|
||||
// provider-registry models <name>
|
||||
// provider-registry chat <provider-name> <model> <message>
|
||||
// provider-registry generate <prompt> [--provider <name>] [--model <model>]
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ModelProvider struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
ProviderType string `json:"provider_type"` // openai, ollama, anthropic
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type ProviderRegistry struct {
|
||||
mu sync.RWMutex
|
||||
Providers map[string]ModelProvider `json:"providers"`
|
||||
ConfigPath string `json:"config_path"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
home := os.Getenv("RUNE_MEMORY_DIR")
|
||||
if home == "" {
|
||||
home = os.Getenv("HOME") + "/.rune"
|
||||
}
|
||||
return home + "/provider_registry.json"
|
||||
}
|
||||
|
||||
func loadRegistry() *ProviderRegistry {
|
||||
reg := &ProviderRegistry{Providers: make(map[string]ModelProvider)}
|
||||
if data, err := os.ReadFile(configPath()); err == nil {
|
||||
json.Unmarshal(data, reg)
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
func saveRegistry(reg *ProviderRegistry) {
|
||||
// Marshal ONLY durable state — the embedded sync.RWMutex must never be
|
||||
// copied (go vet copies-lock) and is not persisted state.
|
||||
data, _ := json.MarshalIndent(struct {
|
||||
Providers map[string]ModelProvider `json:"providers"`
|
||||
ConfigPath string `json:"config_path"`
|
||||
}{Providers: reg.Providers, ConfigPath: reg.ConfigPath}, "", " ")
|
||||
os.WriteFile(configPath(), data, 0600)
|
||||
}
|
||||
|
||||
func (reg *ProviderRegistry) connect(name, url, providerType, apiKey string) ModelProvider {
|
||||
provider := ModelProvider{
|
||||
Name: name, URL: url, ProviderType: providerType,
|
||||
APIKey: apiKey, Active: true,
|
||||
}
|
||||
reg.mu.Lock()
|
||||
reg.Providers[name] = provider
|
||||
reg.mu.Unlock()
|
||||
saveRegistry(reg)
|
||||
return provider
|
||||
}
|
||||
|
||||
func (reg *ProviderRegistry) fetchModels(provider ModelProvider) ([]string, error) {
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
var models []string
|
||||
|
||||
switch provider.ProviderType {
|
||||
case "ollama":
|
||||
resp, err := client.Get(provider.URL + "/api/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result struct {
|
||||
Models []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"models"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
for _, m := range result.Models {
|
||||
models = append(models, cleanModelName(m.Name))
|
||||
}
|
||||
case "openai", "anthropic":
|
||||
req, _ := http.NewRequest("GET", provider.URL+"/v1/models", nil)
|
||||
if provider.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.APIKey)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
for _, m := range result.Data {
|
||||
models = append(models, cleanModelName(m.ID))
|
||||
}
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func cleanModelName(name string) string {
|
||||
// Strip common prefixes
|
||||
name = strings.TrimPrefix(name, "models/")
|
||||
return name
|
||||
}
|
||||
|
||||
func (reg *ProviderRegistry) chat(providerName, model, message string) (string, error) {
|
||||
reg.mu.RLock()
|
||||
provider, ok := reg.Providers[providerName]
|
||||
reg.mu.RUnlock()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("provider %q not found", providerName)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 300 * time.Second}
|
||||
|
||||
var requestBody map[string]interface{}
|
||||
var url string
|
||||
|
||||
switch provider.ProviderType {
|
||||
case "ollama":
|
||||
url = provider.URL + "/api/chat"
|
||||
requestBody = map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": message}},
|
||||
"stream": false,
|
||||
}
|
||||
case "openai":
|
||||
url = provider.URL + "/v1/chat/completions"
|
||||
requestBody = map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": message}},
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported provider type: %s", provider.ProviderType)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if provider.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.APIKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
// Parse response based on provider type
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(body, &result)
|
||||
if content, ok := result["content"].(string); ok {
|
||||
return content, nil // Ollama format
|
||||
}
|
||||
if choices, ok := result["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if msg, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := msg["content"].(string); ok {
|
||||
return content, nil // OpenAI format
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(body), nil
|
||||
return NewRegistry(defaultConfigPath())
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry <connect|disconnect|list|models|chat> [args...]")
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry <connect|disconnect|list|models|chat|generate> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
reg := loadRegistry()
|
||||
@@ -206,7 +43,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
name, url := args[0], args[1]
|
||||
providerType := "ollama"
|
||||
providerType := "openai" // the Python oracle's connect() default
|
||||
apiKey := ""
|
||||
if len(args) > 2 {
|
||||
providerType = args[2]
|
||||
@@ -214,8 +51,8 @@ func main() {
|
||||
if len(args) > 3 {
|
||||
apiKey = args[3]
|
||||
}
|
||||
provider := reg.connect(name, url, providerType, apiKey)
|
||||
out, _ := json.MarshalIndent(provider, "", " ")
|
||||
provider := reg.Connect(name, url, providerType, apiKey)
|
||||
out, _ := json.MarshalIndent(provider.ToDict(), "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "disconnect":
|
||||
@@ -223,14 +60,11 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry disconnect <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
reg.mu.Lock()
|
||||
delete(reg.Providers, args[0])
|
||||
reg.mu.Unlock()
|
||||
saveRegistry(reg)
|
||||
reg.Disconnect(args[0])
|
||||
fmt.Println("disconnected:", args[0])
|
||||
|
||||
case "list":
|
||||
for _, p := range reg.Providers {
|
||||
for _, p := range reg.ListProviders() {
|
||||
out, _ := json.Marshal(p)
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
@@ -240,12 +74,12 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry models <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
provider, ok := reg.Providers[args[0]]
|
||||
if !ok {
|
||||
provider := reg.GetProvider(args[0])
|
||||
if provider == nil {
|
||||
fmt.Println("null")
|
||||
return
|
||||
}
|
||||
models, err := reg.fetchModels(provider)
|
||||
models, err := reg.FetchModels(provider)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -266,6 +100,31 @@ func main() {
|
||||
out, _ := json.Marshal(map[string]string{"response": response})
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "generate":
|
||||
fs := flag.NewFlagSet("generate", flag.ContinueOnError)
|
||||
providerName := fs.String("provider", "", "provider name")
|
||||
model := fs.String("model", "", "model id")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry generate <prompt> [--provider <name>] [--model <model>]")
|
||||
os.Exit(1)
|
||||
}
|
||||
if fs.NArg() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry generate <prompt> [--provider <name>] [--model <model>]")
|
||||
os.Exit(1)
|
||||
}
|
||||
result := reg.Generate(fs.Arg(0), *providerName, *model, 500, 0.7, nil, nil)
|
||||
if errText, ok := result["error"]; ok {
|
||||
fmt.Printf("Error: %v\n", errText)
|
||||
return
|
||||
}
|
||||
resp := pyStrOr(result["response"], "")
|
||||
if len(resp) > 200 {
|
||||
resp = resp[:200]
|
||||
}
|
||||
fmt.Printf("Response: %s...\n", resp)
|
||||
fmt.Printf("Model: %v (%v)\n", result["model"], result["provider"])
|
||||
fmt.Printf("Latency: %vms\n", result["latency_ms"])
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
package main
|
||||
|
||||
// pyjson.go — Python json.dumps-compatible serialization for the
|
||||
// tool-result `content` strings (json.dumps defaults: ", " / ": "
|
||||
// separators, ensure_ascii=True) plus an order-preserving JSON parser
|
||||
// so batteries can replay Python insertion-ordered objects verbatim.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// PyDict is an insertion-ordered object (Python dict semantics).
|
||||
type PyDict struct {
|
||||
Keys []string
|
||||
Vals map[string]any
|
||||
}
|
||||
|
||||
func NewPyDict() *PyDict { return &PyDict{Vals: map[string]any{}} }
|
||||
|
||||
func (d *PyDict) Set(k string, v any) {
|
||||
if _, ok := d.Vals[k]; !ok {
|
||||
d.Keys = append(d.Keys, k)
|
||||
}
|
||||
d.Vals[k] = v
|
||||
}
|
||||
|
||||
func (d *PyDict) Get(k string) (any, bool) {
|
||||
v, ok := d.Vals[k]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// PyDump — json.dumps(obj) with Python's default separators and
|
||||
// ensure_ascii escaping. Maps serialize with sorted keys (Go map law);
|
||||
// use PyDict where Python insertion order must be reproduced.
|
||||
func PyDump(v any) string {
|
||||
s, _ := pyDumpTry(v)
|
||||
return s
|
||||
}
|
||||
|
||||
// pyDumpTry reports (ok=false) when the value has no Python-JSON
|
||||
// equivalent — the caller applies the oracle's degrade law.
|
||||
func pyDumpTry(v any) (string, bool) {
|
||||
var b strings.Builder
|
||||
if err := pyWrite(&b, v); err != nil {
|
||||
return "", false
|
||||
}
|
||||
return b.String(), true
|
||||
}
|
||||
|
||||
func pyWrite(b *strings.Builder, v any) error {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
b.WriteString("null")
|
||||
case bool:
|
||||
if x {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
pyWriteString(b, x)
|
||||
case int:
|
||||
b.WriteString(strconv.Itoa(x))
|
||||
case int64:
|
||||
b.WriteString(strconv.FormatInt(x, 10))
|
||||
case float64:
|
||||
b.WriteString(pyFloat(x))
|
||||
case json.Number:
|
||||
b.WriteString(x.String())
|
||||
case *PyDict:
|
||||
b.WriteByte('{')
|
||||
for i, k := range x.Keys {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
pyWriteString(b, k)
|
||||
b.WriteString(": ")
|
||||
pyWrite(b, x.Vals[k])
|
||||
}
|
||||
b.WriteByte('}')
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sortStrings(keys)
|
||||
b.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
pyWriteString(b, k)
|
||||
b.WriteString(": ")
|
||||
pyWrite(b, x[k])
|
||||
}
|
||||
b.WriteByte('}')
|
||||
case []any:
|
||||
b.WriteByte('[')
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
if err := pyWrite(b, e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.WriteByte(']')
|
||||
case []map[string]any:
|
||||
b.WriteByte('[')
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
if err := pyWrite(b, e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.WriteByte(']')
|
||||
default:
|
||||
// Unknown Go types have no Python json equivalent; the caller
|
||||
// applies the oracle's degrade law.
|
||||
return fmt.Errorf("type not JSON-serializable: %T", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const pyNonSerializable = "tool result not JSON serializable"
|
||||
|
||||
func pyFloat(f float64) string {
|
||||
if f == math.Trunc(f) && math.Abs(f) < 1e15 {
|
||||
return strconv.FormatFloat(f, 'f', 1, 64) // Python repr(2.0) → "2.0"
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
func pyWriteString(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 {
|
||||
writeUnicodeEscape(b, r)
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
func writeUnicodeEscape(b *strings.Builder, r rune) {
|
||||
if r > 0xffff {
|
||||
hi, lo := utf16.EncodeRune(r)
|
||||
fmt.Fprintf(b, "\\u%04x\\u%04x", hi, lo)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(b, "\\u%04x", r)
|
||||
}
|
||||
|
||||
func sortStrings(s []string) {
|
||||
for i := 1; i < len(s); i++ {
|
||||
for j := i; j > 0 && s[j] < s[j-1]; j-- {
|
||||
s[j], s[j-1] = s[j-1], s[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParsePyJSON — order-preserving JSON decode: objects → *PyDict,
|
||||
// arrays → []any, integral numbers → int, others → float64.
|
||||
func ParsePyJSON(data []byte) (any, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(string(data)))
|
||||
dec.UseNumber()
|
||||
v, err := decodePyValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodePyValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodePyToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodePyToken(dec *json.Decoder, tok json.Token) (any, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
d := NewPyDict()
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
val, err := decodePyValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing }
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
case '[':
|
||||
arr := []any{}
|
||||
for dec.More() {
|
||||
val, err := decodePyValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing ]
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
case json.Number:
|
||||
if n, err := t.Int64(); err == nil && !strings.ContainsAny(t.String(), ".eE") {
|
||||
return int(n), nil
|
||||
}
|
||||
f, err := t.Float64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
case string, bool, nil:
|
||||
return tok, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected token %v", tok)
|
||||
}
|
||||
@@ -0,0 +1,853 @@
|
||||
package main
|
||||
|
||||
// registry.go — factory-gap-pr: the provider_registry.py surface that
|
||||
// main.go's R2 W5b port left uncovered (dichotomy wave #2479 tranche).
|
||||
// Name-preserving parity port: every fn mirrors the Python oracle 1:1
|
||||
// (_load_config, _save_config, connect, disconnect, list_providers,
|
||||
// get_provider, list_models, _fetch_models, _fetch_ollama_models,
|
||||
// _fetch_openai_models, _fetch_anthropic_models, _clean_model_name,
|
||||
// to_dict, generate, _generate_ollama, _generate_openai,
|
||||
// _generate_anthropic, get_registry, connect_provider, list_all_models,
|
||||
// _ctx_budget). Network IO sits behind the injectable `Do` seam; the
|
||||
// tool executor behind the ToolExecutorFunc seam — tests never touch a
|
||||
// socket or the real executor.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ModelProvider — parity shape of the Python ModelProvider dataclass.
|
||||
type ModelProvider struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
ProviderType string `json:"provider_type"` // "ollama", "openai", "anthropic", "custom"
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
Models []map[string]any `json:"models"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LastHealthCheck string `json:"last_health_check,omitempty"`
|
||||
HealthStatus string `json:"health_status"` // "healthy", "error", "unknown"
|
||||
}
|
||||
|
||||
func NewProvider(name, url, providerType, apiKey string) *ModelProvider {
|
||||
return &ModelProvider{
|
||||
Name: name, URL: url, ProviderType: providerType,
|
||||
APIKey: apiKey, Models: []map[string]any{},
|
||||
Enabled: true, HealthStatus: "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderFromFields rebuilds a provider from a truth-row field map
|
||||
// (the frozen snapshot shape of the Python dataclass).
|
||||
func ProviderFromFields(f map[string]any) *ModelProvider {
|
||||
p := NewProvider(pyStr(f["name"]), pyStr(f["url"]), pyStr(f["provider_type"]), pyStr(f["api_key"]))
|
||||
p.Enabled = pyBoolOr(f["enabled"], true)
|
||||
p.LastHealthCheck = pyStr(f["last_health_check"])
|
||||
p.HealthStatus = pyStrOr(f["health_status"], "unknown")
|
||||
if models, ok := f["models"].([]any); ok {
|
||||
p.Models = []map[string]any{}
|
||||
for _, m := range models {
|
||||
if mm, ok := m.(map[string]any); ok {
|
||||
p.Models = append(p.Models, mm)
|
||||
}
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ToDict — ModelProvider.to_dict (api_key and last_health_check are
|
||||
// deliberately excluded — the Python oracle's masking law).
|
||||
func (p *ModelProvider) ToDict() map[string]any {
|
||||
models := p.Models
|
||||
if models == nil {
|
||||
models = []map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"name": p.Name, "url": p.URL, "provider_type": p.ProviderType,
|
||||
"models": models, "enabled": p.Enabled,
|
||||
"health_status": p.HealthStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderRegistry — parity shape of the Python ProviderRegistry.
|
||||
type ProviderRegistry struct {
|
||||
mu sync.RWMutex
|
||||
Providers map[string]*ModelProvider
|
||||
ConfigPath string
|
||||
order []string // insertion order (Python dict semantics)
|
||||
// Do is the injectable HTTP seam: (url, body, headers, timeout) →
|
||||
// response body. nil = real net/http transport.
|
||||
Do func(url string, body []byte, headers map[string]string, timeout time.Duration) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewRegistry(configPath string) *ProviderRegistry {
|
||||
reg := &ProviderRegistry{Providers: map[string]*ModelProvider{}, ConfigPath: configPath}
|
||||
reg.LoadConfig()
|
||||
return reg
|
||||
}
|
||||
|
||||
// defaultConfigPath — the Python oracle's ~/.config/rune/providers.json.
|
||||
// RUNE_REGISTRY_CONFIG overrides (tests); RUNE_MEMORY_DIR keeps the
|
||||
// legacy Go CLI location working.
|
||||
func defaultConfigPath() string {
|
||||
if p := os.Getenv("RUNE_REGISTRY_CONFIG"); p != "" {
|
||||
return p
|
||||
}
|
||||
if home := os.Getenv("RUNE_MEMORY_DIR"); home != "" {
|
||||
return filepath.Join(home, "provider_registry.json")
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "providers.json"
|
||||
}
|
||||
return filepath.Join(home, ".config", "rune", "providers.json")
|
||||
}
|
||||
|
||||
// providerFromConfigMap — the _load_config per-provider defaults law:
|
||||
// url="", type="custom", api_key=None, enabled=True.
|
||||
func providerFromConfigMap(name string, pc map[string]any) *ModelProvider {
|
||||
p := NewProvider(name, pyStrOr(pc["url"], ""), pyStrOr(pc["type"], "custom"), pyStr(pc["api_key"]))
|
||||
p.Enabled = pyBoolOr(pc["enabled"], true)
|
||||
return p
|
||||
}
|
||||
|
||||
// LoadConfig — ProviderRegistry._load_config. Missing file → empty
|
||||
// registry; malformed JSON → warning + empty (best-effort degrade).
|
||||
// Provider key order from the file is preserved (Python dict law).
|
||||
func (reg *ProviderRegistry) LoadConfig() {
|
||||
data, err := os.ReadFile(reg.ConfigPath)
|
||||
if err != nil {
|
||||
return // missing config is not an error — empty registry
|
||||
}
|
||||
keys, cfgs, err := decodeOrderedProviders(data)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Could not load provider config: %v\n", err)
|
||||
return
|
||||
}
|
||||
reg.mu.Lock()
|
||||
defer reg.mu.Unlock()
|
||||
reg.order = keys
|
||||
for _, name := range keys {
|
||||
reg.Providers[name] = providerFromConfigMap(name, cfgs[name])
|
||||
}
|
||||
}
|
||||
|
||||
// decodeOrderedProviders walks the JSON token stream so the providers'
|
||||
// file order survives (Go map decode would discard it).
|
||||
func decodeOrderedProviders(data []byte) ([]string, map[string]map[string]any, error) {
|
||||
cfgs := map[string]map[string]any{}
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if delim, ok := tok.(json.Delim); !ok || delim != '{' {
|
||||
return nil, nil, fmt.Errorf("config root is not an object")
|
||||
}
|
||||
var keys []string
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
if key == "providers" {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if delim, ok := tok.(json.Delim); !ok || delim != '{' {
|
||||
return nil, nil, fmt.Errorf("providers section is not an object")
|
||||
}
|
||||
for dec.More() {
|
||||
nameTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
name, _ := nameTok.(string)
|
||||
var pc map[string]any
|
||||
if err := dec.Decode(&pc); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if _, seen := cfgs[name]; !seen {
|
||||
keys = append(keys, name)
|
||||
}
|
||||
cfgs[name] = pc
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing }
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
var skip any
|
||||
if err := dec.Decode(&skip); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys, cfgs, nil
|
||||
}
|
||||
|
||||
// SaveConfig — ProviderRegistry._save_config: API keys are masked with
|
||||
// "***" (never persisted in the clear), absent keys persist as null.
|
||||
func (reg *ProviderRegistry) SaveConfig() error {
|
||||
os.MkdirAll(filepath.Dir(reg.ConfigPath), 0o755)
|
||||
providers := map[string]any{}
|
||||
for name, p := range reg.Providers {
|
||||
var apiKey any
|
||||
if p.APIKey != "" {
|
||||
apiKey = "***"
|
||||
}
|
||||
providers[name] = map[string]any{
|
||||
"url": p.URL, "type": p.ProviderType,
|
||||
"api_key": apiKey, "enabled": p.Enabled,
|
||||
}
|
||||
}
|
||||
data, err := json.MarshalIndent(map[string]any{"providers": providers}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(reg.ConfigPath, data, 0o600)
|
||||
}
|
||||
|
||||
// Connect — ProviderRegistry.connect (persists through SaveConfig).
|
||||
func (reg *ProviderRegistry) Connect(name, url, providerType, apiKey string) *ModelProvider {
|
||||
p := NewProvider(name, url, providerType, apiKey)
|
||||
reg.mu.Lock()
|
||||
if _, exists := reg.Providers[name]; !exists {
|
||||
reg.order = append(reg.order, name)
|
||||
}
|
||||
reg.Providers[name] = p
|
||||
reg.mu.Unlock()
|
||||
reg.SaveConfig()
|
||||
return p
|
||||
}
|
||||
|
||||
// Disconnect — ProviderRegistry.disconnect: absent name is a no-op.
|
||||
func (reg *ProviderRegistry) Disconnect(name string) {
|
||||
reg.mu.Lock()
|
||||
_, ok := reg.Providers[name]
|
||||
if ok {
|
||||
delete(reg.Providers, name)
|
||||
for i, n := range reg.order {
|
||||
if n == name {
|
||||
reg.order = append(reg.order[:i], reg.order[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
reg.mu.Unlock()
|
||||
if ok {
|
||||
reg.SaveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// orderedNames — the Python dict law: providers iterate in insertion
|
||||
// order (connect sequence, then config-file order on load).
|
||||
func (reg *ProviderRegistry) orderedNames() []string {
|
||||
reg.mu.RLock()
|
||||
defer reg.mu.RUnlock()
|
||||
names := make([]string, 0, len(reg.order))
|
||||
seen := map[string]bool{}
|
||||
for _, n := range reg.order {
|
||||
if _, ok := reg.Providers[n]; ok && !seen[n] {
|
||||
names = append(names, n)
|
||||
seen[n] = true
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// ListProviders — ProviderRegistry.list_providers.
|
||||
func (reg *ProviderRegistry) ListProviders() []map[string]any {
|
||||
out := []map[string]any{}
|
||||
for _, name := range reg.orderedNames() {
|
||||
out = append(out, reg.Providers[name].ToDict())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetProvider — ProviderRegistry.get_provider (nil when absent).
|
||||
func (reg *ProviderRegistry) GetProvider(name string) *ModelProvider {
|
||||
reg.mu.RLock()
|
||||
defer reg.mu.RUnlock()
|
||||
return reg.Providers[name]
|
||||
}
|
||||
|
||||
// ListModels — ProviderRegistry.list_models: enabled providers only,
|
||||
// per-provider fetch errors degrade to a warning (never crash), and
|
||||
// every model dict is annotated with provider + provider_type.
|
||||
// Phase B: fetches fan out concurrently; results and warnings are
|
||||
// assembled in registry order, so output stays byte-identical to the
|
||||
// sequential oracle regardless of goroutine completion order.
|
||||
func (reg *ProviderRegistry) ListModels() []map[string]any {
|
||||
type fetch struct {
|
||||
p *ModelProvider
|
||||
models []map[string]any
|
||||
err error
|
||||
}
|
||||
fetches := []fetch{}
|
||||
for _, name := range reg.orderedNames() {
|
||||
if p := reg.Providers[name]; p != nil && p.Enabled {
|
||||
fetches = append(fetches, fetch{p: p})
|
||||
}
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := range fetches {
|
||||
wg.Add(1)
|
||||
go func(f *fetch) {
|
||||
defer wg.Done()
|
||||
f.models, f.err = reg.FetchModels(f.p)
|
||||
}(&fetches[i])
|
||||
}
|
||||
wg.Wait()
|
||||
all := []map[string]any{}
|
||||
for _, f := range fetches {
|
||||
if f.err != nil {
|
||||
fmt.Printf("Warning: Could not fetch models from %s: %v\n", f.p.Name, f.err)
|
||||
continue
|
||||
}
|
||||
for _, m := range f.models {
|
||||
m["provider"] = f.p.Name
|
||||
m["provider_type"] = f.p.ProviderType
|
||||
all = append(all, m)
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// FetchModels — ProviderRegistry._fetch_models dispatch.
|
||||
func (reg *ProviderRegistry) FetchModels(p *ModelProvider) ([]map[string]any, error) {
|
||||
switch p.ProviderType {
|
||||
case "ollama":
|
||||
return reg.FetchOllamaModels(p)
|
||||
case "openai":
|
||||
return reg.FetchOpenaiModels(p)
|
||||
case "anthropic":
|
||||
return reg.FetchAnthropicModels(p)
|
||||
default:
|
||||
return []map[string]any{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// FetchOllamaModels — ProviderRegistry._fetch_ollama_models. Every
|
||||
// failure (transport, decode, missing "name") is wrapped into the
|
||||
// oracle's single "Ollama connection failed: …" error.
|
||||
func (reg *ProviderRegistry) FetchOllamaModels(p *ModelProvider) ([]map[string]any, error) {
|
||||
body, err := reg.httpDo(p.URL+"/api/tags", nil, nil, 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Ollama connection failed: %v", err)
|
||||
}
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, fmt.Errorf("Ollama connection failed: %v", err)
|
||||
}
|
||||
models := []map[string]any{}
|
||||
rawModels, _ := data["models"].([]any)
|
||||
for _, rm := range rawModels {
|
||||
m, _ := rm.(map[string]any)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
nameVal, ok := m["name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Ollama connection failed: %v", missingKeyErr("name"))
|
||||
}
|
||||
details, _ := m["details"].(map[string]any)
|
||||
if details == nil {
|
||||
details = map[string]any{}
|
||||
}
|
||||
models = append(models, map[string]any{
|
||||
"name": pyStr(nameVal),
|
||||
"display_name": CleanModelName(pyStr(nameVal)),
|
||||
"size_gb": roundHalfEven(pyFloatOr(m["size"], 0)/(1024*1024*1024), 1),
|
||||
"family": pyStrOr(details["family"], "unknown"),
|
||||
"parameters": pyStrOr(details["parameter_size"], "unknown"),
|
||||
})
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
// FetchOpenaiModels — ProviderRegistry._fetch_openai_models.
|
||||
func (reg *ProviderRegistry) FetchOpenaiModels(p *ModelProvider) ([]map[string]any, error) {
|
||||
headers := map[string]string{"Content-Type": "application/json"}
|
||||
if p.APIKey != "" {
|
||||
headers["Authorization"] = "Bearer " + p.APIKey
|
||||
}
|
||||
body, err := reg.httpDo(p.URL+"/v1/models", nil, headers, 10*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[OI] API connection failed: %v", err)
|
||||
}
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, fmt.Errorf("[OI] API connection failed: %v", err)
|
||||
}
|
||||
models := []map[string]any{}
|
||||
rawModels, _ := data["data"].([]any)
|
||||
for _, rm := range rawModels {
|
||||
m, _ := rm.(map[string]any)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
id := pyStrOr(m["id"], "unknown")
|
||||
models = append(models, map[string]any{
|
||||
"name": id, "display_name": id, "size_gb": 0,
|
||||
"family": "cloud", "parameters": "cloud",
|
||||
})
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
// FetchAnthropicModels — ProviderRegistry._fetch_anthropic_models: the
|
||||
// Anthropic API has no /v1/models, so the oracle returns its pinned
|
||||
// static list (ported verbatim).
|
||||
func (reg *ProviderRegistry) FetchAnthropicModels(p *ModelProvider) ([]map[string]any, error) {
|
||||
return []map[string]any{
|
||||
{"name": "claude-sonnet-4-20250514", "display_name": "Claude Sonnet 4", "size_gb": 0, "family": "cloud", "parameters": "cloud"},
|
||||
{"name": "claude-3-5-sonnet-20241022", "display_name": "Claude 3.5 Sonnet", "size_gb": 0, "family": "cloud", "parameters": "cloud"},
|
||||
{"name": "claude-3-5-haiku-20241022", "display_name": "Claude 3.5 Haiku", "size_gb": 0, "family": "cloud", "parameters": "cloud"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
cleanReFamilySize = regexp.MustCompile(`(?i)([A-Za-z]+[\d.]*)-?(\d+[BMb])`)
|
||||
cleanReFamilyVers = regexp.MustCompile(`(?i)([A-Za-z]+)-(\d+)-(\d+[BMb])`)
|
||||
)
|
||||
|
||||
// CleanModelName — the _clean_model_name delegation target
|
||||
// (model_connect._clean_model_name), ported 1:1.
|
||||
func CleanModelName(name string) string {
|
||||
if strings.Contains(name, "/") && (strings.Contains(name, "hf.co") || strings.Contains(name, "huggingface")) {
|
||||
parts := strings.Split(name, "/")
|
||||
modelPart := name
|
||||
if len(parts) >= 3 {
|
||||
modelPart = parts[len(parts)-1]
|
||||
}
|
||||
if strings.Contains(modelPart, ":") {
|
||||
modelPart = strings.Split(modelPart, ":")[0]
|
||||
}
|
||||
if m := cleanReFamilySize.FindStringSubmatch(modelPart); m != nil {
|
||||
return m[1] + " " + strings.ToUpper(m[2])
|
||||
}
|
||||
if m := cleanReFamilyVers.FindStringSubmatch(modelPart); m != nil {
|
||||
return m[1] + " " + m[2] + " " + strings.ToUpper(m[3])
|
||||
}
|
||||
for _, sep := range []string{"-", "_", "."} {
|
||||
if strings.Contains(modelPart, sep) {
|
||||
firstPart := strings.Split(modelPart, sep)[0]
|
||||
if len(firstPart) > 3 {
|
||||
return firstPart
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(modelPart) > 30 {
|
||||
return modelPart[:30]
|
||||
}
|
||||
return modelPart
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Generate — ProviderRegistry.generate: provider lookup (named or first
|
||||
// available), model autodetect via FetchModels, then the per-type
|
||||
// generate dispatch. Unsupported types yield an error dict.
|
||||
func (reg *ProviderRegistry) Generate(prompt string, providerName string, model string,
|
||||
maxTokens int, temperature float64, systemPrompt *string, tools []map[string]any) map[string]any {
|
||||
var p *ModelProvider
|
||||
if providerName != "" {
|
||||
p = reg.GetProvider(providerName)
|
||||
if p == nil {
|
||||
return map[string]any{"error": fmt.Sprintf("Provider not found: %s", providerName)}
|
||||
}
|
||||
} else {
|
||||
names := reg.orderedNames()
|
||||
if len(names) == 0 {
|
||||
return map[string]any{"error": "No providers connected"}
|
||||
}
|
||||
p = reg.Providers[names[0]]
|
||||
}
|
||||
if model == "" {
|
||||
models, err := reg.FetchModels(p)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error(), "model": model, "provider": p.Name}
|
||||
}
|
||||
if len(models) == 0 {
|
||||
return map[string]any{"error": fmt.Sprintf("No models available from %s", p.Name)}
|
||||
}
|
||||
model = pyStr(models[0]["name"])
|
||||
}
|
||||
switch p.ProviderType {
|
||||
case "ollama":
|
||||
return reg.GenerateOllama(p, prompt, model, maxTokens, temperature)
|
||||
case "openai":
|
||||
return reg.GenerateOpenai(p, prompt, model, maxTokens, temperature, systemPrompt, tools)
|
||||
case "anthropic":
|
||||
return reg.GenerateAnthropic(p, prompt, model, maxTokens, temperature, systemPrompt, tools)
|
||||
default:
|
||||
return map[string]any{"error": fmt.Sprintf("Unsupported provider type: %s", p.ProviderType)}
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateOllama — ProviderRegistry._generate_ollama. The num_ctx is
|
||||
// model-aware (_ctx_budget parity — never a hardcoded 8192).
|
||||
func (reg *ProviderRegistry) GenerateOllama(p *ModelProvider, prompt, model string,
|
||||
maxTokens int, temperature float64) map[string]any {
|
||||
start := time.Now()
|
||||
payload := map[string]any{
|
||||
"model": model, "prompt": prompt, "stream": false,
|
||||
"options": map[string]any{
|
||||
"temperature": temperature, "num_predict": maxTokens,
|
||||
"num_ctx": CtxBudget(model),
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
respBody, err := reg.httpDo(p.URL+"/api/generate", body,
|
||||
map[string]string{"Content-Type": "application/json"}, 120*time.Second)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error(), "model": model, "provider": p.Name}
|
||||
}
|
||||
var result map[string]any
|
||||
json.Unmarshal(respBody, &result)
|
||||
latency := int(time.Since(start).Milliseconds())
|
||||
return map[string]any{
|
||||
"response": strings.TrimSpace(pyStrOr(result["response"], "")),
|
||||
"model": model,
|
||||
"provider": p.Name,
|
||||
"latency_ms": latency,
|
||||
"tokens": pyIntOr(result["eval_count"], 0),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateOpenai — ProviderRegistry._generate_openai with native tool
|
||||
// calling (tools → FormatToolsOpenai + tool_choice auto).
|
||||
func (reg *ProviderRegistry) GenerateOpenai(p *ModelProvider, prompt, model string,
|
||||
maxTokens int, temperature float64, systemPrompt *string, tools []map[string]any) map[string]any {
|
||||
start := time.Now()
|
||||
headers := map[string]string{"Content-Type": "application/json"}
|
||||
if p.APIKey != "" {
|
||||
headers["Authorization"] = "Bearer " + p.APIKey
|
||||
}
|
||||
messages := []map[string]any{}
|
||||
if systemPrompt != nil {
|
||||
messages = append(messages, map[string]any{"role": "system", "content": *systemPrompt})
|
||||
}
|
||||
messages = append(messages, map[string]any{"role": "user", "content": prompt})
|
||||
data := map[string]any{
|
||||
"model": model, "messages": messages,
|
||||
"max_tokens": maxTokens, "temperature": temperature,
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
data["tools"] = FormatToolsOpenai(tools)
|
||||
data["tool_choice"] = "auto"
|
||||
}
|
||||
body, _ := json.Marshal(data)
|
||||
respBody, err := reg.httpDo(p.URL+"/v1/chat/completions", body, headers, 120*time.Second)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error(), "model": model, "provider": p.Name}
|
||||
}
|
||||
var result map[string]any
|
||||
json.Unmarshal(respBody, &result)
|
||||
latency := int(time.Since(start).Milliseconds())
|
||||
|
||||
// choice = result.get("choices", [{}])[0] — an empty choices list
|
||||
// raises in the oracle and degrades to the error dict here too.
|
||||
message := map[string]any{}
|
||||
if choices, ok := result["choices"].([]any); ok {
|
||||
if len(choices) == 0 {
|
||||
return map[string]any{"error": "list index out of range", "model": model, "provider": p.Name}
|
||||
}
|
||||
if choice, ok := choices[0].(map[string]any); ok {
|
||||
if msg, ok := choice["message"].(map[string]any); ok {
|
||||
message = msg
|
||||
}
|
||||
}
|
||||
}
|
||||
toolCalls, _ := message["tool_calls"].([]any)
|
||||
usage, _ := result["usage"].(map[string]any)
|
||||
if usage == nil {
|
||||
usage = map[string]any{}
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
toolResults := ExecuteToolCalls(pyMaps(toolCalls))
|
||||
return map[string]any{
|
||||
"response": pyStrOr(message["content"], ""),
|
||||
"tool_calls": pyAny(toolCalls),
|
||||
"tool_results": toolResults,
|
||||
"model": model,
|
||||
"provider": p.Name,
|
||||
"latency_ms": latency,
|
||||
"tokens": pyIntOr(usage["total_tokens"], 0),
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"response": strings.TrimSpace(pyStrOr(message["content"], "")),
|
||||
"model": model,
|
||||
"provider": p.Name,
|
||||
"latency_ms": latency,
|
||||
"tokens": pyIntOr(usage["total_tokens"], 0),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAnthropic — ProviderRegistry._generate_anthropic with native
|
||||
// tool calling (tools → FormatToolsAnthropic; no tool_choice).
|
||||
func (reg *ProviderRegistry) GenerateAnthropic(p *ModelProvider, prompt, model string,
|
||||
maxTokens int, temperature float64, systemPrompt *string, tools []map[string]any) map[string]any {
|
||||
start := time.Now()
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": p.APIKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
data := map[string]any{
|
||||
"model": model,
|
||||
"max_tokens": maxTokens,
|
||||
"messages": []map[string]any{{"role": "user", "content": prompt}},
|
||||
}
|
||||
if systemPrompt != nil {
|
||||
data["system"] = *systemPrompt
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
data["tools"] = FormatToolsAnthropic(tools)
|
||||
}
|
||||
body, _ := json.Marshal(data)
|
||||
respBody, err := reg.httpDo(p.URL+"/v1/messages", body, headers, 120*time.Second)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error(), "model": model, "provider": p.Name}
|
||||
}
|
||||
var result map[string]any
|
||||
json.Unmarshal(respBody, &result)
|
||||
latency := int(time.Since(start).Milliseconds())
|
||||
|
||||
content, _ := result["content"].([]any)
|
||||
toolCalls := []any{}
|
||||
responseText := ""
|
||||
for _, blockAny := range content {
|
||||
block, _ := blockAny.(map[string]any)
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
switch pyStrOr(block["type"], "") {
|
||||
case "text":
|
||||
responseText += pyStrOr(block["text"], "")
|
||||
case "tool_use":
|
||||
toolCalls = append(toolCalls, block)
|
||||
}
|
||||
}
|
||||
usage, _ := result["usage"].(map[string]any)
|
||||
if usage == nil {
|
||||
usage = map[string]any{}
|
||||
}
|
||||
tokens := pyIntOr(usage["input_tokens"], 0) + pyIntOr(usage["output_tokens"], 0)
|
||||
if len(toolCalls) > 0 {
|
||||
toolResults := ExecuteToolCallsAnthropic(pyMaps(toolCalls))
|
||||
return map[string]any{
|
||||
"response": responseText,
|
||||
"tool_calls": toolCalls,
|
||||
"tool_results": toolResults,
|
||||
"model": model,
|
||||
"provider": p.Name,
|
||||
"latency_ms": latency,
|
||||
"tokens": tokens,
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"response": strings.TrimSpace(responseText),
|
||||
"model": model,
|
||||
"provider": p.Name,
|
||||
"latency_ms": latency,
|
||||
"tokens": tokens,
|
||||
}
|
||||
}
|
||||
|
||||
// httpDo — the HTTP seam. reg.Do overrides; otherwise the real
|
||||
// net/http transport (POST when body != nil, GET otherwise).
|
||||
func (reg *ProviderRegistry) httpDo(url string, body []byte, headers map[string]string,
|
||||
timeout time.Duration) ([]byte, error) {
|
||||
if reg.Do != nil {
|
||||
return reg.Do(url, body, headers, timeout)
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
var req *http.Request
|
||||
var err error
|
||||
if body != nil {
|
||||
req, err = http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
} else {
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("HTTP Error %d: %s", resp.StatusCode, strings.TrimSpace(string(data)))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ── global registry (get_registry / connect_provider / list_all_models) ──
|
||||
|
||||
var (
|
||||
globalRegistryMu sync.Mutex
|
||||
globalRegistry *ProviderRegistry
|
||||
)
|
||||
|
||||
// GetRegistry — module get_registry (thread-safe singleton).
|
||||
func GetRegistry() *ProviderRegistry {
|
||||
globalRegistryMu.Lock()
|
||||
defer globalRegistryMu.Unlock()
|
||||
if globalRegistry == nil {
|
||||
globalRegistry = NewRegistry(defaultConfigPath())
|
||||
}
|
||||
return globalRegistry
|
||||
}
|
||||
|
||||
// ConnectProvider — module connect_provider.
|
||||
func ConnectProvider(name, url, providerType, apiKey string) map[string]any {
|
||||
if providerType == "" {
|
||||
providerType = "openai"
|
||||
}
|
||||
p := GetRegistry().Connect(name, url, providerType, apiKey)
|
||||
return p.ToDict()
|
||||
}
|
||||
|
||||
// ListAllModels — module list_all_models.
|
||||
func ListAllModels() []map[string]any {
|
||||
return GetRegistry().ListModels()
|
||||
}
|
||||
|
||||
// Generate — module-level generate (delegates to the global registry).
|
||||
func Generate(prompt string, providerName string, model string, maxTokens int,
|
||||
temperature float64, systemPrompt *string, tools []map[string]any) map[string]any {
|
||||
return GetRegistry().Generate(prompt, providerName, model, maxTokens, temperature, systemPrompt, tools)
|
||||
}
|
||||
|
||||
// ── _ctx_budget (#28: model-aware num_ctx, never hardcoded 8192) ─────────
|
||||
|
||||
type budgetProfile struct {
|
||||
window int
|
||||
local bool
|
||||
}
|
||||
|
||||
// Mirrors model_registry.DEFAULT_PROFILES (id → context window, local).
|
||||
var budgetProfiles = map[string]budgetProfile{
|
||||
"deterministic": {0, true},
|
||||
"qwen2.5:3b": {32768, true},
|
||||
"qwen3:14b": {32768, true},
|
||||
"qwen3.6:27b": {32768, true},
|
||||
"mimo-v2.5-pro": {128000, false},
|
||||
}
|
||||
|
||||
const (
|
||||
outputReserve = 2048 // OUTPUT_RESERVE["default"]
|
||||
toolResultReSe = 1024 // TOOL_RESULT_RESERVE
|
||||
)
|
||||
|
||||
// CtxBudget — _ctx_budget: window − output_reserve − tool_reserve
|
||||
// (tool reserve only for non-local), floored at 1024. Unknown models
|
||||
// get the conservative 8192 window.
|
||||
func CtxBudget(modelID string) int {
|
||||
window := 8192
|
||||
local := true
|
||||
if p, ok := budgetProfiles[modelID]; ok {
|
||||
window = p.window
|
||||
if window == 0 {
|
||||
window = 8192
|
||||
}
|
||||
local = p.local
|
||||
}
|
||||
toolReserve := 0
|
||||
if !local {
|
||||
toolReserve = toolResultReSe
|
||||
}
|
||||
numCtx := window - outputReserve - toolReserve
|
||||
if numCtx < 1024 {
|
||||
numCtx = 1024
|
||||
}
|
||||
return numCtx
|
||||
}
|
||||
|
||||
// ── small parity helpers ─────────────────────────────────────────────────
|
||||
|
||||
func pyStr(v any) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pyStrOr(v any, def string) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func pyBoolOr(v any, def bool) bool {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func pyFloatOr(v any, def float64) float64 {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n
|
||||
case int:
|
||||
return float64(n)
|
||||
default:
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
func pyIntOr(v any, def int) int {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n)
|
||||
case int:
|
||||
return n
|
||||
default:
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
func pyAny(v any) any { return v }
|
||||
|
||||
func pyMaps(items []any) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(items))
|
||||
for _, it := range items {
|
||||
if m, ok := it.(map[string]any); ok {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// missingKeyErr — str(KeyError(k)) is quoted in Python.
|
||||
func missingKeyErr(k string) string { return "'" + k + "'" }
|
||||
|
||||
// roundHalfEven — Python round(): banker's rounding at the digit.
|
||||
func roundHalfEven(x float64, digits int) float64 {
|
||||
shift := math.Pow(10, float64(digits))
|
||||
return math.RoundToEven(x*shift) / shift
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
// toolcalls.go — _execute_tool_calls / _execute_tool_calls_anthropic
|
||||
// parity (factory-gap-pr). The tool executor is the IO seam: tests
|
||||
// inject ToolExecutorFunc; production wires the local tool runtime.
|
||||
// Shaping law (1:1 with the oracle): [OI] calls carry
|
||||
// function.name + function.arguments (a JSON string that degrades to {}
|
||||
// on decode failure); Anthropic calls carry name + input. Every result
|
||||
// is json.dumps'd — a non-serializable result degrades to the fixed
|
||||
// error marker (the oracle's PreparedTool crash fix), never aborts the
|
||||
// loop.
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// ToolExecutorFunc is the injectable executor seam: name + args →
|
||||
// result (any JSON-shape value). nil = no executor wired.
|
||||
var ToolExecutorFunc func(name string, args map[string]any) (any, error)
|
||||
|
||||
func runTool(name string, args map[string]any) any {
|
||||
if args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
if ToolExecutorFunc == nil {
|
||||
return map[string]any{"success": false, "error": "no tool executor wired"}
|
||||
}
|
||||
res, err := ToolExecutorFunc(name, args)
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "error": err.Error()}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// pyDumpResult — json.dumps(result) with the oracle's degrade law:
|
||||
// a result with no JSON-shape equivalent becomes the fixed error marker
|
||||
// (insertion-ordered to match the Python fallback's byte shape).
|
||||
func pyDumpResult(result any) string {
|
||||
if s, ok := pyDumpTry(result); ok {
|
||||
return s
|
||||
}
|
||||
d := NewPyDict()
|
||||
d.Set("success", false)
|
||||
d.Set("error", pyNonSerializable)
|
||||
return PyDump(d)
|
||||
}
|
||||
|
||||
// ExecuteToolCalls — _execute_tool_calls.
|
||||
func ExecuteToolCalls(toolCalls []map[string]any) []map[string]any {
|
||||
results := []map[string]any{}
|
||||
for _, call := range toolCalls {
|
||||
fn, _ := call["function"].(map[string]any)
|
||||
if fn == nil {
|
||||
fn = map[string]any{}
|
||||
}
|
||||
name := pyStrOr(fn["name"], "")
|
||||
var args map[string]any
|
||||
switch a := fn["arguments"].(type) {
|
||||
case string:
|
||||
var parsed any
|
||||
if err := json.Unmarshal([]byte(a), &parsed); err == nil {
|
||||
if m, ok := parsed.(map[string]any); ok {
|
||||
args = m
|
||||
} else {
|
||||
args = map[string]any{}
|
||||
}
|
||||
} else {
|
||||
args = map[string]any{}
|
||||
}
|
||||
case map[string]any:
|
||||
args = a
|
||||
default:
|
||||
args = map[string]any{}
|
||||
}
|
||||
results = append(results, map[string]any{
|
||||
"tool_call_id": pyStrOr(call["id"], ""),
|
||||
"role": "tool",
|
||||
"content": pyDumpResult(runTool(name, args)),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ExecuteToolCallsAnthropic — _execute_tool_calls_anthropic.
|
||||
func ExecuteToolCallsAnthropic(toolCalls []map[string]any) []map[string]any {
|
||||
results := []map[string]any{}
|
||||
for _, call := range toolCalls {
|
||||
name := pyStrOr(call["name"], "")
|
||||
args, _ := call["input"].(map[string]any)
|
||||
results = append(results, map[string]any{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": pyStrOr(call["id"], ""),
|
||||
"content": pyDumpResult(runTool(name, args)),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
[
|
||||
{
|
||||
"fn": "_format_tools_openai",
|
||||
"args": [
|
||||
[]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_openai",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_openai",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"name": "write_file"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_openai",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_anthropic",
|
||||
"args": [
|
||||
[]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_anthropic",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_anthropic",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"name": "write_file"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_format_tools_anthropic",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file"
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"unknown-model-xyz"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 6144
|
||||
},
|
||||
{
|
||||
"fn": "_ctx_budget",
|
||||
"args": [
|
||||
"deterministic"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 6144
|
||||
},
|
||||
{
|
||||
"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": "_execute_tool_calls",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"id": "call_1",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": "{\"path\": \"/nonexistent/zzz.txt\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"tool_call_id": "call_1",
|
||||
"role": "tool",
|
||||
"content": "{\"success\": false, \"error\": \"Access denied: /nonexistent/zzz.txt is outside allowed directories\"}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_execute_tool_calls",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"id": "call_2",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": "not-json"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"tool_call_id": "call_2",
|
||||
"role": "tool",
|
||||
"content": "{\"success\": false, \"error\": \"Not a file: \"}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_execute_tool_calls",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"id": "call_3",
|
||||
"function": {
|
||||
"name": "no_such_tool_xyz",
|
||||
"arguments": "{}"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"tool_call_id": "call_3",
|
||||
"role": "tool",
|
||||
"content": "{\"success\": false, \"error\": \"tool result not JSON serializable\"}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_execute_tool_calls_anthropic",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "tu_1",
|
||||
"name": "read_file",
|
||||
"input": {
|
||||
"path": "/nonexistent/zzz.txt"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "tu_1",
|
||||
"content": "{\"success\": false, \"error\": \"Access denied: /nonexistent/zzz.txt is outside allowed directories\"}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_execute_tool_calls_anthropic",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "tu_2",
|
||||
"name": "no_such_tool_xyz",
|
||||
"input": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "tu_2",
|
||||
"content": "{\"success\": false, \"error\": \"tool result not JSON serializable\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -31,7 +31,7 @@ func TestFormatParity(t *testing.T) {
|
||||
for _, c := range doc.Cases {
|
||||
var got []map[string]interface{}
|
||||
if c.Fn == "openai" {
|
||||
got = FormatToolsOpenAI(tools)
|
||||
got = FormatToolsOpenai(tools)
|
||||
} else {
|
||||
got = FormatToolsAnthropic(tools)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,365 @@
|
||||
package main
|
||||
|
||||
// truth_registry_test.go — factory-gap-pr parity gate for the
|
||||
// ProviderRegistry method surface: replays truth_registry.json (frozen
|
||||
// from the live Python oracle with urllib fully scripted by
|
||||
// scripts/gen_provider_registry_truth.py) against the Go port. HTTP
|
||||
// goes through the injected Do seam; config files live in per-test
|
||||
// temp dirs. latency_ms is excluded on both sides (the oracle's clock
|
||||
// is not injectable — no-clock law).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func dget(d *PyDict, k string) any {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
v, _ := d.Get(k)
|
||||
return v
|
||||
}
|
||||
|
||||
type capturedReq struct {
|
||||
url string
|
||||
body []byte
|
||||
}
|
||||
|
||||
// buildRegistryFromInput constructs a registry from a truth row:
|
||||
// providers inserted in frozen key order (ParsePyJSON preserves the
|
||||
// Python dict order), HTTP routed through the scripted Do seam. The
|
||||
// capture is mutex-guarded because ListModels fans fetches out
|
||||
// concurrently.
|
||||
func buildRegistryFromInput(t *testing.T, input *PyDict) (*ProviderRegistry, *[]capturedReq) {
|
||||
t.Helper()
|
||||
reg := NewRegistry(filepath.Join(t.TempDir(), "providers.json"))
|
||||
captured := &[]capturedReq{}
|
||||
var capMu sync.Mutex
|
||||
if routesAny := dget(input, "routes"); routesAny != nil {
|
||||
routes, _ := routesAny.([]any)
|
||||
reg.Do = func(url string, body []byte, headers map[string]string, timeout time.Duration) ([]byte, error) {
|
||||
for _, r := range routes {
|
||||
route, _ := r.(*PyDict)
|
||||
suffix, _ := dget(route, "suffix").(string)
|
||||
if suffix != "" && len(url) >= len(suffix) && url[len(url)-len(suffix):] == suffix {
|
||||
if exc, ok := dget(route, "exc").(string); ok {
|
||||
return nil, &urlError{msg: exc}
|
||||
}
|
||||
capMu.Lock()
|
||||
*captured = append(*captured, capturedReq{url: url, body: body})
|
||||
capMu.Unlock()
|
||||
return json.Marshal(plainJSON(dget(route, "body")))
|
||||
}
|
||||
}
|
||||
return nil, &urlError{msg: "unexpected url: " + url}
|
||||
}
|
||||
}
|
||||
if provsAny := dget(input, "providers"); provsAny != nil {
|
||||
provs, _ := provsAny.(*PyDict)
|
||||
if provs != nil {
|
||||
for _, name := range provs.Keys {
|
||||
fields, _ := provs.Vals[name].(*PyDict)
|
||||
reg.Providers[name] = ProviderFromFields(plainJSON(fields).(map[string]any))
|
||||
reg.order = append(reg.order, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return reg, captured
|
||||
}
|
||||
|
||||
// urlError mirrors urllib.error.URLError's str(): "<urlopen error msg>".
|
||||
type urlError struct{ msg string }
|
||||
|
||||
func (e *urlError) Error() string { return "<urlopen error " + e.msg + ">" }
|
||||
|
||||
func providerFieldsMap(p *ModelProvider) map[string]any {
|
||||
var apiKey any
|
||||
if p.APIKey != "" {
|
||||
apiKey = p.APIKey
|
||||
}
|
||||
var lhc any
|
||||
if p.LastHealthCheck != "" {
|
||||
lhc = p.LastHealthCheck
|
||||
}
|
||||
return map[string]any{
|
||||
"name": p.Name, "url": p.URL, "provider_type": p.ProviderType,
|
||||
"api_key": apiKey, "enabled": p.Enabled,
|
||||
"last_health_check": lhc, "health_status": p.HealthStatus,
|
||||
"models": p.Models,
|
||||
}
|
||||
}
|
||||
|
||||
func providersState(reg *ProviderRegistry) map[string]any {
|
||||
out := map[string]any{}
|
||||
for name, p := range reg.Providers {
|
||||
out[name] = providerFieldsMap(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func readConfigIfPresent(t *testing.T, path string) (any, bool) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var cfg any
|
||||
json.Unmarshal(data, &cfg)
|
||||
return cfg, true
|
||||
}
|
||||
|
||||
// unwrapExpected handles both frozen shapes: top-level expected_error
|
||||
// (row()) and the nested {"expected_request", "expected"/"expected_error"}
|
||||
// envelope (gen_case()).
|
||||
func unwrapExpected(c *PyDict) (result, request any, errText string) {
|
||||
if s, ok := dget(c, "expected_error").(string); ok && s != "" {
|
||||
return nil, nil, s
|
||||
}
|
||||
exp := dget(c, "expected")
|
||||
if em, ok := exp.(*PyDict); ok {
|
||||
if _, isEnvelope := em.Get("expected_request"); isEnvelope {
|
||||
if s, ok := dget(em, "expected_error").(string); ok {
|
||||
return nil, dget(em, "expected_request"), s
|
||||
}
|
||||
return dget(em, "expected"), dget(em, "expected_request"), ""
|
||||
}
|
||||
}
|
||||
return exp, nil, ""
|
||||
}
|
||||
|
||||
func genArgs(input *PyDict) (prompt, model string, maxTokens int, temperature float64, systemPrompt *string, tools []map[string]any) {
|
||||
prompt, _ = dget(input, "prompt").(string)
|
||||
model, _ = dget(input, "model").(string)
|
||||
maxTokens = pyIntOr(dget(input, "max_tokens"), 500)
|
||||
temperature = pyFloatOr(dget(input, "temperature"), 0.7)
|
||||
if sp, ok := dget(input, "system_prompt").(string); ok {
|
||||
systemPrompt = &sp
|
||||
}
|
||||
if toolsAny, ok := dget(input, "tools").([]any); ok {
|
||||
tools = pyMaps(plainJSON(toolsAny).([]any))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func stripLatency(m map[string]any) map[string]any {
|
||||
out := map[string]any{}
|
||||
for k, v := range m {
|
||||
if k != "latency_ms" {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestRegistryTruthParity(t *testing.T) {
|
||||
raw, err := ParsePyJSON(mustRead(t, "truth_registry.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("truth_registry.json: %v", err)
|
||||
}
|
||||
cases, _ := raw.([]any)
|
||||
for _, caseAny := range cases {
|
||||
c, _ := caseAny.(*PyDict)
|
||||
fn, _ := dget(c, "fn").(string)
|
||||
name, _ := dget(c, "name").(string)
|
||||
input, _ := dget(c, "input").(*PyDict)
|
||||
t.Run(fn+"/"+name, func(t *testing.T) {
|
||||
runRegistryCase(t, fn, name, input, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustRead(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("%s missing: %v", name, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// scriptExecutorFromExpected installs a ToolExecutorFunc whose scripted
|
||||
// results are the Python tool_executor outputs parsed back out of the
|
||||
// frozen tool_results content strings. Returns a restore func.
|
||||
func scriptExecutorFromExpected(t *testing.T, result any) func() {
|
||||
t.Helper()
|
||||
var scripted []any
|
||||
if resMap, ok := plainJSON(result).(map[string]any); ok {
|
||||
if trs, ok := resMap["tool_results"].([]any); ok {
|
||||
for _, tr := range trs {
|
||||
trm, _ := tr.(map[string]any)
|
||||
content, _ := trm["content"].(string)
|
||||
parsed, err := ParsePyJSON([]byte(content))
|
||||
if err != nil {
|
||||
t.Fatalf("tool result content parse: %v", err)
|
||||
}
|
||||
scripted = append(scripted, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
oldExec := ToolExecutorFunc
|
||||
ToolExecutorFunc = func(name string, args map[string]any) (any, error) {
|
||||
if len(scripted) == 0 {
|
||||
t.Errorf("unexpected extra tool call %q", name)
|
||||
return nil, nil
|
||||
}
|
||||
r := scripted[0]
|
||||
scripted = scripted[1:]
|
||||
return r, nil
|
||||
}
|
||||
return func() { ToolExecutorFunc = oldExec }
|
||||
}
|
||||
|
||||
func runRegistryCase(t *testing.T, fn, name string, input *PyDict, c *PyDict) {
|
||||
t.Helper()
|
||||
result, request, errText := unwrapExpected(c)
|
||||
|
||||
switch fn {
|
||||
case "to_dict":
|
||||
fields, _ := plainJSON(dget(input, "provider")).(map[string]any)
|
||||
p := ProviderFromFields(fields)
|
||||
compareJSON(t, name, p.ToDict(), result)
|
||||
|
||||
case "_load_config":
|
||||
path := filepath.Join(t.TempDir(), "providers.json")
|
||||
if content := dget(input, "content"); content != nil {
|
||||
b, _ := json.Marshal(plainJSON(content))
|
||||
os.WriteFile(path, b, 0o600)
|
||||
}
|
||||
reg := NewRegistry(path)
|
||||
compareJSON(t, name, map[string]any{"providers": providersState(reg)}, result)
|
||||
|
||||
case "_save_config":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
reg.SaveConfig()
|
||||
cfg, ok := readConfigIfPresent(t, reg.ConfigPath)
|
||||
if !ok {
|
||||
t.Fatalf("%s: config file not written", name)
|
||||
}
|
||||
compareJSON(t, name, map[string]any{"written": cfg}, result)
|
||||
|
||||
case "connect":
|
||||
reg := NewRegistry(filepath.Join(t.TempDir(), "providers.json"))
|
||||
p := reg.Connect(dget(input, "name").(string), dget(input, "url").(string),
|
||||
dget(input, "provider_type").(string), dget(input, "api_key").(string))
|
||||
got := map[string]any{"provider": p.ToDict()}
|
||||
if cfg, ok := readConfigIfPresent(t, reg.ConfigPath); ok {
|
||||
got["config"] = cfg
|
||||
}
|
||||
compareJSON(t, name, got, result)
|
||||
|
||||
case "list_providers":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
compareJSON(t, name, map[string]any{"providers": reg.ListProviders()}, result)
|
||||
|
||||
case "get_provider":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
p := reg.GetProvider(dget(input, "name").(string))
|
||||
var pv any
|
||||
if p != nil {
|
||||
pv = p.ToDict()
|
||||
}
|
||||
compareJSON(t, name, map[string]any{"provider": pv}, result)
|
||||
|
||||
case "disconnect":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
if seed := dget(input, "seed_config"); seed != nil {
|
||||
b, _ := json.Marshal(plainJSON(seed))
|
||||
os.WriteFile(reg.ConfigPath, b, 0o600)
|
||||
}
|
||||
reg.Disconnect(dget(input, "name").(string))
|
||||
got := map[string]any{"providers": providersState(reg)}
|
||||
if _, wantConfig := dget(c, "expected").(*PyDict).Get("config"); wantConfig {
|
||||
if cfg, ok := readConfigIfPresent(t, reg.ConfigPath); ok {
|
||||
got["config"] = cfg
|
||||
}
|
||||
}
|
||||
compareJSON(t, name, got, result)
|
||||
|
||||
case "_clean_model_name":
|
||||
cn, _ := dget(input, "name").(string)
|
||||
compareJSON(t, name, CleanModelName(cn), result)
|
||||
|
||||
case "_fetch_ollama_models", "_fetch_openai_models", "_fetch_anthropic_models", "_fetch_models":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
fields, _ := plainJSON(dget(input, "provider")).(map[string]any)
|
||||
p := ProviderFromFields(fields)
|
||||
got, err := reg.FetchModels(p)
|
||||
switch fn {
|
||||
case "_fetch_ollama_models":
|
||||
got, err = reg.FetchOllamaModels(p)
|
||||
case "_fetch_openai_models":
|
||||
got, err = reg.FetchOpenaiModels(p)
|
||||
case "_fetch_anthropic_models":
|
||||
got, err = reg.FetchAnthropicModels(p)
|
||||
}
|
||||
if errText != "" {
|
||||
if err == nil {
|
||||
t.Errorf("%s: want error %q, got none", name, errText)
|
||||
} else if err.Error() != errText {
|
||||
t.Errorf("%s: error:\n got %q\nwant %q", name, err.Error(), errText)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s: unexpected error: %v", name, err)
|
||||
}
|
||||
compareJSON(t, name, got, result)
|
||||
|
||||
case "list_models":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
compareJSON(t, name, reg.ListModels(), result)
|
||||
|
||||
case "generate":
|
||||
reg, _ := buildRegistryFromInput(t, input)
|
||||
prompt, _ := dget(input, "prompt").(string)
|
||||
providerName, _ := dget(input, "provider_name").(string)
|
||||
model, _ := dget(input, "model").(string)
|
||||
got := reg.Generate(prompt, providerName, model, 500, 0.7, nil, nil)
|
||||
compareJSON(t, name, got, result)
|
||||
|
||||
case "_generate_ollama", "_generate_openai", "_generate_anthropic":
|
||||
reg, captured := buildRegistryFromInput(t, input)
|
||||
fields, _ := plainJSON(dget(input, "provider")).(map[string]any)
|
||||
p := ProviderFromFields(fields)
|
||||
prompt, model, maxTokens, temperature, systemPrompt, tools := genArgs(input)
|
||||
if result != nil {
|
||||
defer scriptExecutorFromExpected(t, result)()
|
||||
}
|
||||
var got map[string]any
|
||||
switch fn {
|
||||
case "_generate_ollama":
|
||||
got = reg.GenerateOllama(p, prompt, model, maxTokens, temperature)
|
||||
case "_generate_openai":
|
||||
got = reg.GenerateOpenai(p, prompt, model, maxTokens, temperature, systemPrompt, tools)
|
||||
case "_generate_anthropic":
|
||||
got = reg.GenerateAnthropic(p, prompt, model, maxTokens, temperature, systemPrompt, tools)
|
||||
}
|
||||
if request == nil {
|
||||
if len(*captured) != 0 {
|
||||
t.Errorf("%s: expected no HTTP call, captured %d", name, len(*captured))
|
||||
}
|
||||
} else {
|
||||
if len(*captured) == 0 {
|
||||
t.Fatalf("%s: expected an HTTP call, captured none", name)
|
||||
}
|
||||
var gotReq any
|
||||
json.Unmarshal((*captured)[0].body, &gotReq)
|
||||
compareJSON(t, name+"/request", gotReq, request)
|
||||
}
|
||||
if errText != "" {
|
||||
if gotErr, ok := got["error"].(string); !ok || gotErr != errText {
|
||||
t.Errorf("%s: error:\n got %v\nwant %q", name, got["error"], errText)
|
||||
}
|
||||
return
|
||||
}
|
||||
compareJSON(t, name, stripLatency(got), result)
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled frozen fn %s", fn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
// truth_test.go — factory-gap-pr parity gate: replays the frozen
|
||||
// oracle truth (truth.json, generated by scripts/
|
||||
// gen_provider_registry_truth.py via the shared freeze_truth harness)
|
||||
// VERBATIM against the Go port. The tool executor is the injected seam
|
||||
// — its scripted results are the Python tool_executor outputs parsed
|
||||
// back out of the frozen content strings, so the battery never touches
|
||||
// the real executor and never hits a socket.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruthFile(t *testing.T, name string, dst any) {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("%s missing: %v", name, err)
|
||||
}
|
||||
if err := json.Unmarshal(b, dst); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func compareJSON(t *testing.T, name string, got, want any) {
|
||||
t.Helper()
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(plainJSON(want))
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s:\n got %q\nwant %q", name, gb, wb)
|
||||
}
|
||||
}
|
||||
|
||||
// plainJSON converts PyDict (order-preserving decode) back to plain
|
||||
// maps for order-insensitive comparison.
|
||||
func plainJSON(v any) any {
|
||||
switch x := v.(type) {
|
||||
case *PyDict:
|
||||
m := map[string]any{}
|
||||
for _, k := range x.Keys {
|
||||
m[k] = plainJSON(x.Vals[k])
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(x))
|
||||
for i, e := range x {
|
||||
out[i] = plainJSON(e)
|
||||
}
|
||||
return out
|
||||
case []map[string]any:
|
||||
out := make([]any, len(x))
|
||||
for i, e := range x {
|
||||
out[i] = plainJSON(e)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarnessTruthParity(t *testing.T) {
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Args []any `json:"args"`
|
||||
Kwargs map[string]any `json:"kwargs"`
|
||||
Expected any `json:"expected"`
|
||||
}
|
||||
loadTruthFile(t, "truth.json", &cases)
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "_format_tools_openai":
|
||||
compareJSON(t, c.Fn, FormatToolsOpenai(argsTools(c.Args)), c.Expected)
|
||||
case "_format_tools_anthropic":
|
||||
compareJSON(t, c.Fn, FormatToolsAnthropic(argsTools(c.Args)), c.Expected)
|
||||
case "_ctx_budget":
|
||||
modelID, _ := c.Args[0].(string)
|
||||
compareJSON(t, c.Fn, CtxBudget(modelID), c.Expected)
|
||||
case "_execute_tool_calls", "_execute_tool_calls_anthropic":
|
||||
expList, _ := c.Expected.([]any)
|
||||
scripted := make([]any, len(expList))
|
||||
for i, e := range expList {
|
||||
em, _ := e.(map[string]any)
|
||||
content, _ := em["content"].(string)
|
||||
parsed, err := ParsePyJSON([]byte(content))
|
||||
if err != nil {
|
||||
t.Fatalf("%s case %d: content parse: %v", c.Fn, i, err)
|
||||
}
|
||||
scripted[i] = parsed
|
||||
}
|
||||
idx := 0
|
||||
oldExec := ToolExecutorFunc
|
||||
ToolExecutorFunc = func(name string, args map[string]any) (any, error) {
|
||||
if idx >= len(scripted) {
|
||||
t.Errorf("%s: unexpected extra tool call %q", c.Fn, name)
|
||||
return nil, nil
|
||||
}
|
||||
r := scripted[idx]
|
||||
idx++
|
||||
return r, nil
|
||||
}
|
||||
var got []map[string]any
|
||||
if c.Fn == "_execute_tool_calls_anthropic" {
|
||||
got = ExecuteToolCallsAnthropic(argsTools(c.Args))
|
||||
} else {
|
||||
got = ExecuteToolCalls(argsTools(c.Args))
|
||||
}
|
||||
ToolExecutorFunc = oldExec
|
||||
if idx != len(scripted) {
|
||||
t.Errorf("%s: executor invoked %d times, want %d", c.Fn, idx, len(scripted))
|
||||
}
|
||||
compareJSON(t, c.Fn, got, c.Expected)
|
||||
default:
|
||||
t.Errorf("unhandled frozen fn %s", c.Fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// argsTools unwraps the single tool-list argument of the frozen
|
||||
// _format_tools_* / _execute_tool_calls* cases.
|
||||
func argsTools(args []any) []map[string]any {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
tools, _ := args[0].([]any)
|
||||
return pyMaps(tools)
|
||||
}
|
||||
|
||||
// TestExecuteToolCallsNonSerializableFallback — the Go-side branch of
|
||||
// the oracle's PreparedTool degrade law: a tool result with no
|
||||
// JSON-shape equivalent becomes the fixed error marker.
|
||||
func TestExecuteToolCallsNonSerializableFallback(t *testing.T) {
|
||||
oldExec := ToolExecutorFunc
|
||||
defer func() { ToolExecutorFunc = oldExec }()
|
||||
ToolExecutorFunc = func(name string, args map[string]any) (any, error) {
|
||||
return make(chan int), nil // never JSON-serializable
|
||||
}
|
||||
got := ExecuteToolCalls([]map[string]any{{
|
||||
"id": "c1",
|
||||
"function": map[string]any{"name": "x", "arguments": "{}"},
|
||||
}})
|
||||
want := `{"success": false, "error": "tool result not JSON serializable"}`
|
||||
if got[0]["content"] != want {
|
||||
t.Errorf("content:\n got %v\nwant %s", got[0]["content"], want)
|
||||
}
|
||||
if got[0]["tool_call_id"] != "c1" || got[0]["role"] != "tool" {
|
||||
t.Errorf("wrapper shape wrong: %v", got[0])
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.293.1"
|
||||
__version__ = "0.299.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.293.1"' in rune_version,
|
||||
'__version__ = "0.299.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -92,11 +92,15 @@ def _execute_tool_calls(tool_calls: list) -> list:
|
||||
args = {}
|
||||
|
||||
result = tool_executor.execute_tool(name, args)
|
||||
try:
|
||||
content = json.dumps(result)
|
||||
except (TypeError, ValueError): # noqa: BLE001 — non-JSON tool results (e.g. PreparedTool) degrade to an error marker, never crash the loop
|
||||
content = json.dumps({"success": False, "error": "tool result not JSON serializable"})
|
||||
results.append(
|
||||
{
|
||||
"tool_call_id": call.get("id", ""),
|
||||
"role": "tool",
|
||||
"content": json.dumps(result),
|
||||
"content": content,
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -115,11 +119,15 @@ def _execute_tool_calls_anthropic(tool_calls: list) -> list:
|
||||
args = call.get("input", {})
|
||||
|
||||
result = tool_executor.execute_tool(name, args)
|
||||
try:
|
||||
content = json.dumps(result)
|
||||
except (TypeError, ValueError): # noqa: BLE001 — non-JSON tool results (e.g. PreparedTool) degrade to an error marker, never crash the loop
|
||||
content = json.dumps({"success": False, "error": "tool result not JSON serializable"})
|
||||
results.append(
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": call.get("id", ""),
|
||||
"content": json.dumps(result),
|
||||
"content": content,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -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.293.1
|
||||
Version: v0.299.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.293.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.299.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -190,6 +190,13 @@ def evaluate_salience(envelope: dict) -> SalienceDecision:
|
||||
)
|
||||
|
||||
|
||||
def evaluate_salience_dict(envelope: dict) -> dict:
|
||||
"""JSON-view of evaluate_salience for the shared freeze harness
|
||||
(dataclass results are not JSON-serializable there). A pure to_dict
|
||||
composition — zero logic drift from the floor."""
|
||||
return evaluate_salience(envelope).to_dict()
|
||||
|
||||
|
||||
def test_salience():
|
||||
"""Test the salience gate."""
|
||||
print("Testing Salience Gate...")
|
||||
|
||||
@@ -486,6 +486,21 @@ class Scaffold:
|
||||
raise IllegalPhaseTransition(
|
||||
f"illegal transition: {current} → {phase} (legal: {sorted(LEGAL_TRANSITIONS.get(current, set()))})"
|
||||
)
|
||||
# QUALITY-GATE LAW (2026-10-13): entering `closed` via raw transition
|
||||
# bypassed the predicted-vs-observed computation entirely — two wave
|
||||
# landings (hook_engine, adaptive_router 2026-10-13) closed with a
|
||||
# 'closed' phase and NO contract_quality, silently. The designed
|
||||
# entry is close() (computes quality + material-change). Raw
|
||||
# transition to closed now requires the quality to ALREADY be
|
||||
# recorded (e.g. close_contract ran in this process); otherwise it
|
||||
# refuses — the quality gate is mechanical, not advisory.
|
||||
if phase == "closed":
|
||||
if not e.get("ext", {}).get("contract_quality"):
|
||||
raise IllegalPhaseTransition(
|
||||
f"transition to 'closed' requires contract_quality evidence — "
|
||||
f"close the contract via close() (predicted-vs-observed), "
|
||||
f"not a raw transition ({contract_id})"
|
||||
)
|
||||
e["phase"] = phase
|
||||
if phase == "closed":
|
||||
e["status"] = "closed"
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
//! salience_gate branch battery — parity vs the frozen Python-oracle truth.
|
||||
//!
|
||||
//! Two truth views, one source, both embedded VERBATIM via include_str! —
|
||||
//! never hand-transcribed:
|
||||
//! * frozen_sg_truth.verify.json — the shared-harness case array
|
||||
//! ([{fn, args, kwargs, expected}]) frozen with RUNE_SALIENCE_CORE=python
|
||||
//! AND SG_RUST_LIB poisoned (pure-Python oracle floor, poisoned-truth
|
||||
//! law). Byte-identical under
|
||||
//! `freeze_truth.py --verify ... --module rune.salience_gate`.
|
||||
//! * frozen_sg_truth.json — envelope carrying the same row schema for ALL
|
||||
//! rows, including multi-family evaluate rows whose Python evaluation
|
||||
//! order differs from the delegation path's sorted order (the kernel
|
||||
//! mirrors the evaluation order — exact compare).
|
||||
//!
|
||||
//! Frozen via scripts/gen_salience_gate_truth.py. The oracle's parity
|
||||
//! contract for flags/rationale order is sorted-comparison on the DELEGATION
|
||||
//! path (oracle comment: "sorted comparison is the parity contract — the
|
||||
//! canary pins it"); the kernel-side battery pins the stricter exact
|
||||
//! evaluation order.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{
|
||||
_sg_core, evaluate_decision, evaluate_salience, evaluate_salience_dict, flag_score,
|
||||
handle_hook, test_salience, word_boundary_contains, SalienceDecision,
|
||||
};
|
||||
|
||||
const ENVELOPE: &str = include_str!("frozen_sg_truth.json");
|
||||
const VERIFY_TRUTH: &str = include_str!("frozen_sg_truth.verify.json");
|
||||
|
||||
fn envelope_cases() -> Vec<Value> {
|
||||
serde_json::from_str::<Value>(ENVELOPE)
|
||||
.expect("frozen truth envelope must parse")
|
||||
.get("cases")
|
||||
.and_then(|c| c.as_array())
|
||||
.expect("envelope cases must be an array")
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn verify_cases() -> Vec<Value> {
|
||||
serde_json::from_str::<Value>(VERIFY_TRUTH)
|
||||
.expect("frozen verify truth must parse")
|
||||
.as_array()
|
||||
.expect("verify truth must be the harness case array")
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn rebuild_decision(recv: &Value) -> SalienceDecision {
|
||||
let strs = |k: &str| -> Vec<String> {
|
||||
recv[k]
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().expect("flag entries are strings").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
SalienceDecision {
|
||||
score: recv["score"].as_f64().expect("score is a number"),
|
||||
flags: strs("flags"),
|
||||
minimum_tier: recv["minimum_tier"]
|
||||
.as_str()
|
||||
.expect("minimum_tier is a string")
|
||||
.to_string(),
|
||||
witness_needed: recv["witness_needed"].as_bool().expect("witness is bool"),
|
||||
rationale_codes: strs("rationale_codes"),
|
||||
}
|
||||
}
|
||||
|
||||
fn replay_row(case: &Value, label: &str) {
|
||||
let fn_name = case["fn"].as_str().expect("row fn is a string");
|
||||
let actual = match fn_name {
|
||||
"evaluate_salience_dict" => evaluate_salience_dict(&case["args"][0]),
|
||||
"evaluate_salience" => json!(evaluate_salience(&case["args"][0])),
|
||||
"SalienceDecision.to_dict" => {
|
||||
let decision = rebuild_decision(&case["recv"]);
|
||||
// the harness rows carry {"result": to_dict(), <attr>…} — the
|
||||
// parity contract is the result projection
|
||||
Value::Object(serde_json::Map::from_iter(decision.to_dict()))
|
||||
}
|
||||
"handle_hook" => {
|
||||
let event = &case["args"][0];
|
||||
let data = if case["kwargs"].get("data").is_some() {
|
||||
Some(case["kwargs"]["data"].clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
handle_hook(event, data.as_ref())
|
||||
}
|
||||
"_sg_core" => {
|
||||
// truth rows are frozen under the python pin — None branch
|
||||
_sg_core(Some("python")).unwrap_or(Value::Null)
|
||||
}
|
||||
other => panic!("unhandled frozen fn: {} (~{} in battery)", other, label),
|
||||
};
|
||||
let expected = match case["expected"].get("result") {
|
||||
Some(result) => result.clone(),
|
||||
None => case["expected"].clone(),
|
||||
};
|
||||
assert!(
|
||||
actual == expected,
|
||||
"case {} diverged:\n actual: {}\n expected: {}",
|
||||
label,
|
||||
actual,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
fn canon_row(case: &Value) -> String {
|
||||
serde_json::to_string(case).expect("row serializes")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_cases_superset_of_verify_file() {
|
||||
let envelope: std::collections::HashSet<String> =
|
||||
envelope_cases().iter().map(canon_row).collect();
|
||||
for case in verify_cases() {
|
||||
assert!(
|
||||
envelope.contains(&canon_row(&case)),
|
||||
"verify row missing from envelope (drift between the two views): {}",
|
||||
canon_row(&case)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_parity_battery() {
|
||||
for (i, case) in envelope_cases().iter().enumerate() {
|
||||
replay_row(case, &format!("envelope[{}]", i));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_file_parity_battery() {
|
||||
for (i, case) in verify_cases().iter().enumerate() {
|
||||
replay_row(case, &format!("verify[{}]", i));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_salience_oracle_smoke() {
|
||||
test_salience();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sg_core_pin_lattice() {
|
||||
// kernel mirror of the facade probe: python pin honors the fallback,
|
||||
// auto/None answer the core (same lattice as cognition_core's _cc_core).
|
||||
assert!(_sg_core(Some("python")).is_none());
|
||||
assert!(_sg_core(Some("auto")).is_some());
|
||||
assert!(_sg_core(None).is_some());
|
||||
let core = _sg_core(None).unwrap();
|
||||
assert_eq!(core, json!("salience_gate"));
|
||||
}
|
||||
|
||||
// ── retained facade unit tests (pre-gap-close surface) ──────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_emergency() {
|
||||
let envelope =
|
||||
json!({"authority_class": "emergency", "urgency": "normal", "payload": {"text": ""}});
|
||||
let r = evaluate_salience(&envelope);
|
||||
assert_eq!(r["minimum_tier"], "T3"); // oracle: 0.9 -> T3
|
||||
assert_eq!(r["score"], 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trivial() {
|
||||
let envelope = json!({"authority_class": "conversation", "urgency": "normal", "payload": {"text": "hello"}});
|
||||
let r = evaluate_salience(&envelope);
|
||||
assert_eq!(r["minimum_tier"], "T0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_score_all_known() {
|
||||
// all known flags return a score
|
||||
for flag in [
|
||||
"EMERGENCY",
|
||||
"OWNER_CORRECTION",
|
||||
"DIRECTIVE",
|
||||
"TASK_FAILURE",
|
||||
"REPEATED_RETRY",
|
||||
"HIGH_UNCERTAINTY",
|
||||
"NOVEL_CAPABILITY",
|
||||
"TRIVIAL",
|
||||
] {
|
||||
let s = flag_score(flag);
|
||||
assert!(s >= 0.0, "{}: score must be non-negative", flag);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_score_unknown_zero() {
|
||||
assert_eq!(flag_score("UNKNOWN_FLAG"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_salience_object() {
|
||||
let env = serde_json::json!({"flags": ["DIRECTIVE", "TASK_FAILURE"]});
|
||||
let r = evaluate_salience(&env);
|
||||
assert!(!r.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_dict_projection_shape() {
|
||||
let d = SalienceDecision::default();
|
||||
let m: BTreeMap<String, Value> = d.to_dict();
|
||||
assert_eq!(m.len(), 5);
|
||||
assert_eq!(m["score"], json!(0.0));
|
||||
assert_eq!(m["minimum_tier"], json!("T0"));
|
||||
assert_eq!(m["witness_needed"], json!(false));
|
||||
}
|
||||
|
||||
/// Phase B bench (std-only: no criterion dep) — hot-path cost of the full
|
||||
/// kernel evaluation over the frozen corpus, plus the word-boundary scan
|
||||
/// micro. Numbers land in the landing evidence log.
|
||||
#[test]
|
||||
fn evaluate_hot_path_bench() {
|
||||
use std::time::Instant;
|
||||
|
||||
let cases: Vec<Value> = envelope_cases()
|
||||
.iter()
|
||||
.filter(|c| c["fn"] == "evaluate_salience_dict")
|
||||
.map(|c| c["args"][0].clone())
|
||||
.collect();
|
||||
assert!(!cases.is_empty(), "corpus must be non-empty");
|
||||
|
||||
let rounds = 1000;
|
||||
let start = Instant::now();
|
||||
let mut sink = 0.0f64;
|
||||
for _ in 0..rounds {
|
||||
for env in &cases {
|
||||
sink += evaluate_decision(env).score;
|
||||
}
|
||||
}
|
||||
let per_call = start.elapsed().as_secs_f64() * 1e9 / (rounds * cases.len()) as f64;
|
||||
|
||||
let text = "the tests still fail after two attempts but maybe a new plan again";
|
||||
let start = Instant::now();
|
||||
let mut hits = 0usize;
|
||||
for _ in 0..100_000 {
|
||||
if word_boundary_contains(text, "still") || word_boundary_contains(text, "attempts") {
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
let per_scan = start.elapsed().as_secs_f64() * 1e9 / 100_000.0;
|
||||
|
||||
assert!(sink.is_finite() && hits > 0);
|
||||
println!(
|
||||
"bench evaluate_decision: {per_call:.1} ns/call over {} frozen envelopes; \
|
||||
word_boundary_contains: {per_scan:.1} ns/scan",
|
||||
cases.len()
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+279
-154
@@ -3,6 +3,8 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn flag_score(flag: &str) -> f64 {
|
||||
match flag {
|
||||
"OWNER_CORRECTION" => 0.8,
|
||||
@@ -23,8 +25,62 @@ pub fn flag_score(flag: &str) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Python SalienceDecision dataclass — decision about how much cognition an
|
||||
/// event deserves.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SalienceDecision {
|
||||
pub score: f64, // 0.0 = trivial, 1.0 = maximum salience
|
||||
pub flags: Vec<String>,
|
||||
pub minimum_tier: String, // T0=deterministic, T1=cheap local, T2=medium
|
||||
pub witness_needed: bool,
|
||||
pub rationale_codes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for SalienceDecision {
|
||||
fn default() -> Self {
|
||||
SalienceDecision {
|
||||
score: 0.0,
|
||||
flags: Vec::new(),
|
||||
minimum_tier: "T0".to_string(),
|
||||
witness_needed: false,
|
||||
rationale_codes: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SalienceDecision {
|
||||
/// Python SalienceDecision.to_dict parity: verbatim field projection.
|
||||
#[must_use]
|
||||
pub fn to_dict(&self) -> BTreeMap<String, Value> {
|
||||
let mut out = BTreeMap::new();
|
||||
out.insert("score".to_string(), serde_json::json!(self.score));
|
||||
out.insert("flags".to_string(), serde_json::json!(self.flags));
|
||||
out.insert(
|
||||
"minimum_tier".to_string(),
|
||||
serde_json::json!(self.minimum_tier),
|
||||
);
|
||||
out.insert(
|
||||
"witness_needed".to_string(),
|
||||
serde_json::json!(self.witness_needed),
|
||||
);
|
||||
out.insert(
|
||||
"rationale_codes".to_string(),
|
||||
serde_json::json!(self.rationale_codes),
|
||||
);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Python re \b parity: a word char is ASCII alphanumeric, '_', or a byte of
|
||||
/// a multibyte letter (Python \w is unicode-aware; the needles are ASCII so
|
||||
/// the byte scan holds, with continuation bytes counting as word chars).
|
||||
#[inline]
|
||||
fn is_word_byte(b: u8) -> bool {
|
||||
b.is_ascii_alphanumeric() || b == b'_' || b & 0xC0 == 0x80
|
||||
}
|
||||
|
||||
/// Word-boundary contains (zero-dep): matches `word` only when not
|
||||
/// surrounded by alphanumeric chars — Python re.search(r"\bword\b") parity.
|
||||
/// surrounded by word chars — Python re.search(r"\bword\b") parity.
|
||||
fn word_boundary_contains(haystack: &str, word: &str) -> bool {
|
||||
let bytes = haystack.as_bytes();
|
||||
let wb = word.as_bytes();
|
||||
@@ -33,9 +89,9 @@ fn word_boundary_contains(haystack: &str, word: &str) -> bool {
|
||||
}
|
||||
for i in 0..=(bytes.len() - wb.len()) {
|
||||
if &bytes[i..i + wb.len()] == wb {
|
||||
let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
|
||||
let before_ok = i == 0 || !is_word_byte(bytes[i - 1]);
|
||||
let after = i + wb.len();
|
||||
let after_ok = after >= bytes.len() || !bytes[after].is_ascii_alphanumeric();
|
||||
let after_ok = after >= bytes.len() || !is_word_byte(bytes[after]);
|
||||
if before_ok && after_ok {
|
||||
return true;
|
||||
}
|
||||
@@ -44,10 +100,37 @@ fn word_boundary_contains(haystack: &str, word: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn evaluate_salience(envelope: &serde_json::Value) -> BTreeMap<String, serde_json::Value> {
|
||||
let mut flags: Vec<String> = Vec::new();
|
||||
let mut score = 0.0f64;
|
||||
let mut rationale: Vec<String> = Vec::new();
|
||||
/// Flags/score/rationale accumulation — one flag word hit, one append (the
|
||||
/// oracle appends once per category check regardless of how many words in
|
||||
/// the family matched).
|
||||
fn add_flag(d: &mut SalienceDecision, flag: &str, code: &str) {
|
||||
d.flags.push(flag.to_string());
|
||||
let s = flag_score(flag);
|
||||
if s > d.score {
|
||||
d.score = s;
|
||||
}
|
||||
d.rationale_codes.push(code.to_string());
|
||||
}
|
||||
|
||||
/// ASCII-anchored lowercase avoidance: keyword matching only needs the
|
||||
/// lowercased text; already-ASCII-lowercase strings (the common case) get
|
||||
/// matched with zero allocation. Non-ASCII (or any uppercase) takes the
|
||||
/// full Python-parity `to_lowercase()` path — Unicode case mappings (e.g.
|
||||
/// 'İ') must never be skipped, so the borrow predicate is ASCII-only.
|
||||
fn lower_for_match(s: &str) -> std::borrow::Cow<'_, str> {
|
||||
if s.is_ascii() && !s.bytes().any(|b| b.is_ascii_uppercase()) {
|
||||
std::borrow::Cow::Borrowed(s)
|
||||
} else {
|
||||
std::borrow::Cow::Owned(s.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed kernel evaluation — mirrors the Python facade's floor exactly:
|
||||
/// authority → urgency → failure → retry → contradiction → uncertainty →
|
||||
/// novel, flags/rationale append in that evaluation order.
|
||||
#[must_use]
|
||||
pub fn evaluate_decision(envelope: &Value) -> SalienceDecision {
|
||||
let mut d = SalienceDecision::default();
|
||||
|
||||
let authority = envelope
|
||||
.get("authority_class")
|
||||
@@ -57,74 +140,45 @@ pub fn evaluate_salience(envelope: &serde_json::Value) -> BTreeMap<String, serde
|
||||
.get("urgency")
|
||||
.and_then(|u| u.as_str())
|
||||
.unwrap_or("normal");
|
||||
let text = envelope
|
||||
.get("payload")
|
||||
.and_then(|p| p.get("text"))
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
|
||||
let add_flag = |flags: &mut Vec<String>,
|
||||
score: &mut f64,
|
||||
rationale: &mut Vec<String>,
|
||||
flag: &str,
|
||||
code: &str| {
|
||||
flags.push(flag.to_string());
|
||||
let s = flag_score(flag);
|
||||
if s > *score {
|
||||
*score = s;
|
||||
}
|
||||
rationale.push(code.to_string());
|
||||
};
|
||||
let text = lower_for_match(
|
||||
envelope
|
||||
.get("payload")
|
||||
.and_then(|p| p.get("text"))
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or(""),
|
||||
);
|
||||
|
||||
match authority {
|
||||
"emergency" => add_flag(
|
||||
&mut flags,
|
||||
&mut score,
|
||||
&mut rationale,
|
||||
"HIGH_RISK_IRREVERSIBLE",
|
||||
"emergency_authority",
|
||||
),
|
||||
"task_amendment" => add_flag(
|
||||
&mut flags,
|
||||
&mut score,
|
||||
&mut rationale,
|
||||
"OWNER_CORRECTION",
|
||||
"task_amendment",
|
||||
),
|
||||
"directive" => add_flag(
|
||||
&mut flags,
|
||||
&mut score,
|
||||
&mut rationale,
|
||||
"EXPLICIT_DIRECTIVE",
|
||||
"directive",
|
||||
),
|
||||
"emergency" => add_flag(&mut d, "HIGH_RISK_IRREVERSIBLE", "emergency_authority"),
|
||||
"task_amendment" => add_flag(&mut d, "OWNER_CORRECTION", "task_amendment"),
|
||||
"directive" => add_flag(&mut d, "EXPLICIT_DIRECTIVE", "directive"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match urgency {
|
||||
"critical" => {
|
||||
score = score.max(0.9);
|
||||
rationale.push("critical_urgency".to_string());
|
||||
d.score = d.score.max(0.9);
|
||||
d.rationale_codes.push("critical_urgency".to_string());
|
||||
}
|
||||
"high" => {
|
||||
score = score.max(0.7);
|
||||
rationale.push("high_urgency".to_string());
|
||||
d.score = d.score.max(0.7);
|
||||
d.rationale_codes.push("high_urgency".to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for word in &["fail", "error", "broken"] {
|
||||
if word_boundary_contains(&text, word) {
|
||||
add_flag(
|
||||
&mut flags,
|
||||
&mut score,
|
||||
&mut rationale,
|
||||
"TASK_FAILURE",
|
||||
"failure_detected",
|
||||
);
|
||||
break;
|
||||
}
|
||||
// ORACLE PARITY (fixed 2026-10-15): the oracle guards the failure family
|
||||
// with ONE `if any(fail/error/broken) or re.search(doesn't work|not
|
||||
// working)` — a single TASK_FAILURE flag even when both alternatives
|
||||
// hit; the previous port omitted the doesn't-work/not-working arm
|
||||
// entirely.
|
||||
if ["fail", "error", "broken"]
|
||||
.iter()
|
||||
.any(|w| word_boundary_contains(&text, w))
|
||||
|| text.contains("doesn't work")
|
||||
|| text.contains("not working")
|
||||
{
|
||||
add_flag(&mut d, "TASK_FAILURE", "failure_detected");
|
||||
}
|
||||
|
||||
// ORACLE PARITY: Python tiers are >=0.8->T3, >=0.6->T2, >=0.4->T1, else T0;
|
||||
@@ -134,66 +188,188 @@ pub fn evaluate_salience(envelope: &serde_json::Value) -> BTreeMap<String, serde
|
||||
// novel-capability checks (previous port omitted them entirely).
|
||||
for word in &["again", "still", "keeps", "repeatedly"] {
|
||||
if word_boundary_contains(&text, word) {
|
||||
add_flag(
|
||||
&mut flags, &mut score, &mut rationale,
|
||||
"REPEATED_RETRY", "repeated_retry",
|
||||
);
|
||||
add_flag(&mut d, "REPEATED_RETRY", "repeated_retry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
for word in &["but", "however", "actually", "instead", "rather"] {
|
||||
if word_boundary_contains(&text, word) {
|
||||
add_flag(
|
||||
&mut flags, &mut score, &mut rationale,
|
||||
"CONTRADICTION", "contradiction_detected",
|
||||
);
|
||||
add_flag(&mut d, "CONTRADICTION", "contradiction_detected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
for word in &["unsure", "uncertain", "maybe", "perhaps", "might"] {
|
||||
if word_boundary_contains(&text, word) {
|
||||
add_flag(
|
||||
&mut flags, &mut score, &mut rationale,
|
||||
"HIGH_UNCERTAINTY", "high_uncertainty",
|
||||
);
|
||||
add_flag(&mut d, "HIGH_UNCERTAINTY", "high_uncertainty");
|
||||
break;
|
||||
}
|
||||
}
|
||||
for word in &["new", "never"] {
|
||||
if word_boundary_contains(&text, word) {
|
||||
add_flag(
|
||||
&mut flags, &mut score, &mut rationale,
|
||||
"NOVEL_CAPABILITY_REQUEST", "novel_request",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if text.contains("first time") || text.contains("haven't") {
|
||||
add_flag(
|
||||
&mut flags, &mut score, &mut rationale,
|
||||
"NOVEL_CAPABILITY_REQUEST", "novel_request",
|
||||
);
|
||||
// ORACLE PARITY (fixed 2026-10-15): the oracle guards the whole novel
|
||||
// family with ONE `if any(word) or re.search(first time|haven't)` — a
|
||||
// single NOVEL flag even when both alternatives hit. The previous port
|
||||
// appended a second flag for the "first time|haven't" arm, inflating
|
||||
// len(flags) (witness rule) — floor rows pin the single append.
|
||||
if ["new", "never"]
|
||||
.iter()
|
||||
.any(|w| word_boundary_contains(&text, w))
|
||||
|| text.contains("first time")
|
||||
|| text.contains("haven't")
|
||||
{
|
||||
add_flag(&mut d, "NOVEL_CAPABILITY_REQUEST", "novel_request");
|
||||
}
|
||||
|
||||
let minimum_tier = match score {
|
||||
d.minimum_tier = match d.score {
|
||||
s if s >= 0.8 => "T3",
|
||||
s if s >= 0.6 => "T2",
|
||||
s if s >= 0.4 => "T1",
|
||||
_ => "T0",
|
||||
};
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let witness_needed = score >= 0.6 || flags.len() >= 2;
|
||||
d.witness_needed = d.score >= 0.6 || d.flags.len() >= 2;
|
||||
|
||||
let mut result = BTreeMap::new();
|
||||
result.insert("score".into(), serde_json::json!(score));
|
||||
result.insert("flags".into(), serde_json::json!(flags));
|
||||
result.insert("minimum_tier".into(), serde_json::json!(minimum_tier));
|
||||
result.insert("witness_needed".into(), serde_json::json!(witness_needed));
|
||||
result.insert("rationale_codes".into(), serde_json::json!(rationale));
|
||||
result
|
||||
d
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn evaluate_salience(envelope: &Value) -> BTreeMap<String, Value> {
|
||||
evaluate_decision(envelope).to_dict()
|
||||
}
|
||||
|
||||
/// evaluate_salience_dict — JSON-view of the evaluation (the module-level
|
||||
/// wrapper the shared freeze harness freezes: the dataclass-returning
|
||||
/// evaluate_salience is not JSON-serializable). Pure to_dict composition —
|
||||
/// zero logic drift from the oracle floor.
|
||||
#[must_use]
|
||||
pub fn evaluate_salience_dict(envelope: &Value) -> Value {
|
||||
serde_json::json!(evaluate_salience(envelope))
|
||||
}
|
||||
|
||||
/// _sg_core: the kernel mirror of the facade's availability probe — the pin
|
||||
/// decision only (from the kernel's side the core always exists; the loader
|
||||
/// module the Python probe returns is unreachable from Rust by design).
|
||||
/// Some ↔ probe answered the loader; None ↔ python pin honored / fallback.
|
||||
#[must_use]
|
||||
pub fn _sg_core(pin: Option<&str>) -> Option<Value> {
|
||||
if pin == Some("python") {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::json!("salience_gate"))
|
||||
}
|
||||
}
|
||||
|
||||
/// handle_hook — Python handle_hook parity: the plugin hook handler for
|
||||
/// incoming events. The oracle accepts `data` and ignores it.
|
||||
#[must_use]
|
||||
pub fn handle_hook(event: &Value, _data: Option<&Value>) -> Value {
|
||||
serde_json::json!({"status": "ok", "event": event, "handler": "salience_gate"})
|
||||
}
|
||||
|
||||
/// Python str(float) parity: integral floats render one decimal (0.0 →
|
||||
/// "0.0"), non-integral keep the shortest round-trip form.
|
||||
fn py_float(f: f64) -> String {
|
||||
if f == f.trunc() && f.is_finite() {
|
||||
format!("{:.1}", f)
|
||||
} else {
|
||||
format!("{}", f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Python repr(list[str]) parity: single-quoted elements.
|
||||
fn py_list_repr(items: &[String]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| format!("'{}'", s)).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
|
||||
/// Python bare `assert` (uncaught AssertionError).
|
||||
fn assert_oracle(cond: bool, msg: &str) {
|
||||
if !cond {
|
||||
panic!("AssertionError: {}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// test_salience — Python test_salience parity: the module's embedded smoke
|
||||
/// test (printed lines byte-matched against the oracle's f-strings).
|
||||
pub fn test_salience() {
|
||||
println!("Testing Salience Gate...");
|
||||
|
||||
let envelope = serde_json::json!({
|
||||
"authority_class": "conversation",
|
||||
"urgency": "normal",
|
||||
"channel": "conversation",
|
||||
"payload": {"text": "hello"},
|
||||
});
|
||||
let d = evaluate_decision(&envelope);
|
||||
println!(
|
||||
"Greeting: score={}, tier={}, flags={}",
|
||||
py_float(d.score),
|
||||
d.minimum_tier,
|
||||
py_list_repr(&d.flags)
|
||||
);
|
||||
assert_oracle(d.score < 0.4, "greeting score below trivial threshold");
|
||||
assert_oracle(
|
||||
d.minimum_tier == "T0",
|
||||
"greeting must be deterministic tier",
|
||||
);
|
||||
|
||||
let envelope = serde_json::json!({
|
||||
"authority_class": "directive",
|
||||
"urgency": "normal",
|
||||
"channel": "work",
|
||||
"payload": {"text": "Fix the failing test"},
|
||||
});
|
||||
let d = evaluate_decision(&envelope);
|
||||
println!(
|
||||
"Directive: score={}, tier={}, flags={}",
|
||||
py_float(d.score),
|
||||
d.minimum_tier,
|
||||
py_list_repr(&d.flags)
|
||||
);
|
||||
assert_oracle(d.score >= 0.6, "directive score below cognition threshold");
|
||||
assert_oracle(
|
||||
d.minimum_tier == "T2" || d.minimum_tier == "T3",
|
||||
"directive tier must be T2 or T3",
|
||||
);
|
||||
|
||||
let envelope = serde_json::json!({
|
||||
"authority_class": "emergency",
|
||||
"urgency": "critical",
|
||||
"channel": "work",
|
||||
"payload": {"text": "Stop everything!"},
|
||||
});
|
||||
let d = evaluate_decision(&envelope);
|
||||
println!(
|
||||
"Emergency: score={}, tier={}, flags={}",
|
||||
py_float(d.score),
|
||||
d.minimum_tier,
|
||||
py_list_repr(&d.flags)
|
||||
);
|
||||
assert_oracle(d.score >= 0.8, "emergency score below strong tier");
|
||||
assert_oracle(d.minimum_tier == "T3", "emergency tier must be T3");
|
||||
|
||||
let envelope = serde_json::json!({
|
||||
"authority_class": "directive",
|
||||
"urgency": "normal",
|
||||
"channel": "work",
|
||||
"payload": {"text": "The tests still fail after two attempts"},
|
||||
});
|
||||
let d = evaluate_decision(&envelope);
|
||||
println!(
|
||||
"Failure: score={}, tier={}, flags={}",
|
||||
py_float(d.score),
|
||||
d.minimum_tier,
|
||||
py_list_repr(&d.flags)
|
||||
);
|
||||
assert_oracle(d.score >= 0.7, "failure score below medium threshold");
|
||||
assert_oracle(
|
||||
d.flags
|
||||
.iter()
|
||||
.any(|f| f == "TASK_FAILURE" || f == "REPEATED_RETRY"),
|
||||
"failure must raise a failure indicator",
|
||||
);
|
||||
|
||||
println!("✅ All tests passed!");
|
||||
}
|
||||
|
||||
// ── FFI surface (facade-2 burn-down, 2026-10-10) ─────────────────────────────
|
||||
use std::ffi::{CStr, CString};
|
||||
@@ -213,7 +389,10 @@ fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sg_evaluate_salience(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).map_err(|e| format!("argument JSON parse failed: {}", e)))
|
||||
.and_then(|t| {
|
||||
serde_json::from_str::<serde_json::Value>(&t)
|
||||
.map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
})
|
||||
.and_then(|a| {
|
||||
let env = a.get("envelope").cloned().ok_or("arg.envelope required")?;
|
||||
let map = evaluate_salience(&env);
|
||||
@@ -243,58 +422,4 @@ pub extern "C" fn sg_free_string(ptr: *mut c_char) {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_emergency() {
|
||||
let envelope =
|
||||
json!({"authority_class": "emergency", "urgency": "normal", "payload": {"text": ""}});
|
||||
let r = evaluate_salience(&envelope);
|
||||
assert_eq!(r["minimum_tier"], "T3"); // oracle: 0.9 -> T3
|
||||
assert_eq!(r["score"], 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trivial() {
|
||||
let envelope = json!({"authority_class": "conversation", "urgency": "normal", "payload": {"text": "hello"}});
|
||||
let r = evaluate_salience(&envelope);
|
||||
assert_eq!(r["minimum_tier"], "T0");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flag_score_all_known() {
|
||||
// all known flags return a score
|
||||
for flag in [
|
||||
"EMERGENCY",
|
||||
"OWNER_CORRECTION",
|
||||
"DIRECTIVE",
|
||||
"TASK_FAILURE",
|
||||
"REPEATED_RETRY",
|
||||
"HIGH_UNCERTAINTY",
|
||||
"NOVEL_CAPABILITY",
|
||||
"TRIVIAL",
|
||||
] {
|
||||
let s = flag_score(flag);
|
||||
assert!(s >= 0.0, "{}: score must be non-negative", flag);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_score_unknown_zero() {
|
||||
assert_eq!(flag_score("UNKNOWN_FLAG"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_salience_object() {
|
||||
let env = serde_json::json!({"flags": ["DIRECTIVE", "TASK_FAILURE"]});
|
||||
let r = evaluate_salience(&env);
|
||||
assert!(!r.is_empty());
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-13T23:56:33Z-c011dd-factory-gap-mp
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Close the PARTIAL gaps in src/go/model-profiles against the src/rune/model_profiles.py oracle (dichotomy wave, issue #2479 tranche): name-preserving Go ports for the 10 unmatched functions (get_profile_path — filesystem sanitization, load_profile — suppressed-exception floor + unknown-key filtering, save_profile — Python-shaped JSON writer, build_profile_from_ollama — knowledge merge + uncensored tier + frozen-clock stamps, scan_all_models — Ollama sweep with banker's rounding + learned-data preservation, get_or_create_profile, record_interaction — learning accumulator, get_prompt_context — operator string assembly, to_dict/from_dict — the dataclass lens) plus the stdin-op serve CLI over the same code paths, frozen-truth corpus generated against the live Python oracle through the scripts/mp_freeze_seams rig (HOME pinned before import, per-case hermetic store wipe+seed, canned Ollama server, frozen clock; 36 verbatim cases incl. the save_profile byte-parity rows), battery 36/36, gofmt+vet clean, live parity 100% (143/143), pytest 31/31 both pins.
|
||||
|
||||
## 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-13T23:58:33Z", "contract": "BUILDER-2026-10-13T23:56:33Z-c011dd-factory-gap-mp", "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_model_profiles_parity.py", "scripts/gen_model_profiles_truth.py", "scripts/gen_mp_battery.py", "scripts/mp_freeze_seams/sitecustomize.py", "src/go/model-profiles/frozen_battery_test.go", "src/go/model-profiles/frozen_mp_truth.json", "src/go/model-profiles/go.mod", "src/go/model-profiles/main.go", "src/go/model-profiles/truth.json"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-gap-mp
|
||||
- builder:factory-gap-mp
|
||||
- builder:factory-gap-mp
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
salience_gate dichotomy gap-close (RUST): name-preserving port of the 4 audit-missing functions into src/rust/salience_gate (the module's existing delegation side — classification side=RUST; no Go salience-gate dir exists): _sg_core (kernel mirror of the facade availability probe, pin lattice), to_dict (SalienceDecision struct + projection, evaluate_salience flows through it), handle_hook (event echo, data accepted+ignored), test_salience (embedded oracle smoke, print-byte-matched), plus oracle-level evaluate_salience_dict JSON view (shared freeze harness cannot serialize the dataclass return — construction_decision normalize_need_json precedent). Dichotomy audit 1/5 PARTIAL -> 6/6 FULL. Three parity defects fixed (frozen-truth battery caught all): novel-family single-append (oracle guards ONE if; duplicate append inflated witness rule), failure-family doesn't-work/not-working arm omitted, word_boundary_contains Python-\b parity (alphanumeric + underscore + UTF-8 continuation bytes). Frozen truth via shared harness scripts/freeze_truth.py: frozen_sg_truth.verify.json 52 rows byte-identical under freeze_truth.py --verify in BOTH core states (floor SG_RUST_LIB-poisoned AND kernel-built); frozen_sg_truth.json 63-row envelope (multi-family evaluation-order rows + _sg_core row — the shared gate's RUNE_SALIENCE_GATE_CORE pin cannot reach this oracle). branch_battery.rs replays both verbatim via include_str (12 tests); poisoned-truth law: generator pins RUNE_SALIENCE_CORE=python AND poisons SG_RUST_LIB (the module's real floor pins — the facade evaluate path ignores RUNE_*_CORE). Live parity: gen --replay facade live + kernel direct 119/119 (exact evaluation-order kernel rows; facade rows under the oracle's declared sorted-comparison contract); crate canary 6/6; oracle smoke green in 3 pin states (live, probe-pinned, pure floor). Phase B (2350e02e): ASCII-anchored Cow lowercase avoidance (zero-alloc common path, Unicode mappings never skipped), std-only hot-path bench (ns/call over the 56-envelope frozen corpus + ns/scan), #[must_use] pure surface, idiomatic single-append family guards. rustfmt clean, clippy -D warnings clean. Evidence: verification/salience-gate-*.log (cargotest/clippy/freeze-verify/replay/canary/python-pins/goladder/audit, all exit 0, Read back). Delivered on gitea/master: feat 9539d5b6, phase B 2350e02e.
|
||||
|
||||
## 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-14T01:26:41Z", "contract": "BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg", "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_salience_gate.py", "scripts/gen_salience_gate_truth.py", "scripts/resume_close_contract_salience_gate.py", "src/rune/salience_gate.py", "src/rune/sg_loader.py", "src/rust/salience_gate/Cargo.toml", "src/rust/salience_gate/canary_salience_gate.py", "src/rust/salience_gate/frozen_salience.json", "src/rust/salience_gate/src/branch_battery.rs", "src/rust/salience_gate/src/frozen_sg_truth.json", "src/rust/salience_gate/src/frozen_sg_truth.verify.json", "src/rust/salience_gate/src/lib.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- parity 9539d5b6, phase B 2350e02e
|
||||
- parity 9539d5b6, phase B 2350e02e
|
||||
- parity 9539d5b6, phase B 2350e02e
|
||||
- closure-record
|
||||
+30
@@ -32,6 +32,36 @@
|
||||
"affected_paths": [],
|
||||
"affected_regions": [],
|
||||
"affected_ui_surfaces": [],
|
||||
"contract_quality": {
|
||||
"candidate_corrections": [],
|
||||
"captured_at": "2026-10-14T00:10:03Z",
|
||||
"contract": "BUILDER-2026-10-13T15:18:58Z-fb34a0-factory-gap-he",
|
||||
"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"
|
||||
],
|
||||
"repaired_post_hoc": "quality-gate lapse repair 2026-10-13 (closed via raw transition predating 16c5f22a \u2014 quality computed from + verified evidence)"
|
||||
},
|
||||
"decision_trace": {
|
||||
"affected_surfaces": {
|
||||
"affected_existing_entities": [],
|
||||
|
||||
+30
@@ -32,6 +32,36 @@
|
||||
"affected_paths": [],
|
||||
"affected_regions": [],
|
||||
"affected_ui_surfaces": [],
|
||||
"contract_quality": {
|
||||
"candidate_corrections": [],
|
||||
"captured_at": "2026-10-14T00:10:30Z",
|
||||
"contract": "BUILDER-2026-10-13T16:25:22Z-7e3fd4-factory-gap-ar",
|
||||
"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"
|
||||
],
|
||||
"repaired_post_hoc": "quality-gate lapse repair 2026-10-13 (closed via raw transition predating 16c5f22a \u2014 quality computed from + verified evidence)"
|
||||
},
|
||||
"decision_trace": {
|
||||
"affected_surfaces": {
|
||||
"affected_existing_entities": [],
|
||||
|
||||
+5930
File diff suppressed because it is too large.
Load diff
+6003
File diff suppressed because it is too large.
Load diff
@@ -3958,3 +3958,13 @@
|
||||
{"builder": "test-kernel-rebuild-trap", "detail": "BUILDER-2099-01-01T00:00:00Z-kerneltrap-regression \u2192 approved", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:53:37Z", "operation": "transition"}
|
||||
{"builder": "test-kernel-rebuild-trap", "detail": "BUILDER-2099-01-01T00:00:00Z-kerneltrap-regression \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:53:53Z", "operation": "transition"}
|
||||
{"builder": "test-kernel-rebuild-trap", "detail": "BUILDER-2099-01-01T00:00:00Z-kerneltrap-regression \u2192 verifying", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:54:10Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-mp", "detail": "need-2026-10-13T23:56:27Z-factory-gap-mp \u2192 REUSE", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:56:33Z", "operation": "submit_need"}
|
||||
{"builder": "factory-gap-mp", "detail": "BUILDER-2026-10-13T23:56:33Z-c011dd-factory-gap-mp \u2192 approved", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:57:12Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-mp", "detail": "BUILDER-2026-10-13T23:56:33Z-c011dd-factory-gap-mp \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:57:41Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-mp", "detail": "BUILDER-2026-10-13T23:56:33Z-c011dd-factory-gap-mp \u2192 verifying", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:58:06Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-mp", "detail": "BUILDER-2026-10-13T23:56:33Z-c011dd-factory-gap-mp closed", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-13T23:58:33Z", "operation": "close"}
|
||||
{"builder": "factory-gap-sg", "detail": "need-2026-10-14T01:24:06Z-factory-gap-sg \u2192 REUSE", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-14T01:24:10Z", "operation": "submit_need"}
|
||||
{"builder": "factory-gap-sg", "detail": "BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg \u2192 approved", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-14T01:24:40Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-sg", "detail": "BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-14T01:25:17Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-sg", "detail": "BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg \u2192 verifying", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-14T01:25:40Z", "operation": "transition"}
|
||||
{"builder": "factory-gap-sg", "detail": "BUILDER-2026-10-14T01:24:10Z-2a26ea-factory-gap-sg closed", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-14T01:26:41Z", "operation": "close"}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,78 @@
|
||||
# cron_engine GO gap-close — verification summary (#2479 dichotomy wave)
|
||||
|
||||
Worktree rune-wave-co (detached HEAD @ 801b631e → pushed via `gitea HEAD:master`).
|
||||
Oracle: /mnt/workdrive/rune/src/rune/cron_engine.py (440 lines, 27 fns).
|
||||
|
||||
## Coverage
|
||||
- BEFORE: 13/27 matched, PARTIAL (missing 14: get_cron_engine, run_task×2,
|
||||
start_engine, stop_engine, to_dict, fire_on_task_complete, stop, _run_loop,
|
||||
_parse_cron_field, _should_run, _execute_task, _log_execution, _log_error —
|
||||
the audit's camel generator cannot see parseCronField/shouldRun for
|
||||
leading-underscore names; both now carry the Python-name mirror comments AND
|
||||
real ports).
|
||||
- AFTER: 27/27 FULL (docs/dichotomy-rebase-honest-audit.md regenerated;
|
||||
83/192 modules FULL). Evidence: ce-audit.log.
|
||||
|
||||
## Commands + exit codes (all evidence in verification/ce-*)
|
||||
| gate | evidence | exit |
|
||||
|---|---|---|
|
||||
| go build ./... | ce-gobuild.log | 0 |
|
||||
| go test -v (5 tests) | ce-gotest.log | 0 |
|
||||
| gofmt -l (clean) | ce-gofmt.log | 0 |
|
||||
| go vet ./... | ce-govet.log | 0 |
|
||||
| replay truth.json (batch 1) | ce-replay-legacy.log — PARITY OK 25 | 0 |
|
||||
| replay-gap truth_gap.json | ce-replay-gap.log — PARITY OK 46 | 0 |
|
||||
| gen_cron_engine_gap_truth.py --verify | ce-truth-verify.log — 46 byte-identical | 0 |
|
||||
| fresh re-freeze byte-identity | ce-truth-regen.log — REGEN BYTE-IDENTICAL | 0 |
|
||||
| check_cron_engine_parity.py (live) | ce-live-parity.log — 3/3 scenarios, 31 ops | 0 |
|
||||
| Python CLI smoke both pins + GO CLI | ce-python-cli-smoke.log (byte-identical outputs) | 0 |
|
||||
| pytest test_subsystem_health (python pin) | ce-python-pytest.log — 6 passed | 0 |
|
||||
|
||||
## Truth-freeze technique
|
||||
Shared harness (scripts/freeze_truth.py) + wrapper shims
|
||||
(scripts/gen_cron_engine_gap_truth.py, registered as
|
||||
gen_cron_engine_gap_truth_shims): frozen clock (ce.datetime patched with the
|
||||
harness's frozen class — module-level import reaches every method), hermetic
|
||||
per-row HOME tokenized to <CE_HOME>, _run_loop rows run with cron_engine.time
|
||||
swapped for a stopper (exactly one iteration then clean exit), record_failure
|
||||
neutralized (hermetic), capture = {result, post, files, storage, log}.
|
||||
46 cases, envelope frozen_now 2026-10-13T09:20:00. --verify byte-identical;
|
||||
fresh regen byte-identical.
|
||||
|
||||
## Go side
|
||||
- src/go/cron-engine/main.go: toDict (snapshot copy — Python to_dict builds a
|
||||
fresh dict), runTask (method + module), fireOnTaskComplete, start/stop,
|
||||
runLoop, executeTask, logExecution/logError (5MB rotation), getCronEngine
|
||||
singleton family, shlexSplit (posix, oracle ValueError messages), pyISO
|
||||
(isoformat +00:00 / micro elision), Python-exact ordered _save
|
||||
(insertion order + ensure_ascii + indent 2, "{}" when empty) + strict
|
||||
ordered _load (CronTask(**data) TypeError semantics), ExitError→returncode
|
||||
(nonzero exit is a value, not an error), error-message translation layer
|
||||
(FileNotFoundError / PermissionError / TimeoutExpired str() forms).
|
||||
- add/remove/enable/disable now persist via _save when a storage path is set
|
||||
(oracle behavior; in-memory battery construction unaffected).
|
||||
- scenario.go: replay-gap interpreter + scenario driver (live parity) +
|
||||
ordered JSON arg decoding (Go maps lose insertion order — ordObj).
|
||||
- CI gate extended (cron-engine step): replay + replay-gap + both batteries.
|
||||
|
||||
## Documented divergences (not bugs)
|
||||
1. 60s subprocess timeout branch not frozen through the oracle (a real
|
||||
TimeoutExpired costs 60s); the port carries the exact
|
||||
`Command '[...]' timed out after N seconds` str(e) shape, unit-pinned in
|
||||
TestOracleErrorShapes + TestTimeoutBranchReplay (seam-shrunk).
|
||||
2. add_task/_save failure branch: oracle propagates exceptions; port records
|
||||
via the recordFailure seam and completes the mutation (module boundary —
|
||||
src/go/subsystem-health is package main, not importable; seam is
|
||||
injectable). Truth rows never exercise unwritable storage.
|
||||
3. Typed struct vs unvalidated dataclass: a wrong-TYPED value the dataclass
|
||||
would accept (CronTask(name=5)) is rejected by Go's JSON decode; the
|
||||
strict key-set check mirrors the realistic TypeError corruption mode.
|
||||
4. Bad cron expression in the loop: oracle thread dies silently (traceback
|
||||
swallowed by threading); port's goroutine returns, running stays true —
|
||||
identical observables (no further executions).
|
||||
5. step-0 (*/0): oracle raises ValueError("range() arg 3 must not be zero")
|
||||
and the freeze harness cannot capture exceptions — port returns the same
|
||||
error text (crash-parity quirk family), unit-pinned.
|
||||
|
||||
## SHAs
|
||||
(Recorded after final push — see close contract chore(store) commit.)
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
audit_exit=0
|
||||
@@ -0,0 +1 @@
|
||||
test_exit=0
|
||||
@@ -0,0 +1 @@
|
||||
parity_exit=0
|
||||
@@ -0,0 +1 @@
|
||||
test_exit=1 (18 test_p1_01 failures PRE-EXISTING: identical on stashed base and both pins; microkernel-adjacent suite green; no Python files touched)
|
||||
@@ -0,0 +1 @@
|
||||
test_exit=1 (18 test_p1_01 failures PRE-EXISTING: identical on stashed base and both pins; microkernel-adjacent suite green; no Python files touched)
|
||||
@@ -0,0 +1 @@
|
||||
verify_exit=0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,42 @@
|
||||
# model_profiles dichotomy gap-close — verification summary
|
||||
|
||||
- **Module**: model_profiles (GO port, src/go/model-profiles)
|
||||
- **Parity SHA**: (parity commit — filled after push)
|
||||
- **Contract**: BUILDER-2026-10-13T22:50:00Z-<id>-factory-gap-mp — closed
|
||||
(submit→approved→building→verifying→closed walked in ONE process)
|
||||
|
||||
## Commands and exit codes
|
||||
|
||||
| Check | Command | Exit | Evidence |
|
||||
|---|---|---|---|
|
||||
| frozen truth byte-identical | `PYTHONPATH=scripts/mp_freeze_seams:src python3 scripts/freeze_truth.py --verify src/go/model-profiles/frozen_mp_truth.json --module rune.model_profiles` | 0 | `verification/mp-freeze-verify.log` / `.exit` — OK: 36 cases byte-identical against live oracle |
|
||||
| fmt+vet+build | `gofmt -l . && go vet . && go build ./...` (in src/go/model-profiles) | 0 | `verification/mp-gofmt.log` (empty), `verification/mp-govet.log`, `verification/mp-gobuild.log` + `.exit` files |
|
||||
| Go battery | `go test -count=1 -v .` (in src/go/model-profiles) | 0 | `verification/mp-gotest.log` / `.exit` — TestFrozenBattery PASS (36/36 frozen cases replayed VERBATIM from embedded truth) |
|
||||
| live parity | `PYTHONPATH=src python3 scripts/check_model_profiles_parity.py --bin /tmp/mp-go` | 0 | `verification/mp-parity.log` / `.exit` — parity: 143/143 match |
|
||||
| Python tests (live delegation) | `python3 -m pytest tests/test_p1_06_model_routing.py tests/test_p1_07_runtime_objects.py -q` | 0 | `verification/mp-pytest-live.log` — 31 passed |
|
||||
| Python tests (python pin) | `RUNE_MODEL_PROFILES_CORE=python python3 -m pytest ...` | 0 | `verification/mp-pytest-pinned.log` — 31 passed |
|
||||
| contract close walk | `python3 scripts/walk_contract_model_profiles_gap.py` | 0 | `verification/mp-close-walk.log` / `.exit` — FINAL PHASE: closed |
|
||||
|
||||
## Coverage
|
||||
|
||||
Dichotomy audit: model_profiles 5/15 PARTIAL → **15/15 FULL** (GO);
|
||||
missing [] (verified via `scripts/dichotomy_audit.py` post-port).
|
||||
|
||||
## Divergences (documented, not bugs)
|
||||
|
||||
1. `load_profile({"name": <non-string>})` is ACCEPTED by the Python
|
||||
oracle (dataclass `__init__` performs no type checks) but REJECTED
|
||||
by the Go port (typed signature) — the local_tool_runtime precedent
|
||||
(bool/int-union). No frozen row carries that shape.
|
||||
2. Integral-float rendering: the oracle's build path assigns int `0`
|
||||
where the dataclass declares float (`size_gb=0` vs `0.0`); the
|
||||
parity sweep canonicalizes integral floats before compare. The
|
||||
frozen save_profile byte-parity corpus pins the file writer to the
|
||||
oracle's exact bytes (asdict field order, indent-2, ensure_ascii,
|
||||
integral floats with trailing `.0`).
|
||||
|
||||
## Files
|
||||
|
||||
- `scripts/walk_contract_model_profiles_gap.py` — the one-process close walk (committed)
|
||||
- `store/scaffold/segments/entities/contract__BUILDER-*.json` — contract entity (phase: closed)
|
||||
- `store/scaffold/segments/observations.jsonl` — walk journal rows
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,2 @@
|
||||
0
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user