Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd4c0beb2f | ||
|
|
9436e6ca26 | ||
|
|
bb7251e1aa | ||
|
|
0cd50f46ed | ||
|
|
2e5ba22249 | ||
|
|
7fbbdeba71 | ||
|
|
f129a6a469 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.217.0**
|
||||
**Current release: v0.219.0**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.217.0"
|
||||
version = "0.219.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_outcome_ledger_truth.py — outcome_ledger dichotomy wave truth battery
|
||||
(#2479 tranche). Drives the LIVE python oracle (RUNE_OL_CORE=python pinned)
|
||||
through every public function and branch, and freezes the observations to
|
||||
src/rust/outcome_ledger/src/frozen_to_truth.json.
|
||||
|
||||
Public surface exercised:
|
||||
* OutcomeLedger.__init__ (fresh db + reopen-existing branches)
|
||||
* OutcomeLedger.record (all field branches: success/fail, correction,
|
||||
floats, defaults)
|
||||
* OutcomeLedger.get_stats (no-family branch AND task_family branch,
|
||||
empty-db zero branch AND populated branch)
|
||||
* OutcomeLedger.get_model_stats (hit and miss)
|
||||
* OutcomeRecord.to_dict (full record)
|
||||
(test_ledger stays a facade-level python self-test — covered by live
|
||||
parity, not by the kernel battery.)
|
||||
|
||||
Timestamps are PINNED in the inputs, so observations are fully
|
||||
deterministic and need no masking.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
os.environ["RUNE_OL_CORE"] = "python"
|
||||
|
||||
from rune.outcome_ledger import OutcomeLedger, OutcomeRecord # noqa: E402
|
||||
|
||||
T0 = "2026-10-11T00:00:01+00:00"
|
||||
T1 = "2026-10-11T00:00:02+00:00"
|
||||
T2 = "2026-10-11T00:00:03+00:00"
|
||||
T3 = "2026-10-11T00:00:04+00:00"
|
||||
T4 = "2026-10-11T00:00:05+00:00"
|
||||
T5 = "2026-10-11T00:00:06+00:00"
|
||||
|
||||
|
||||
class Driver:
|
||||
"""Drives op scripts against the live oracle the way the truth was
|
||||
frozen — each scenario gets its own temp db path."""
|
||||
|
||||
def __init__(self, workdir: Path, n: int):
|
||||
self.path = str(workdir / f"ol_{n}.db")
|
||||
self.ledger = None
|
||||
|
||||
def step(self, op: dict):
|
||||
kind = op["op"]
|
||||
if kind == "new":
|
||||
if not op.get("reuse"):
|
||||
self.ledger = OutcomeLedger(self.path)
|
||||
else:
|
||||
# fresh instance over the SAME db (CREATE TABLE IF NOT
|
||||
# EXISTS idempotent branch)
|
||||
self.ledger = OutcomeLedger(self.path)
|
||||
return {"result": None}
|
||||
if kind == "record":
|
||||
self.ledger.record(OutcomeRecord(**op["record"]))
|
||||
return {"result": None}
|
||||
if kind == "to_dict":
|
||||
return {"result": OutcomeRecord(**op["record"]).to_dict()}
|
||||
if kind == "get_stats":
|
||||
fam = op.get("family")
|
||||
if fam is None:
|
||||
return {"result": self.ledger.get_stats()}
|
||||
return {"result": self.ledger.get_stats(fam)}
|
||||
if kind == "get_model_stats":
|
||||
return {"result": self.ledger.get_model_stats(op["model"])}
|
||||
raise ValueError(f"unknown op {kind}")
|
||||
|
||||
|
||||
def s1_empty_db(workdir):
|
||||
"""Empty-database branches of every stats path."""
|
||||
d = Driver(workdir, 1)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_stats", "family": "greeting"},
|
||||
{"op": "get_model_stats", "model": "qwen2.5:3b"},
|
||||
]
|
||||
return {"name": "s1_empty_db", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s2_record_and_stats(workdir):
|
||||
"""The oracle's own test_ledger data, plus every stats entry point."""
|
||||
d = Driver(workdir, 2)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T0, "task_family": "greeting", "model": "qwen2.5:3b",
|
||||
"provider": "ollama", "tier": "T1", "task_success": True,
|
||||
"latency_ms": 100.0}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T1, "task_family": "coding", "model": "qwen3:14b",
|
||||
"provider": "ollama", "tier": "T2", "task_success": True,
|
||||
"attempts": 2, "latency_ms": 500.0}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T2, "task_family": "coding", "model": "qwen3:14b",
|
||||
"provider": "ollama", "tier": "T2", "task_success": False,
|
||||
"user_correction": True, "latency_ms": 600.0}},
|
||||
{"op": "get_stats", "family": "greeting"},
|
||||
{"op": "get_stats", "family": "coding"},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_model_stats", "model": "qwen3:14b"},
|
||||
{"op": "get_model_stats", "model": "unknown-model"},
|
||||
]
|
||||
return {"name": "s2_record_and_stats", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s3_full_record_to_dict(workdir):
|
||||
"""OutcomeRecord.to_dict with every field set; float-heavy stats."""
|
||||
d = Driver(workdir, 3)
|
||||
full = {
|
||||
"timestamp": T3, "task_family": "review", "model": "gpt-5-mini",
|
||||
"provider": "openai", "tier": "T3", "prompt_tokens": 1200,
|
||||
"output_tokens": 340, "cached_tokens": 512, "latency_ms": 123.75,
|
||||
"attempts": 1, "verification_result": "pass", "user_correction": False,
|
||||
"task_success": True, "estimated_cost": 0.0025,
|
||||
}
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "to_dict", "record": full},
|
||||
{"op": "record", "record": full},
|
||||
{"op": "record", "record": {**full, "timestamp": T4,
|
||||
"estimated_cost": 0.0075,
|
||||
"verification_result": "fail",
|
||||
"task_success": False}},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_stats", "family": "review"},
|
||||
{"op": "get_model_stats", "model": "gpt-5-mini"},
|
||||
]
|
||||
return {"name": "s3_full_record_to_dict", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s4_persistence_reopen(workdir):
|
||||
"""A fresh OutcomeLedger over an existing db — rows survive, DDL is
|
||||
idempotent."""
|
||||
d = Driver(workdir, 4)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T0, "task_family": "port", "model": "qwen2.5:3b",
|
||||
"provider": "ollama", "tier": "T1", "task_success": True,
|
||||
"latency_ms": 42.5}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T1, "task_family": "port", "model": "qwen3:14b",
|
||||
"provider": "ollama", "tier": "T2", "task_success": False,
|
||||
"user_correction": True, "attempts": 3, "latency_ms": 900.0}},
|
||||
{"op": "new", "reuse": True},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_stats", "family": "port"},
|
||||
{"op": "get_model_stats", "model": "qwen2.5:3b"},
|
||||
]
|
||||
return {"name": "s4_persistence_reopen", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s5_averages_and_rates(workdir):
|
||||
"""AVG/rate arithmetic: fractional averages, 0%/50%/100% rates, both
|
||||
correction branches."""
|
||||
d = Driver(workdir, 5)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T0, "task_family": "ops", "model": "m1",
|
||||
"task_success": True, "attempts": 1, "latency_ms": 10.5,
|
||||
"estimated_cost": 0.0}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T1, "task_family": "ops", "model": "m1",
|
||||
"task_success": False, "user_correction": True, "attempts": 2,
|
||||
"latency_ms": 20.25, "estimated_cost": 1.5}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T2, "task_family": "ops", "model": "m2",
|
||||
"task_success": True, "attempts": 3, "latency_ms": 30.125,
|
||||
"user_correction": True, "estimated_cost": 2.25}},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_model_stats", "model": "m1"},
|
||||
{"op": "get_model_stats", "model": "m2"},
|
||||
{"op": "get_stats", "family": "ops"},
|
||||
]
|
||||
return {"name": "s5_averages_and_rates", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def main():
|
||||
out = {
|
||||
"module": "outcome_ledger",
|
||||
"seed": 2479,
|
||||
"oracle": "src/rune/outcome_ledger.py",
|
||||
"pin": "RUNE_OL_CORE=python",
|
||||
"generated": "2026-10-11",
|
||||
"scenarios": [],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
workdir = Path(td)
|
||||
for gen in (s1_empty_db, s2_record_and_stats, s3_full_record_to_dict,
|
||||
s4_persistence_reopen, s5_averages_and_rates):
|
||||
out["scenarios"].append(gen(workdir))
|
||||
dest = REPO / "src" / "rust" / "outcome_ledger" / "src" / "frozen_to_truth.json"
|
||||
dest.write_text(json.dumps(out, indent=1) + "\n")
|
||||
n_obs = sum(len(s["obs"]) for s in out["scenarios"])
|
||||
print(f"wrote {len(out['scenarios'])} scenarios / {n_obs} observations to {dest}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/auto_context
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,864 @@
|
||||
// Package main — the FULL GO port of src/rune/auto_context.py (#2479
|
||||
// dichotomy tranche, GO side verdict). Every public Python function has a
|
||||
// name-preserving counterpart: run_warm_router → runWarmRouter,
|
||||
// run_memory_recall → runMemoryRecall, run_always_digest → runAlwaysDigest,
|
||||
// load_active_tasks → loadActiveTasks, build_auto_context →
|
||||
// buildAutoContext, save_context → saveContext, handle_hook → handleHook,
|
||||
// and the module CLI main() (argparse-compatible: build/show/hook, --event,
|
||||
// --data, --json; invalid choices exit 2). The PLUGIN manifest rides along
|
||||
// as pluginManifest().
|
||||
//
|
||||
// IO (subprocess runs, memory paths, wall clock) stays behind injectable
|
||||
// seams so the frozen truth battery replays deterministically: RunScript,
|
||||
// MemoryDir, ScriptsDir, PythonBin and ClockNow are swappable vars, swapped
|
||||
// per truth row by replay.go and restored after each row. The oracle's
|
||||
// subprocess payloads (warm_router.py route/always, memory_store.py recall)
|
||||
// arrive through the RunScript seam; the frozen truth pins the exact
|
||||
// message/query strings the port hands to that seam.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ── configuration ────────────────────────────────────────────────────────
|
||||
|
||||
// stopwordSet — the oracle's recall stopword table, 1:1.
|
||||
var stopwordSet = map[string]struct{}{
|
||||
"what": {}, "when": {}, "where": {}, "which": {}, "that": {}, "this": {},
|
||||
"with": {}, "from": {}, "have": {}, "been": {}, "were": {}, "they": {},
|
||||
"their": {}, "about": {}, "would": {}, "could": {}, "should": {},
|
||||
"will": {}, "into": {}, "your": {}, "just": {}, "like": {}, "know": {},
|
||||
"think": {}, "also": {}, "back": {}, "then": {}, "than": {}, "more": {},
|
||||
"some": {}, "only": {}, "very": {}, "does": {}, "done": {}, "make": {},
|
||||
"made": {}, "want": {}, "need": {}, "give": {}, "tell": {}, "there": {},
|
||||
"these": {}, "those": {}, "because": {}, "between": {}, "after": {},
|
||||
"before": {}, "under": {}, "over": {}, "again": {}, "once": {},
|
||||
"here": {}, "each": {}, "every": {}, "both": {}, "few": {}, "most": {},
|
||||
"other": {}, "such": {}, "same": {}, "being": {}, "having": {},
|
||||
}
|
||||
|
||||
// ── seams (swapped per truth row by replay.go) ───────────────────────────
|
||||
|
||||
// ExecResult — the slice of subprocess.run(capture_output=True, text=True)
|
||||
// the oracle consumes: stdout text, return code, and a non-nil Err when the
|
||||
// run failed to start, was killed, or hit its timeout (subprocess raised).
|
||||
type ExecResult struct {
|
||||
Stdout string
|
||||
ReturnCode int
|
||||
Err error
|
||||
}
|
||||
|
||||
var (
|
||||
// defaultPythonBin — memory_env.python_bin(): sys.executable of the
|
||||
// parent process (same venv, portable by construction).
|
||||
defaultPythonBin = func() string {
|
||||
if p := os.Getenv("RUNE_PYTHON"); p != "" {
|
||||
return p
|
||||
}
|
||||
return "python3"
|
||||
}
|
||||
// defaultScriptsDir — memory_env.scripts_dir() (RUNE_SCRIPTS_DIR).
|
||||
defaultScriptsDir = func() string {
|
||||
if d := os.Getenv("RUNE_SCRIPTS_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
return "src/rune"
|
||||
}
|
||||
// defaultMemoryDir — memory_env.memory_dir() (RUNE_MEMORY_DIR else
|
||||
// RUNE_HOME/memory else the default rune home).
|
||||
defaultMemoryDir = func() string {
|
||||
if d := os.Getenv("RUNE_MEMORY_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
home := os.Getenv("RUNE_HOME")
|
||||
if home == "" {
|
||||
home = "/mnt/workdrive/rune"
|
||||
}
|
||||
return filepath.Join(home, "memory")
|
||||
}
|
||||
// defaultClockNow — datetime.now(timezone.utc); build_auto_context
|
||||
// calls it once per build.
|
||||
defaultClockNow = func() time.Time { return time.Now().UTC() }
|
||||
// defaultRunScript — subprocess.run([python, script, ...], timeout=N):
|
||||
// timeout expiry surfaces as Err (subprocess.TimeoutExpired), a
|
||||
// nonzero exit keeps Stdout and sets ReturnCode.
|
||||
defaultRunScript = func(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, exe, args...)
|
||||
out, err := cmd.Output()
|
||||
res := ExecResult{Stdout: string(out)}
|
||||
if err != nil {
|
||||
res.Err = err
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
res.ReturnCode = ee.ExitCode()
|
||||
} else {
|
||||
res.ReturnCode = -1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
PythonBin = defaultPythonBin
|
||||
ScriptsDir = defaultScriptsDir
|
||||
MemoryDir = defaultMemoryDir
|
||||
)
|
||||
|
||||
// CommandRunner — the subprocess seam as an interface (Phase B): the
|
||||
// runner is swappable in embedded callers and tests (fake runners,
|
||||
// counting/pooled runners) without touching port code; the default
|
||||
// implementation is compile-time asserted against the contract.
|
||||
type CommandRunner interface {
|
||||
Run(exe string, args []string, timeout time.Duration) ExecResult
|
||||
}
|
||||
|
||||
// RunnerFunc adapts a plain function to CommandRunner.
|
||||
type RunnerFunc func(exe string, args []string, timeout time.Duration) ExecResult
|
||||
|
||||
// Run satisfies CommandRunner.
|
||||
func (f RunnerFunc) Run(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
return f(exe, args, timeout)
|
||||
}
|
||||
|
||||
// compile-time: the default subprocess implementation satisfies the seam
|
||||
var _ CommandRunner = RunnerFunc(defaultRunScript)
|
||||
|
||||
// Clock — the wall-clock seam as an interface (Phase B): build_auto_context
|
||||
// reads it exactly once per build, like the oracle's datetime.now call.
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// ClockFunc adapts a plain function to Clock.
|
||||
type ClockFunc func() time.Time
|
||||
|
||||
// Now satisfies Clock.
|
||||
func (f ClockFunc) Now() time.Time { return f() }
|
||||
|
||||
// compile-time: the default clock satisfies the seam
|
||||
var _ Clock = ClockFunc(defaultClockNow)
|
||||
|
||||
// defaultRunner / defaultClock — the real implementations, kept as named
|
||||
// seams so per-row swaps in replay.go can restore them.
|
||||
var defaultRunner CommandRunner = RunnerFunc(defaultRunScript)
|
||||
var defaultClock Clock = ClockFunc(defaultClockNow)
|
||||
|
||||
// Runner — the swapped-per-truth-row subprocess seam.
|
||||
var Runner CommandRunner = defaultRunner
|
||||
|
||||
// ClockNow — the swapped-per-truth-row clock seam.
|
||||
var ClockNow Clock = defaultClock
|
||||
|
||||
func warmRouterPath() string { return filepath.Join(ScriptsDir(), "warm_router.py") }
|
||||
func memoryStorePath() string {
|
||||
return filepath.Join(ScriptsDir(), "memory_store.py")
|
||||
}
|
||||
func contextFilePath() string {
|
||||
return filepath.Join(MemoryDir(), "index", "auto_context.json")
|
||||
}
|
||||
func tasksFilePath() string {
|
||||
return filepath.Join(MemoryDir(), "tasks", "persisted.json")
|
||||
}
|
||||
|
||||
// isoformat — datetime.isoformat() on an aware UTC datetime: fractional
|
||||
// part only when microseconds are nonzero, offset always "+00:00" shape.
|
||||
func isoformat(t time.Time) string {
|
||||
base := t.Format("2006-01-02T15:04:05")
|
||||
if us := t.Nanosecond() / 1000; us != 0 {
|
||||
base += fmt.Sprintf(".%06d", us)
|
||||
}
|
||||
return base + t.Format("-07:00")
|
||||
}
|
||||
|
||||
// ── Python value semantics helpers ───────────────────────────────────────
|
||||
|
||||
// pyTruthy — Python truthiness for JSON-decoded values: None, False, 0,
|
||||
// "" and every empty container are falsy.
|
||||
func pyTruthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
case string:
|
||||
return t != ""
|
||||
case float64:
|
||||
return t != 0
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f != 0
|
||||
case []any:
|
||||
return len(t) > 0
|
||||
case []*OrderedMap:
|
||||
return len(t) > 0
|
||||
case *OrderedMap:
|
||||
return t != nil && t.Len() > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// pyTypeName — Python type() name for JSON-decoded values (integral
|
||||
// floats decode as 'int', mirroring json.loads producing ints).
|
||||
func pyTypeName(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "NoneType"
|
||||
case bool:
|
||||
return "bool"
|
||||
case string:
|
||||
return "str"
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return "int"
|
||||
}
|
||||
return "float"
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
if f == float64(int64(f)) && !strings.ContainsAny(t.String(), ".eE") {
|
||||
return "int"
|
||||
}
|
||||
return "float"
|
||||
case []any, []*OrderedMap:
|
||||
return "list"
|
||||
case *OrderedMap:
|
||||
return "dict"
|
||||
default:
|
||||
return "object"
|
||||
}
|
||||
}
|
||||
|
||||
// pyIsAlpha — str.isalpha(): non-empty and every rune a Unicode letter.
|
||||
func pyIsAlpha(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if !unicode.IsLetter(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// runeSlice — Python's s[:n] (codepoint slice).
|
||||
func runeSlice(s string, n int) string {
|
||||
if utf8.RuneCountInString(s) <= n {
|
||||
return s
|
||||
}
|
||||
return string([]rune(s)[:n])
|
||||
}
|
||||
|
||||
func truncateRunes(s string, n int) string { return runeSlice(s, n) }
|
||||
|
||||
// pyParse — json.loads with order-preserving objects and json.Number
|
||||
// numerals (so ints survive byte-parity re-encoding).
|
||||
func pyParse(s string) (any, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(s))
|
||||
dec.UseNumber()
|
||||
return decodeJSONValue(dec)
|
||||
}
|
||||
|
||||
// pythonRepr — repr() of a JSON-decoded value (the handle_hook
|
||||
// str(data) fallback), Python quoting rules included.
|
||||
func pythonRepr(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case string:
|
||||
return pythonReprString(t)
|
||||
case json.Number:
|
||||
if !strings.ContainsAny(t.String(), ".eE") {
|
||||
return t.String()
|
||||
}
|
||||
f, _ := t.Float64()
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'g', -1, 64)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = pythonRepr(item)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case *OrderedMap:
|
||||
entries := t.Entries()
|
||||
parts := make([]string, 0, len(entries))
|
||||
for _, kv := range entries {
|
||||
parts = append(parts, pythonReprString(kv.Key)+": "+pythonRepr(kv.Value))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// pythonReprString — repr() of a str: single quotes preferred, double
|
||||
// quotes when the string contains ' but not ", backslash and quote
|
||||
// escapes, \n \r \t shortcuts.
|
||||
func pythonReprString(s string) string {
|
||||
escape := func(quote byte) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte(quote)
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\'':
|
||||
if quote == '\'' {
|
||||
sb.WriteString(`\'`)
|
||||
} else {
|
||||
sb.WriteByte('\'')
|
||||
}
|
||||
case '"':
|
||||
if quote == '"' {
|
||||
sb.WriteString(`\"`)
|
||||
} else {
|
||||
sb.WriteByte('"')
|
||||
}
|
||||
default:
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
sb.WriteByte(quote)
|
||||
return sb.String()
|
||||
}
|
||||
if strings.Contains(s, "'") && !strings.Contains(s, "\"") {
|
||||
return escape('"')
|
||||
}
|
||||
return escape('\'')
|
||||
}
|
||||
|
||||
// ── the port (1:1 with the oracle) ───────────────────────────────────────
|
||||
|
||||
// runWarmRouter — run_warm_router: route the message through the warm
|
||||
// router subprocess and return the fired-neurons payload; ANY failure
|
||||
// (nonzero exit, timeout, bad JSON, empty stdout) degrades to None.
|
||||
func runWarmRouter(message string) any {
|
||||
res := Runner.Run(PythonBin(), []string{warmRouterPath(), "route", message, "--json"}, 10*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMemoryRecall — run_memory_recall: recall relevant entries; a
|
||||
// non-list payload degrades to [], a list is capped at budget entries.
|
||||
func runMemoryRecall(query string, budget int) []any {
|
||||
res := Runner.Run(PythonBin(), []string{memoryStorePath(), "recall", query, "--json"}, 15*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
if list, ok := v.([]any); ok {
|
||||
if budget >= 0 && budget < len(list) {
|
||||
return list[:budget]
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
}
|
||||
return []any{}
|
||||
}
|
||||
|
||||
// runAlwaysDigest — run_always_digest: the always-on digest payload or
|
||||
// None on any failure.
|
||||
func runAlwaysDigest() any {
|
||||
res := Runner.Run(PythonBin(), []string{warmRouterPath(), "always", "--json"}, 10*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadActiveTasks — load_active_tasks: read the persistence ledger and
|
||||
// keep open/in_progress tasks; missing file, malformed JSON, a non-dict
|
||||
// ledger, a non-dict task entry — every failure degrades to [].
|
||||
func loadActiveTasks() []any {
|
||||
raw, err := os.ReadFile(tasksFilePath())
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
v, err := pyParse(string(raw))
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
ledger, ok := v.(*OrderedMap)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
tasks, ok := ledger.Get("tasks")
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
list, ok := tasks.([]any)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
active := []any{}
|
||||
for _, t := range list {
|
||||
tom, ok := t.(*OrderedMap)
|
||||
if !ok {
|
||||
return []any{} // t.get on a non-dict raises → the oracle's except
|
||||
}
|
||||
status, _ := tom.Get2("status").(string)
|
||||
if status == "open" || status == "in_progress" {
|
||||
active = append(active, tom)
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
// recallQueryWords — the oracle's key-term extraction: lowercase, split on
|
||||
// whitespace, keep words longer than 4 chars that are pure alphabetic and
|
||||
// not stopwords.
|
||||
func recallQueryWords(message string) []string {
|
||||
words := []string{}
|
||||
for _, w := range strings.Fields(strings.ToLower(message)) {
|
||||
if utf8.RuneCountInString(w) > 4 && pyIsAlpha(w) {
|
||||
if _, stop := stopwordSet[w]; !stop {
|
||||
words = append(words, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// buildAutoContext — build_auto_context: warm neurons, active tasks,
|
||||
// memory recall (substantive messages only), always-on digest.
|
||||
//
|
||||
// Phase B (language advantage): the oracle's four IO calls are mutually
|
||||
// independent (pure reads of external state), so they fan out to
|
||||
// goroutines — wall time is the slowest call, not the sum of four
|
||||
// subprocesses. Results are assembled in the oracle's exact field order,
|
||||
// so output is byte-identical to the serial Python semantics (the battery
|
||||
// replays the frozen truth against THIS parallel path).
|
||||
func buildAutoContext(message string) *OrderedMap {
|
||||
context := NewOrderedMap(
|
||||
KV{"timestamp", isoformat(ClockNow.Now().UTC())},
|
||||
KV{"message_preview", runeSlice(message, 200)},
|
||||
KV{"warm_neurons", nil},
|
||||
KV{"memory_recall", nil},
|
||||
KV{"always_digest", nil},
|
||||
KV{"active_tasks", nil},
|
||||
)
|
||||
|
||||
// cheap, CPU-only: key-term extraction (no IO) stays on the caller
|
||||
// goroutine so the fan-out carries the exact query the oracle computes
|
||||
query := ""
|
||||
if message != "" && utf8.RuneCountInString(strings.TrimSpace(message)) > 10 {
|
||||
words := recallQueryWords(message)
|
||||
if len(words) > 0 {
|
||||
n := 5
|
||||
if len(words) < n {
|
||||
n = len(words)
|
||||
}
|
||||
query = strings.Join(words[:n], " ")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
warm any
|
||||
digest any
|
||||
tasks []any
|
||||
recall []any
|
||||
)
|
||||
launch := func(f func()) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
launch(func() { warm = runWarmRouter(message) })
|
||||
launch(func() { tasks = loadActiveTasks() })
|
||||
if query != "" {
|
||||
launch(func() { recall = runMemoryRecall(query, 5) })
|
||||
}
|
||||
launch(func() { digest = runAlwaysDigest() })
|
||||
wg.Wait()
|
||||
|
||||
if pyTruthy(warm) {
|
||||
context.Set("warm_neurons", warm)
|
||||
}
|
||||
if len(tasks) > 0 {
|
||||
context.Set("active_tasks", tasks)
|
||||
}
|
||||
if len(recall) > 0 {
|
||||
context.Set("memory_recall", recall)
|
||||
}
|
||||
if pyTruthy(digest) {
|
||||
context.Set("always_digest", digest)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
// saveContext — save_context: atomic write (tmp file + rename) of the
|
||||
// context JSON, indent=2, ensure_ascii.
|
||||
//
|
||||
// Phase B: the tmp file is per-writer (os.CreateTemp in the target dir),
|
||||
// not the oracle's fixed CONTEXT_FILE+".tmp" — the rename semantics are
|
||||
// identical, but concurrent hooks no longer clobber each other's tmp
|
||||
// (the oracle's shared-tmp path races ENOENT under concurrency).
|
||||
func saveContext(context *OrderedMap) error {
|
||||
dir := filepath.Dir(contextFilePath())
|
||||
if err := os.MkdirAll(dir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(dir, ".auto_context-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := tmp.Name()
|
||||
defer os.Remove(name) // no-op after a successful rename
|
||||
if _, err := tmp.Write([]byte(encodePythonJSON(context, 0))); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(name, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, contextFilePath())
|
||||
}
|
||||
|
||||
// handleHook — handle_hook: the on_message plugin hook. The truthiness
|
||||
// guard, the message/text/str(data) fallback chain and the two nested
|
||||
// count expressions reproduce the oracle exactly, including its
|
||||
// best-effort error dicts (truncated to 120 runes).
|
||||
func handleHook(event string, data *OrderedMap) *OrderedMap {
|
||||
errorDict := func(msg string) *OrderedMap {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "error"},
|
||||
KV{"event", event},
|
||||
KV{"error", truncateRunes(msg, 120)},
|
||||
)
|
||||
}
|
||||
if event == "on_message" && pyTruthy(data) {
|
||||
message := any("")
|
||||
for _, key := range []string{"message", "text"} {
|
||||
v, ok := data.Get(key)
|
||||
if !ok {
|
||||
v = ""
|
||||
}
|
||||
if pyTruthy(v) {
|
||||
message = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if !pyTruthy(message) {
|
||||
message = pythonRepr(data)
|
||||
}
|
||||
msgStr, isStr := message.(string)
|
||||
if !isStr {
|
||||
return errorDict(fmt.Sprintf("'%s' object has no attribute 'strip'", pyTypeName(message)))
|
||||
}
|
||||
if msgStr != "" && utf8.RuneCountInString(strings.TrimSpace(msgStr)) > 2 {
|
||||
context := buildAutoContext(msgStr)
|
||||
if err := saveContext(context); err != nil {
|
||||
return errorDict(err.Error())
|
||||
}
|
||||
neurons := context.Get2("warm_neurons")
|
||||
recall := context.Get2("memory_recall")
|
||||
tasks := context.Get2("active_tasks")
|
||||
neuronsFired := 0
|
||||
if pyTruthy(neurons) {
|
||||
om, ok := neurons.(*OrderedMap)
|
||||
if !ok {
|
||||
return errorDict(fmt.Sprintf("'%s' object has no attribute 'get'", pyTypeName(neurons)))
|
||||
}
|
||||
fired, _ := om.Get("fired")
|
||||
// .get("fired", []) — a MISSING key defaults to [], a
|
||||
// present-but-None key does not.
|
||||
if fired == nil {
|
||||
if _, present := om.Get("fired"); !present {
|
||||
fired = []any{}
|
||||
}
|
||||
}
|
||||
n, err := pyLen(fired)
|
||||
if err != nil {
|
||||
return errorDict(err.Error())
|
||||
}
|
||||
neuronsFired = n
|
||||
}
|
||||
memoriesRecalled := 0
|
||||
if pyTruthy(recall) {
|
||||
if n, err := pyLen(recall); err != nil {
|
||||
return errorDict(err.Error())
|
||||
} else {
|
||||
memoriesRecalled = n
|
||||
}
|
||||
}
|
||||
activeTasks := 0
|
||||
if pyTruthy(tasks) {
|
||||
if n, err := pyLen(tasks); err != nil {
|
||||
return errorDict(err.Error())
|
||||
} else {
|
||||
activeTasks = n
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_context"},
|
||||
KV{"neurons_fired", neuronsFired},
|
||||
KV{"memories_recalled", memoriesRecalled},
|
||||
KV{"active_tasks", activeTasks},
|
||||
KV{"context_file", contextFilePath()},
|
||||
)
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_context"},
|
||||
)
|
||||
}
|
||||
|
||||
// pyLen — len() for JSON-decoded values; non-sized types raise TypeError
|
||||
// in Python ("object of type 'int' has no len()").
|
||||
func pyLen(v any) (int, error) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return 0, fmt.Errorf("object of type 'NoneType' has no len()")
|
||||
case string:
|
||||
return utf8.RuneCountInString(t), nil
|
||||
case []any:
|
||||
return len(t), nil
|
||||
case []*OrderedMap:
|
||||
return len(t), nil
|
||||
case *OrderedMap:
|
||||
return t.Len(), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("object of type '%s' has no len()", pyTypeName(v))
|
||||
}
|
||||
}
|
||||
|
||||
// pluginManifest — the module-level PLUGIN dict (microkernel discovery
|
||||
// metadata), key order preserved.
|
||||
func pluginManifest() *OrderedMap {
|
||||
return NewOrderedMap(
|
||||
KV{"name", "auto_context"},
|
||||
KV{"version", "1.0.0"},
|
||||
KV{"description", "Auto-context — automatic memory and warm neuron activation on every message."},
|
||||
KV{"capabilities", []any{"context", "memory", "activation"}},
|
||||
KV{"hooks", NewOrderedMap(KV{"on_message", "handle_hook"})},
|
||||
KV{"dependencies", []any{"memory_store", "warm_router"}},
|
||||
KV{"provides", []any{"auto_context"}},
|
||||
KV{"priority", 5},
|
||||
KV{"auto_activate", true},
|
||||
)
|
||||
}
|
||||
|
||||
// ── CLI (auto_context.py main(), byte-parity stdout) ─────────────────────
|
||||
|
||||
func argparseUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage: auto_context.py [-h] [--event EVENT] [--data DATA] [--json] {build,show,hook} [arg]")
|
||||
}
|
||||
|
||||
// RunCLI executes auto_context.py main() against argv (program name at
|
||||
// index 0) and returns the process exit code. argparse failures print
|
||||
// usage to stderr and exit 2.
|
||||
func RunCLI(argv []string, stdout io.Writer) int {
|
||||
stderr := os.Stderr
|
||||
var positionals []string
|
||||
event, hasEvent := "", false
|
||||
dataStr, hasData := "", false
|
||||
jsonFlag := false
|
||||
i := 1
|
||||
for i < len(argv) {
|
||||
a := argv[i]
|
||||
switch {
|
||||
case a == "--json":
|
||||
jsonFlag = true
|
||||
case a == "--event":
|
||||
i++
|
||||
if i >= len(argv) {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument --event: expected one argument\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
event, hasEvent = argv[i], true
|
||||
case strings.HasPrefix(a, "--event="):
|
||||
event, hasEvent = strings.TrimPrefix(a, "--event="), true
|
||||
case a == "--data":
|
||||
i++
|
||||
if i >= len(argv) {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument --data: expected one argument\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
dataStr, hasData = argv[i], true
|
||||
case strings.HasPrefix(a, "--data="):
|
||||
dataStr, hasData = strings.TrimPrefix(a, "--data="), true
|
||||
case a == "-h" || a == "--help":
|
||||
argparseUsage(stdout)
|
||||
return 0
|
||||
case strings.HasPrefix(a, "-") && a != "-":
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: unrecognized arguments: %s\n", a)
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
default:
|
||||
positionals = append(positionals, a)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: the following arguments are required: cmd\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
if len(positionals) > 2 {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: unrecognized arguments: %s\n", strings.Join(positionals[2:], " "))
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
cmd, arg := positionals[0], ""
|
||||
if len(positionals) == 2 {
|
||||
arg = positionals[1]
|
||||
}
|
||||
switch cmd {
|
||||
case "build":
|
||||
message := arg
|
||||
if message == "" {
|
||||
message = "general context"
|
||||
}
|
||||
context := buildAutoContext(message)
|
||||
if err := saveContext(context); err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(context, 0))
|
||||
} else {
|
||||
n := 0
|
||||
if warm := context.Get2("warm_neurons"); pyTruthy(warm) {
|
||||
if om, ok := warm.(*OrderedMap); ok {
|
||||
fired, _ := om.Get("fired")
|
||||
if fired == nil {
|
||||
if _, present := om.Get("fired"); !present {
|
||||
fired = []any{}
|
||||
}
|
||||
}
|
||||
if c, err := pyLen(fired); err == nil {
|
||||
n = c
|
||||
}
|
||||
}
|
||||
}
|
||||
m := 0
|
||||
if recall, ok := context.Get2("memory_recall").([]any); ok {
|
||||
m = len(recall)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Auto-context built: %d neurons, %d memories\n", n, m)
|
||||
}
|
||||
|
||||
case "show":
|
||||
raw, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
fmt.Fprintln(stdout, "No auto-context file found")
|
||||
return 0
|
||||
}
|
||||
v, err := pyParse(string(raw))
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintln(stdout, encodePythonJSON(v, 0))
|
||||
|
||||
case "hook":
|
||||
event2 := "unknown"
|
||||
if hasEvent && event != "" {
|
||||
event2 = event
|
||||
}
|
||||
var data *OrderedMap
|
||||
if hasData && dataStr != "" {
|
||||
v, err := pyParse(dataStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if om, ok := v.(*OrderedMap); ok {
|
||||
data = om
|
||||
} else {
|
||||
// json.loads gave a non-object; data.get would raise
|
||||
// AttributeError outside handle_hook's try
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): '%s' object has no attribute 'get'\n", pyTypeName(v))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
result := handleHook(event2, data)
|
||||
fmt.Fprintln(stdout, encodePythonJSONCompact(result))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument cmd: invalid choice: '%s' (choose from 'build', 'show', 'hook')\n", cmd)
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ── entry point: CLI + truth replay subcommand for the HARD CI gate ─────
|
||||
|
||||
func truthRowCount(path string) int {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Fprintln(os.Stderr, f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d truth rows exact-equal vs the Python oracle\n", truthRowCount(path))
|
||||
return
|
||||
}
|
||||
os.Exit(RunCLI(os.Args, os.Stdout))
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
// Phase B battery — pins the language-advantage guarantees: the subprocess
|
||||
// and clock seams are interfaces (compile-time asserted in main.go),
|
||||
// build_auto_context fans the oracle's four independent IO calls out to
|
||||
// goroutines with byte-identical results, and concurrent builds are
|
||||
// race-clean (run under -race in CI).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// countingRunner — a CommandRunner decorator that counts invocations: the
|
||||
// seam swaps in without touching port code.
|
||||
type countingRunner struct {
|
||||
mu sync.Mutex
|
||||
inner CommandRunner
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *countingRunner) Run(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
return c.inner.Run(exe, args, timeout)
|
||||
}
|
||||
|
||||
// swapSeams — install the frozen seams for one test; restore on cleanup.
|
||||
func swapSeams(t *testing.T, routeMode, alwaysMode, recallMode string) string {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
memdir := filepath.Join(home, "memory")
|
||||
if err := os.MkdirAll(memdir, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
MemoryDir = func() string { return memdir }
|
||||
ScriptsDir = func() string { return "/tmp/actx_stubs" }
|
||||
PythonBin = func() string { return "python3" }
|
||||
Runner = buildFakeRunner(routeMode, alwaysMode, recallMode)
|
||||
base, _ := time.Parse(time.RFC3339, "2026-10-11T12:00:00.123456+00:00")
|
||||
ClockNow = ClockFunc(func() time.Time { return base })
|
||||
t.Cleanup(func() {
|
||||
MemoryDir = defaultMemoryDir
|
||||
ScriptsDir = defaultScriptsDir
|
||||
PythonBin = defaultPythonBin
|
||||
Runner = defaultRunner
|
||||
ClockNow = defaultClock
|
||||
})
|
||||
return memdir
|
||||
}
|
||||
|
||||
// TestPhaseBParallelBuildParity — the frozen build truth replays green
|
||||
// against the GOROUTINE fan-out path (results assembled in oracle order
|
||||
// are byte-identical to the serial Python semantics).
|
||||
func TestPhaseBParallelBuildParity(t *testing.T) {
|
||||
swapSeams(t, "ok", "ok", "ok")
|
||||
doc := loadTruth(t)
|
||||
for _, name := range []string{"build:full", "build:unicode-preview", "build:stopwords", "build:short-message"} {
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("parallel build diverged on %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBRunnerSeamIsAnInterface — a counting decorator rides the
|
||||
// CommandRunner seam; a full build issues exactly 3 subprocess runs (route,
|
||||
// recall, always — the ledger read is file IO, not a subprocess).
|
||||
func TestPhaseBRunnerSeamIsAnInterface(t *testing.T) {
|
||||
swapSeams(t, "ok", "ok", "ok")
|
||||
counter := &countingRunner{inner: Runner}
|
||||
Runner = counter
|
||||
ctx := buildAutoContext("remember the config parser override discussion tests")
|
||||
if counter.count != 3 {
|
||||
t.Fatalf("expected 3 subprocess runs per build, got %d", counter.count)
|
||||
}
|
||||
if warm := ctx.Get2("warm_neurons"); !pyTruthy(warm) {
|
||||
t.Fatal("warm payload lost through the decorated seam")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBConcurrentBuilds — 8 goroutines building contexts through the
|
||||
// shared fake runner must all agree (and stay race-clean).
|
||||
func TestPhaseBConcurrentBuilds(t *testing.T) {
|
||||
swapSeams(t, "ok", "ok", "ok")
|
||||
const n = 8
|
||||
results := make([]string, n)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
results[i] = mustJSON(buildAutoContext("remember the config parser override discussion tests"))
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 1; i < n; i++ {
|
||||
if results[i] != results[0] {
|
||||
t.Fatalf("concurrent build %d diverged:\n first: %s\n got: %s", i, results[0], results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBConcurrentHooks — concurrent handle_hook calls write the
|
||||
// context file through per-writer tmp + atomic rename: every rename
|
||||
// succeeds (no shared-tmp ENOENT race, which the oracle's fixed
|
||||
// CONTEXT_FILE+".tmp" would hit) and every reader sees complete JSON.
|
||||
func TestPhaseBConcurrentHooks(t *testing.T) {
|
||||
memdir := swapSeams(t, "ok", "ok", "ok")
|
||||
data := NewOrderedMap(KV{"message", "remember the config parser override discussion tests"})
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if r := handleHook("on_message", data); r.Get2("status") != "ok" {
|
||||
t.Errorf("hook failed: %v", mustJSON(r))
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
raw, err := os.ReadFile(filepath.Join(memdir, "index", "auto_context.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pyParse(string(raw)); err != nil {
|
||||
t.Fatalf("context file torn or invalid: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent=2 and the default compact (", ", ": ") separators,
|
||||
// ensure_ascii=True) so save_context, show and hook CLI output are
|
||||
// byte-parity with the oracle (adapted from the message_queue port's
|
||||
// pyjson.go; the float renderer follows Python repr, which keeps ".0" on
|
||||
// integral floats).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// KV — one ordered key/value pair.
|
||||
type KV struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
// OrderedMap — a dict with preserved insertion order.
|
||||
type OrderedMap struct {
|
||||
Keys []string
|
||||
Vals map[string]any
|
||||
}
|
||||
|
||||
// NewOrderedMap builds an ordered map from pairs.
|
||||
func NewOrderedMap(pairs ...KV) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for _, kv := range pairs {
|
||||
om.Set(kv.Key, kv.Value)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// OrderedMapFromMap wraps a plain map (order unspecified — canonical
|
||||
// comparisons sort keys anyway).
|
||||
func OrderedMapFromMap(m map[string]any) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// Set inserts or replaces, preserving first-insertion order.
|
||||
func (o *OrderedMap) Set(key string, value any) {
|
||||
if _, ok := o.Vals[key]; !ok {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.Vals[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a value.
|
||||
func (o *OrderedMap) Get(key string) (any, bool) {
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Get2 returns the value at key (nil when absent) for direct type asserts.
|
||||
func (o *OrderedMap) Get2(key string) any {
|
||||
return o.Vals[key]
|
||||
}
|
||||
|
||||
// Entries returns key/value pairs in insertion order.
|
||||
func (o *OrderedMap) Entries() []KV {
|
||||
out := make([]KV, 0, len(o.Keys))
|
||||
for _, k := range o.Keys {
|
||||
out = append(out, KV{k, o.Vals[k]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of entries.
|
||||
func (o *OrderedMap) Len() int { return len(o.Keys) }
|
||||
|
||||
// decodeJSONValue decodes one JSON value from the decoder, decoding objects
|
||||
// as *OrderedMap (order-preserving) and numbers as float64.
|
||||
func decodeJSONValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeFromToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeFromToken(dec *json.Decoder, tok json.Token) (any, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
om.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume '}'
|
||||
return nil, err
|
||||
}
|
||||
return om, nil
|
||||
case '[':
|
||||
arr := []any{}
|
||||
for dec.More() {
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume ']'
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
default:
|
||||
return tok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSON renders v exactly like Python json.dumps(v, indent=N)
|
||||
// with default ensure_ascii=True and (',', ': ') separators.
|
||||
func encodePythonJSON(v any, indent int) string {
|
||||
var sb strings.Builder
|
||||
encodeValue(&sb, v, indent)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeValue(sb *strings.Builder, v any, indent int) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
if t.Len() == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
writeIndent(sb, indent+1)
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, indent+1)
|
||||
if i < len(entries)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeValue(sb, OrderedMapFromMap(t), indent)
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func writeIndent(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSONCompact renders v exactly like Python json.dumps(v) with
|
||||
// default separators (", ", ": ") and ensure_ascii=True — the compact form
|
||||
// the CLI hook path prints.
|
||||
func encodePythonJSONCompact(v any) string {
|
||||
var sb strings.Builder
|
||||
encodeCompactValue(&sb, v)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeCompactValue(sb *strings.Builder, v any) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
sb.WriteString("{")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeCompactValue(sb, kv.Value)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeCompactValue(sb, OrderedMapFromMap(t))
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr (json.dumps uses
|
||||
// float.__repr__): shortest round-trip digits, and integral floats keep
|
||||
// one decimal place (1.0 stays "1.0", not "1").
|
||||
func pythonFloat(f float64) string {
|
||||
s := strconv.FormatFloat(f, 'g', -1, 64)
|
||||
if !strings.ContainsAny(s, ".eE") &&
|
||||
!strings.Contains(s, "Inf") && !strings.Contains(s, "NaN") {
|
||||
s += ".0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// pythonEscapeString — Python json.dumps string escaping with
|
||||
// ensure_ascii=True: control chars via \uXXXX (with \b \f \n \r \t
|
||||
// shortcuts), non-ASCII via \uXXXX / surrogate pairs, lowercase hex.
|
||||
func pythonEscapeString(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
sb.WriteString(`\"`)
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\b':
|
||||
sb.WriteString(`\b`)
|
||||
case '\f':
|
||||
sb.WriteString(`\f`)
|
||||
default:
|
||||
if r < 0x20 || r > 0x7e {
|
||||
if r > 0xffff {
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
fmt.Fprintf(&sb, `\u%04x\u%04x`, r1, r2)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, `\u%04x`, r)
|
||||
}
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// /tmp/gen_auto_context_truth.py against the live Python oracle with the
|
||||
// clock frozen and env-driven stub scripts pinned) and re-executes every
|
||||
// row through this Go port, comparing results. Exact rows (context-file
|
||||
// bytes, CLI stdout) compare byte-for-byte; everything else compares
|
||||
// canonically (sorted-key JSON). Absolute memory-dir paths are normalized
|
||||
// to @MEMDIR@ on both sides.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
Oracle string `json:"oracle"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on 100%).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(&doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
// compareOne replays one row and returns "" on parity, else a divergence
|
||||
// description. Shared by RunTruth and the per-function battery tests.
|
||||
func compareOne(doc *TruthDoc, row *TruthRow) string {
|
||||
got, exact, err := execRow(doc, row)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: %v", err)
|
||||
}
|
||||
var wantStr, gotStr string
|
||||
if exact {
|
||||
var w string
|
||||
if err := json.Unmarshal(row.Want, &w); err != nil {
|
||||
return fmt.Sprintf("want not a string: %v", err)
|
||||
}
|
||||
wantStr, gotStr = w, got.(string)
|
||||
} else {
|
||||
wantStr, err = canonicalFromRaw(row.Want)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize want: %v", err)
|
||||
}
|
||||
gotStr, err = canonicalFromRaw([]byte(mustJSON(got)))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize got: %v", err)
|
||||
}
|
||||
}
|
||||
if wantStr != gotStr {
|
||||
return fmt.Sprintf("\n want: %s\n got: %s", wantStr, gotStr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// execRow executes one truth row: returns got (any) and, for exact rows,
|
||||
// the exact string in got.
|
||||
func execRow(doc *TruthDoc, row *TruthRow) (got any, exact bool, err error) {
|
||||
setup := NewOrderedMap()
|
||||
if len(row.Setup) > 0 {
|
||||
dec := json.NewDecoder(bytes.NewReader(row.Setup))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return nil, false, fmt.Errorf("setup not an object")
|
||||
}
|
||||
v, err := decodeFromToken(dec, tok)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
setup = v.(*OrderedMap)
|
||||
}
|
||||
|
||||
// freeze the seams per row: fresh temp memory dir, frozen clock,
|
||||
// env-driven fake runner mirroring /tmp/actx_stubs payloads
|
||||
home, err := os.MkdirTemp("", "actx_truth_row_")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer os.RemoveAll(home)
|
||||
|
||||
defer func() {
|
||||
MemoryDir = defaultMemoryDir
|
||||
Runner = defaultRunner
|
||||
ClockNow = defaultClock
|
||||
PythonBin = defaultPythonBin
|
||||
ScriptsDir = defaultScriptsDir
|
||||
}()
|
||||
memdir := filepath.Join(home, "memory")
|
||||
if err := os.MkdirAll(memdir, 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
MemoryDir = func() string { return memdir }
|
||||
ScriptsDir = func() string { return "/tmp/actx_stubs" }
|
||||
PythonBin = func() string { return "python3" }
|
||||
base, err := time.Parse(time.RFC3339, doc.FrozenTS)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
ClockNow = ClockFunc(func() time.Time { return base })
|
||||
|
||||
// file seeds from setup
|
||||
if f, ok := setup.Get("tasks_file"); ok {
|
||||
s, _ := f.(string)
|
||||
if err := os.MkdirAll(filepath.Join(memdir, "tasks"), 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memdir, "tasks", "persisted.json"), []byte(s), 0o644); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
if f, ok := setup.Get("seed_context"); ok {
|
||||
s, _ := f.(string)
|
||||
if err := os.MkdirAll(filepath.Join(memdir, "index"), 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memdir, "index", "auto_context.json"), []byte(s), 0o644); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// stub modes (default "ok", like the stub scripts' env defaults)
|
||||
mode := func(key string) string {
|
||||
if v, ok := setup.Get(key); ok {
|
||||
if s, ok2 := v.(string); ok2 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
Runner = buildFakeRunner(mode("stub_route"), mode("stub_always"), mode("stub_recall"))
|
||||
|
||||
scrub := func(v any) any {
|
||||
return scrubPaths(v, memdir)
|
||||
}
|
||||
|
||||
strSetup := func(key string) string {
|
||||
s, _ := setup.Get2(key).(string)
|
||||
return s
|
||||
}
|
||||
|
||||
switch row.Op {
|
||||
case "warm_router":
|
||||
return scrub(runWarmRouter(strSetup("message"))), false, nil
|
||||
|
||||
case "recall":
|
||||
budget := 5
|
||||
if b, ok := setup.Get("budget"); ok {
|
||||
switch n := b.(type) {
|
||||
case float64:
|
||||
budget = int(n)
|
||||
case json.Number:
|
||||
f, _ := n.Float64()
|
||||
budget = int(f)
|
||||
}
|
||||
}
|
||||
return scrub(runMemoryRecall(strSetup("query"), budget)), false, nil
|
||||
|
||||
case "always":
|
||||
return scrub(runAlwaysDigest()), false, nil
|
||||
|
||||
case "load_tasks":
|
||||
return scrub(loadActiveTasks()), false, nil
|
||||
|
||||
case "build":
|
||||
return scrub(buildAutoContext(strSetup("message"))), false, nil
|
||||
|
||||
case "save":
|
||||
om, ok := setup.Get2("context").(*OrderedMap)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("save: no context")
|
||||
}
|
||||
if err := saveContext(om); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reads := []any{scrubPaths(string(raw), memdir)}
|
||||
if om2, ok2 := setup.Get2("context2").(*OrderedMap); ok2 {
|
||||
// overwrite semantics: second save replaces the first atomically
|
||||
if err := saveContext(om2); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw2, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reads = append(reads, scrubPaths(string(raw2), memdir))
|
||||
}
|
||||
_, statErr := os.Stat(contextFilePath() + ".tmp")
|
||||
return NewOrderedMap(
|
||||
KV{"reads", reads},
|
||||
KV{"tmp_gone", os.IsNotExist(statErr)},
|
||||
), false, nil
|
||||
|
||||
case "hook":
|
||||
event := strSetup("event")
|
||||
var data *OrderedMap
|
||||
if v, ok := setup.Get("data"); ok && v != nil {
|
||||
om, ok2 := v.(*OrderedMap)
|
||||
if !ok2 {
|
||||
return nil, false, fmt.Errorf("hook: data not an object")
|
||||
}
|
||||
data = om
|
||||
}
|
||||
return scrub(handleHook(event, data)), false, nil
|
||||
|
||||
case "cli":
|
||||
argvAny, _ := setup.Get2("argv").([]any)
|
||||
argv := []string{}
|
||||
for _, a := range argvAny {
|
||||
s, _ := a.(string)
|
||||
argv = append(argv, s)
|
||||
}
|
||||
var sb strings.Builder
|
||||
code := RunCLI(argv, &sb)
|
||||
out := strings.ReplaceAll(sb.String(), memdir, "@MEMDIR@")
|
||||
return NewOrderedMap(KV{"stdout", out}, KV{"exit", code}), false, nil
|
||||
|
||||
case "plugin":
|
||||
return pluginManifest(), false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("unknown row op %s (%s)", row.Op, row.Name)
|
||||
}
|
||||
|
||||
// buildFakeRunner — the env-driven stub scripts' behavior as a
|
||||
// CommandRunner (mirrors /tmp/actx_stubs byte-for-byte): payloads ECHO the
|
||||
// caller's message/query so the frozen truth pins the exact strings the
|
||||
// port hands to the subprocess seam. Reused by the Phase B battery.
|
||||
func buildFakeRunner(routeMode, alwaysMode, recallMode string) RunnerFunc {
|
||||
return func(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
var out string
|
||||
code := 0
|
||||
base := filepath.Base(args[0])
|
||||
if base == "warm_router.py" {
|
||||
if len(args) > 1 && args[1] == "always" {
|
||||
switch alwaysMode {
|
||||
case "ok":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"digest", "daily-summary"}, KV{"items", 7},
|
||||
KV{"topics", []any{"memory", "tasks", "recall"}}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "not-json{{{"
|
||||
case "empty":
|
||||
// print nothing
|
||||
}
|
||||
} else {
|
||||
msg := ""
|
||||
if len(args) > 2 {
|
||||
msg = args[2]
|
||||
}
|
||||
switch routeMode {
|
||||
case "ok":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"echo_message", msg},
|
||||
KV{"fired", []any{"warm-alpha", "warm-beta", "warm-gamma"}},
|
||||
KV{"matched", 3}, KV{"route", "focus"}))
|
||||
case "list":
|
||||
out = "[1, 2]"
|
||||
case "emptylist":
|
||||
out = "[]"
|
||||
case "firedint":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"fired", 3}, KV{"echo_message", msg}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "not-json{{{"
|
||||
case "empty":
|
||||
// print nothing
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := ""
|
||||
if len(args) > 2 {
|
||||
query = args[2]
|
||||
}
|
||||
switch recallMode {
|
||||
case "ok":
|
||||
items := []any{NewOrderedMap(KV{"echo_query", query})}
|
||||
for _, e := range recallEntries {
|
||||
items = append(items, e)
|
||||
}
|
||||
out = encodePythonJSONCompact(items)
|
||||
case "nonlist":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"query", query}, KV{"note", "not a list"}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "<xml?>"
|
||||
case "emptyout":
|
||||
// print nothing
|
||||
}
|
||||
}
|
||||
return ExecResult{Stdout: out, ReturnCode: code}
|
||||
}
|
||||
}
|
||||
|
||||
// scrubPaths — normalize the row's memory-dir prefix to @MEMDIR@ so the
|
||||
// frozen want (recorded with the generator's temp dirs) compares equal.
|
||||
func scrubPaths(v any, memdir string) any {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.ReplaceAll(t, memdir, "@MEMDIR@")
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = scrubPaths(item, memdir)
|
||||
}
|
||||
return out
|
||||
case *OrderedMap:
|
||||
out := NewOrderedMap()
|
||||
for _, kv := range t.Entries() {
|
||||
out.Set(kv.Key, scrubPaths(kv.Value, memdir))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// recallEntries — the memory_store.py stub's fixed entry list (m2's title
|
||||
// pins ensure_ascii round-tripping).
|
||||
var recallEntries = []any{
|
||||
NewOrderedMap(KV{"id", "m1"}, KV{"score", 0.91},
|
||||
KV{"tags", []any{"alpha", "beta"}}, KV{"title", "memory-entry-one"}),
|
||||
NewOrderedMap(KV{"id", "m2"}, KV{"score", 0.84},
|
||||
KV{"tags", []any{}}, KV{"title", "mémoire—café"}),
|
||||
NewOrderedMap(KV{"id", "m3"}, KV{"score", 0.77},
|
||||
KV{"tags", []any{"gamma"}}, KV{"title", "memory-entry-three"}),
|
||||
NewOrderedMap(KV{"id", "m4"}, KV{"score", 0.7},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-four"}),
|
||||
NewOrderedMap(KV{"id", "m5"}, KV{"score", 0.62},
|
||||
KV{"tags", []any{"delta"}}, KV{"title", "memory-entry-five"}),
|
||||
NewOrderedMap(KV{"id", "m6"}, KV{"score", 0.55},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-six"}),
|
||||
NewOrderedMap(KV{"id", "m7"}, KV{"score", 0.41},
|
||||
KV{"tags", []any{"epsilon"}}, KV{"title", "memory-entry-seven"}),
|
||||
NewOrderedMap(KV{"id", "m8"}, KV{"score", 0.2},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-eight"}),
|
||||
}
|
||||
|
||||
// canonicalFromRaw — canonical string of a raw JSON document.
|
||||
func canonicalFromRaw(raw []byte) (string, error) {
|
||||
var v any
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonical(normalize(v)), nil
|
||||
}
|
||||
|
||||
// canonical — sorted-key deterministic rendering of a decoded JSON value.
|
||||
func canonical(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case string:
|
||||
return pythonEscapeString(t)
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return canonical(f)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = canonical(normalize(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, pythonEscapeString(k)+": "+canonical(normalize(t[k])))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = kv.Value
|
||||
}
|
||||
return canonical(m)
|
||||
default:
|
||||
return fmt.Sprintf("«%v»", t)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize — convert OrderedMap/map trees into plain map[string]any with
|
||||
// json.Number→float64 so canonical() sees one representation.
|
||||
func normalize(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = normalize(kv.Value)
|
||||
}
|
||||
return m
|
||||
case map[string]any:
|
||||
m := map[string]any{}
|
||||
for k, val := range t {
|
||||
m[k] = normalize(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = normalize(item)
|
||||
}
|
||||
return out
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(normalize(v))
|
||||
if err != nil {
|
||||
return "null"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,264 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate plus per-function batteries replaying the
|
||||
// frozen oracle truth (truth.json, generated against the LIVE Python
|
||||
// oracle with the clock frozen and env-driven stub scripts pinned)
|
||||
// VERBATIM. Any divergence fails; the truth file rides the repo so the
|
||||
// gate holds in CI without /tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) *TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth.json: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth.json contained no rows")
|
||||
}
|
||||
return &doc
|
||||
}
|
||||
|
||||
func findRow(t *testing.T, name string) TruthRow {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
for _, row := range doc.Rows {
|
||||
if row.Name == name {
|
||||
return row
|
||||
}
|
||||
}
|
||||
t.Fatalf("frozen row %q missing from truth.json — battery/truth drift", name)
|
||||
return TruthRow{}
|
||||
}
|
||||
|
||||
func mustReplay(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func replayOp(t *testing.T, op string) int {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
n := 0
|
||||
for _, row := range doc.Rows {
|
||||
if row.Op != op {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("no %s rows in truth.json", op)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// rowWant decodes a row's want JSON into a generic map.
|
||||
func rowWant(t *testing.T, name string) map[string]any {
|
||||
t.Helper()
|
||||
row := findRow(t, name)
|
||||
var want map[string]any
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
t.Fatalf("decode want of %s: %v", name, err)
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// TestTruthParity — the whole frozen battery, one shot.
|
||||
func TestTruthParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
t.Fatalf("parity replay diverged after %d clean rows:\n%s",
|
||||
len(doc.Rows)-len(fails), strings.Join(fails, "\n"))
|
||||
}
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", len(doc.Rows))
|
||||
}
|
||||
|
||||
// TestBatteryRunWarmRouter — ok / nonzero-exit / bad-json / empty-stdout /
|
||||
// JSON-list payload / empty-list payload; the ok row pins the exact
|
||||
// message the port hands to the seam.
|
||||
func TestBatteryRunWarmRouter(t *testing.T) {
|
||||
if n := replayOp(t, "warm_router"); n != 6 {
|
||||
t.Fatalf("expected 6 frozen warm_router rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "warm:ok")
|
||||
if want["echo_message"] != "hello warm router" || want["matched"] != float64(3) {
|
||||
t.Fatalf("frozen echo row changed: %v", want)
|
||||
}
|
||||
if got := len(want["fired"].([]any)); got != 3 {
|
||||
t.Fatalf("fired payload changed: %v", want["fired"])
|
||||
}
|
||||
mustReplay(t, "warm:list")
|
||||
mustReplay(t, "warm:emptylist")
|
||||
}
|
||||
|
||||
// TestBatteryRunMemoryRecall — ok (echo pins the query string), budget
|
||||
// truncation, non-list payload, and every degrade branch.
|
||||
func TestBatteryRunMemoryRecall(t *testing.T) {
|
||||
if n := replayOp(t, "recall"); n != 6 {
|
||||
t.Fatalf("expected 6 frozen recall rows, got %d", n)
|
||||
}
|
||||
// recall:ok's want is the returned LIST — element 0 echoes the query
|
||||
rawWant := findRow(t, "recall:ok").Want
|
||||
var list []map[string]any
|
||||
if err := json.Unmarshal(rawWant, &list); err != nil {
|
||||
t.Fatalf("decode recall:ok want: %v", err)
|
||||
}
|
||||
if len(list) != 5 || list[0]["echo_query"] != "alpha beta gamma" {
|
||||
t.Fatalf("frozen recall:ok changed: %v", list)
|
||||
}
|
||||
mustReplay(t, "recall:budget2")
|
||||
mustReplay(t, "recall:nonlist")
|
||||
mustReplay(t, "recall:emptyout")
|
||||
}
|
||||
|
||||
// TestBatteryRunAlwaysDigest — ok / fail / bad-json / empty.
|
||||
func TestBatteryRunAlwaysDigest(t *testing.T) {
|
||||
if n := replayOp(t, "always"); n != 4 {
|
||||
t.Fatalf("expected 4 frozen always rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "always:ok")
|
||||
if want["digest"] != "daily-summary" || want["items"] != float64(7) {
|
||||
t.Fatalf("frozen digest changed: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryLoadActiveTasks — missing file / mixed statuses (only
|
||||
// open+in_progress survive) / malformed / empty / no-tasks-key.
|
||||
func TestBatteryLoadActiveTasks(t *testing.T) {
|
||||
if n := replayOp(t, "load_tasks"); n != 5 {
|
||||
t.Fatalf("expected 5 frozen load_tasks rows, got %d", n)
|
||||
}
|
||||
mustReplay(t, "tasks:missing")
|
||||
mustReplay(t, "tasks:malformed")
|
||||
}
|
||||
|
||||
// TestBatteryBuild — every build_auto_context branch: full context (the
|
||||
// recall echo pins stopword filtering + isalpha + first-5 join), short
|
||||
// message, stopwords-only, non-alpha, unicode 200-rune preview truncation,
|
||||
// empty message, recall-fail, warm-emptylist, warm-fail; frozen timestamp.
|
||||
func TestBatteryBuild(t *testing.T) {
|
||||
if n := replayOp(t, "build"); n != 9 {
|
||||
t.Fatalf("expected 9 frozen build rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "build:full")
|
||||
recallList, _ := want["memory_recall"].([]any)
|
||||
echo, _ := recallList[0].(map[string]any)
|
||||
if echo["echo_query"] != "remember config parser override discussion" {
|
||||
t.Fatalf("recall query extraction changed: %v", echo["echo_query"])
|
||||
}
|
||||
if want["timestamp"] != "2026-10-11T12:00:00.123456+00:00" {
|
||||
t.Fatalf("frozen timestamp changed: %v", want["timestamp"])
|
||||
}
|
||||
want = rowWant(t, "build:unicode-preview")
|
||||
preview, _ := want["message_preview"].(string)
|
||||
if n := len([]rune(preview)); n != 200 {
|
||||
t.Fatalf("preview must be 200 runes, got %d", n)
|
||||
}
|
||||
mustReplay(t, "build:short-message")
|
||||
mustReplay(t, "build:stopwords")
|
||||
mustReplay(t, "build:nonalpha")
|
||||
mustReplay(t, "build:empty")
|
||||
mustReplay(t, "build:recall-fail")
|
||||
mustReplay(t, "build:warm-emptylist")
|
||||
mustReplay(t, "build:warm-fail")
|
||||
}
|
||||
|
||||
// TestBatterySave — save_context byte-parity: ensure_ascii escapes,
|
||||
// indent=2 layout, and the tmp+rename overwrite (no .tmp leftover).
|
||||
func TestBatterySave(t *testing.T) {
|
||||
if n := replayOp(t, "save"); n != 2 {
|
||||
t.Fatalf("expected 2 frozen save rows, got %d", n)
|
||||
}
|
||||
mustReplay(t, "save:full")
|
||||
mustReplay(t, "save:overwrite")
|
||||
}
|
||||
|
||||
// TestBatteryHandleHook — full counts, text fallback, str(data) fallback
|
||||
// (Python dict repr!), short message, other event, no data, zero counts,
|
||||
// and both best-effort error dicts verbatim.
|
||||
func TestBatteryHandleHook(t *testing.T) {
|
||||
if n := replayOp(t, "hook"); n != 9 {
|
||||
t.Fatalf("expected 9 frozen hook rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "hook:full")
|
||||
if want["neurons_fired"] != float64(3) || want["memories_recalled"] != float64(5) ||
|
||||
want["active_tasks"] != float64(2) {
|
||||
t.Fatalf("frozen hook counts changed: %v", want)
|
||||
}
|
||||
if cf, _ := want["context_file"].(string); !strings.HasSuffix(cf, "@MEMDIR@/index/auto_context.json") {
|
||||
t.Fatalf("context_file normalization changed: %v", cf)
|
||||
}
|
||||
errWant := rowWant(t, "hook:warm-list-error")
|
||||
if errWant["error"] != "'list' object has no attribute 'get'" {
|
||||
t.Fatalf("list.get error message changed: %v", errWant["error"])
|
||||
}
|
||||
errWant = rowWant(t, "hook:fired-int-error")
|
||||
if errWant["error"] != "object of type 'int' has no len()" {
|
||||
t.Fatalf("len(int) error message changed: %v", errWant["error"])
|
||||
}
|
||||
mustReplay(t, "hook:text-fallback")
|
||||
mustReplay(t, "hook:str-fallback")
|
||||
mustReplay(t, "hook:short-message")
|
||||
mustReplay(t, "hook:other-event")
|
||||
mustReplay(t, "hook:no-data")
|
||||
mustReplay(t, "hook:counts-zero")
|
||||
}
|
||||
|
||||
// TestBatteryCLI — main() byte-parity stdout + exit codes: build (plain +
|
||||
// --json + default message), show (hit + missing), hook (event+data /
|
||||
// defaults / error dict), argparse exits.
|
||||
func TestBatteryCLI(t *testing.T) {
|
||||
if n := replayOp(t, "cli"); n != 10 {
|
||||
t.Fatalf("expected 10 frozen cli rows, got %d", n)
|
||||
}
|
||||
for name, wantExit := range map[string]float64{
|
||||
"cli:build": 0,
|
||||
"cli:build-json": 0,
|
||||
"cli:build-default": 0,
|
||||
"cli:show-missing": 0,
|
||||
"cli:show-hit": 0,
|
||||
"cli:hook": 0,
|
||||
"cli:hook-default": 0,
|
||||
"cli:hook-error": 0,
|
||||
"cli:bad-cmd": 2,
|
||||
"cli:no-cmd": 2,
|
||||
} {
|
||||
want := rowWant(t, name)
|
||||
if want["exit"] != wantExit {
|
||||
t.Fatalf("%s: frozen exit code %v != %v", name, want["exit"], wantExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryPlugin — the PLUGIN manifest row.
|
||||
func TestBatteryPlugin(t *testing.T) {
|
||||
mustReplay(t, "plugin")
|
||||
want := rowWant(t, "plugin")
|
||||
if want["name"] != "auto_context" || want["priority"] != float64(5) ||
|
||||
want["auto_activate"] != true {
|
||||
t.Fatalf("frozen PLUGIN manifest changed: %v", want)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.217.0"
|
||||
__version__ = "0.219.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.217.0"' in rune_version,
|
||||
'__version__ = "0.219.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust outcome_ledger core (dichotomy wave, #2479
|
||||
tranche). Mirrors to_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* ol_run({op, db_path, record?, task_family?, model?}) ->
|
||||
{"ok": true, "value": ...} | {"ok": false, "err": "..."}
|
||||
|
||||
The kernel is RUST-side per the outcome_ledger intent ledger — it owns the
|
||||
ledger SQL (DDL, INSERT, the six-aggregate stats query) against the system
|
||||
libsqlite3. The facade keeps makedirs, the CLI and test_ledger.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process) — this crate
|
||||
deliberately exports NO free symbol of its own. Override the .so with
|
||||
OL_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "outcome_ledger"
|
||||
_LIB_PATH = Path(os.environ.get("OL_RUST_LIB", _CRATE_DIR / "target" / "release" / "liboutcome_ledger.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class OutcomeLedgerCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust outcome_ledger core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise OutcomeLedgerCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.ol_run.argtypes = [ctypes.c_void_p]
|
||||
lib.ol_run.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.ol_run(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("ol_run returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "ol_run failed"))
|
||||
return doc
|
||||
|
||||
|
||||
def run_op(op: str, db_path: str, *, record: dict | None = None,
|
||||
task_family: str | None = None, model: str | None = None) -> dict:
|
||||
"""Run one kernel op. Returns the envelope value (stats doc, null, ...)."""
|
||||
payload: dict = {"op": op, "db_path": db_path}
|
||||
if record is not None:
|
||||
payload["record"] = record
|
||||
if task_family is not None:
|
||||
payload["task_family"] = task_family
|
||||
if model is not None:
|
||||
payload["model"] = model
|
||||
return _call(payload)["value"]
|
||||
@@ -71,7 +71,13 @@ class OutcomeLedger:
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
"""Initialize the database."""
|
||||
"""Initialize the database (Rust core when built; makedirs stay
|
||||
Python — the adapter owns directory prep, the kernel owns the DDL)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
core.run_op("init_db", self.db_path)
|
||||
return
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
@@ -99,7 +105,12 @@ class OutcomeLedger:
|
||||
conn.close()
|
||||
|
||||
def record(self, outcome: OutcomeRecord):
|
||||
"""Record an outcome."""
|
||||
"""Record an outcome (Rust core when built — the INSERT runs in the
|
||||
kernel against the system libsqlite3)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
core.run_op("record", self.db_path, record=outcome.to_dict())
|
||||
return
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
@@ -179,16 +190,37 @@ class OutcomeLedger:
|
||||
}
|
||||
|
||||
def get_stats(self, task_family: str = None) -> dict:
|
||||
"""Get statistics for a task family."""
|
||||
"""Get statistics for a task family (Rust core when built — the
|
||||
six-aggregate query runs in the kernel)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
return core.run_op("get_stats", self.db_path, task_family=task_family)
|
||||
if task_family:
|
||||
return self._query_stats("WHERE task_family = ?", (task_family,))
|
||||
return self._query_stats("", ())
|
||||
|
||||
def get_model_stats(self, model: str) -> dict:
|
||||
"""Get statistics for a model."""
|
||||
"""Get statistics for a model (Rust core when built)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
return core.run_op("get_model_stats", self.db_path, model=model)
|
||||
return self._query_stats("WHERE model = ?", (model,))
|
||||
|
||||
|
||||
# ── Rust core delegation (dichotomy wave, #2479 tranche) ────────────────
|
||||
|
||||
def _ol_core():
|
||||
"""The Rust outcome_ledger core when available (strangler law).
|
||||
RUNE_OL_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_OL_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ol_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
return ol_loader if ol_loader.available() else None
|
||||
|
||||
|
||||
def test_ledger():
|
||||
"""Test the outcome ledger."""
|
||||
print("Testing Outcome Ledger...")
|
||||
|
||||
@@ -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.217.0
|
||||
Version: v0.219.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.217.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.219.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
Generated
+4
-6
@@ -18,7 +18,6 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
name = "outcome_ledger"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -47,7 +46,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -72,9 +70,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -85,9 +83,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
name = "outcome_ledger"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "FULL parity port of src/rune/outcome_ledger.py (dichotomy wave, #2479 tranche); the ledger SQL runs in Rust against the system libsqlite3, makedirs/CLI/test_ledger stay Python"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Branch battery — replays the FROZEN ORACLE truth VERBATIM
|
||||
//! (frozen_to_truth.json, generated from the live python oracle with
|
||||
//! RUNE_OL_CORE=python pinned, timestamps pinned in the inputs so no
|
||||
//! masking is needed). Each scenario drives the kernel's own temp db
|
||||
//! through the op script, and every observation (the stats dict / the
|
||||
//! to_dict doc / null) must equal the frozen oracle exactly. The python
|
||||
//! side of the same replay lives in tests/test_outcome_ledger_port.py.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{get_model_stats, get_stats, init_db, record, to_dict, RecordView};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_to_truth.json");
|
||||
|
||||
fn step(path: &str, op: &Value) -> Value {
|
||||
let kind = op["op"].as_str().expect("op name");
|
||||
match kind {
|
||||
"new" => {
|
||||
init_db(path).expect("init_db");
|
||||
Value::Null
|
||||
}
|
||||
"record" => {
|
||||
let rec = RecordView::from_value(&op["record"]).expect("record doc");
|
||||
record(path, &rec).expect("record");
|
||||
Value::Null
|
||||
}
|
||||
"to_dict" => to_dict(&RecordView::from_value(&op["record"]).expect("record doc")),
|
||||
"get_stats" => {
|
||||
let family = op.get("family").and_then(Value::as_str);
|
||||
get_stats(path, family).expect("get_stats")
|
||||
}
|
||||
"get_model_stats" => {
|
||||
let model = op["model"].as_str().expect("model");
|
||||
get_model_stats(path, model).expect("get_model_stats")
|
||||
}
|
||||
other => panic!("unknown op {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replays_frozen_oracle_truth_verbatim() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).expect("truth parse");
|
||||
for scenario in truth["scenarios"].as_array().expect("scenarios") {
|
||||
let name = scenario["name"].as_str().expect("scenario name");
|
||||
let dir = std::env::temp_dir().join(format!("ol_battery_{name}"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let path = dir.join("ol.db");
|
||||
let path_str = path.to_str().expect("db path");
|
||||
for (i, op) in scenario["ops"].as_array().expect("ops").iter().enumerate() {
|
||||
let got = step(path_str, op);
|
||||
let want = scenario["obs"][i]["result"].clone();
|
||||
assert_eq!(
|
||||
got, want,
|
||||
"{name} op#{i} ({}) diverged from the frozen oracle",
|
||||
op["op"].as_str().unwrap_or("?")
|
||||
);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_op_is_an_err_envelope_not_a_panic() {
|
||||
let doc = serde_json::json!({"op": "no_such_op", "db_path": "/tmp/ol_never.db"});
|
||||
let arg = serde_json::to_string(&doc).unwrap();
|
||||
let arg = std::ffi::CString::new(arg).unwrap();
|
||||
let ptr = super::ol_run(arg.as_ptr());
|
||||
assert!(!ptr.is_null());
|
||||
let out = unsafe { std::ffi::CStr::from_ptr(ptr) }.to_str().unwrap();
|
||||
let env: Value = serde_json::from_str(out).unwrap();
|
||||
assert_eq!(env["ok"], Value::Bool(false));
|
||||
assert!(env["err"].as_str().unwrap().contains("arg.op must be one of"));
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
{
|
||||
"module": "outcome_ledger",
|
||||
"seed": 2479,
|
||||
"oracle": "src/rune/outcome_ledger.py",
|
||||
"pin": "RUNE_OL_CORE=python",
|
||||
"generated": "2026-10-11",
|
||||
"scenarios": [
|
||||
{
|
||||
"name": "s1_empty_db",
|
||||
"ops": [
|
||||
{
|
||||
"op": "new"
|
||||
},
|
||||
{
|
||||
"op": "get_stats"
|
||||
},
|
||||
{
|
||||
"op": "get_stats",
|
||||
"family": "greeting"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "qwen2.5:3b"
|
||||
}
|
||||
],
|
||||
"obs": [
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 0,
|
||||
"successes": 0,
|
||||
"success_rate": 0,
|
||||
"avg_attempts": 0,
|
||||
"avg_latency_ms": 0,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0,
|
||||
"total_cost": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 0,
|
||||
"successes": 0,
|
||||
"success_rate": 0,
|
||||
"avg_attempts": 0,
|
||||
"avg_latency_ms": 0,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0,
|
||||
"total_cost": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 0,
|
||||
"successes": 0,
|
||||
"success_rate": 0,
|
||||
"avg_attempts": 0,
|
||||
"avg_latency_ms": 0,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0,
|
||||
"total_cost": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "s2_record_and_stats",
|
||||
"ops": [
|
||||
{
|
||||
"op": "new"
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:01+00:00",
|
||||
"task_family": "greeting",
|
||||
"model": "qwen2.5:3b",
|
||||
"provider": "ollama",
|
||||
"tier": "T1",
|
||||
"task_success": true,
|
||||
"latency_ms": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:02+00:00",
|
||||
"task_family": "coding",
|
||||
"model": "qwen3:14b",
|
||||
"provider": "ollama",
|
||||
"tier": "T2",
|
||||
"task_success": true,
|
||||
"attempts": 2,
|
||||
"latency_ms": 500.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:03+00:00",
|
||||
"task_family": "coding",
|
||||
"model": "qwen3:14b",
|
||||
"provider": "ollama",
|
||||
"tier": "T2",
|
||||
"task_success": false,
|
||||
"user_correction": true,
|
||||
"latency_ms": 600.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "get_stats",
|
||||
"family": "greeting"
|
||||
},
|
||||
{
|
||||
"op": "get_stats",
|
||||
"family": "coding"
|
||||
},
|
||||
{
|
||||
"op": "get_stats"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "qwen3:14b"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "unknown-model"
|
||||
}
|
||||
],
|
||||
"obs": [
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 1,
|
||||
"successes": 1,
|
||||
"success_rate": 1.0,
|
||||
"avg_attempts": 1.0,
|
||||
"avg_latency_ms": 100.0,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0.0,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 1.5,
|
||||
"avg_latency_ms": 550.0,
|
||||
"corrections": 1,
|
||||
"correction_rate": 0.5,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 3,
|
||||
"successes": 2,
|
||||
"success_rate": 0.6666666666666666,
|
||||
"avg_attempts": 1.3333333333333333,
|
||||
"avg_latency_ms": 400.0,
|
||||
"corrections": 1,
|
||||
"correction_rate": 0.3333333333333333,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 1.5,
|
||||
"avg_latency_ms": 550.0,
|
||||
"corrections": 1,
|
||||
"correction_rate": 0.5,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 0,
|
||||
"successes": 0,
|
||||
"success_rate": 0,
|
||||
"avg_attempts": 0,
|
||||
"avg_latency_ms": 0,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0,
|
||||
"total_cost": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "s3_full_record_to_dict",
|
||||
"ops": [
|
||||
{
|
||||
"op": "new"
|
||||
},
|
||||
{
|
||||
"op": "to_dict",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:04+00:00",
|
||||
"task_family": "review",
|
||||
"model": "gpt-5-mini",
|
||||
"provider": "openai",
|
||||
"tier": "T3",
|
||||
"prompt_tokens": 1200,
|
||||
"output_tokens": 340,
|
||||
"cached_tokens": 512,
|
||||
"latency_ms": 123.75,
|
||||
"attempts": 1,
|
||||
"verification_result": "pass",
|
||||
"user_correction": false,
|
||||
"task_success": true,
|
||||
"estimated_cost": 0.0025
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:04+00:00",
|
||||
"task_family": "review",
|
||||
"model": "gpt-5-mini",
|
||||
"provider": "openai",
|
||||
"tier": "T3",
|
||||
"prompt_tokens": 1200,
|
||||
"output_tokens": 340,
|
||||
"cached_tokens": 512,
|
||||
"latency_ms": 123.75,
|
||||
"attempts": 1,
|
||||
"verification_result": "pass",
|
||||
"user_correction": false,
|
||||
"task_success": true,
|
||||
"estimated_cost": 0.0025
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:05+00:00",
|
||||
"task_family": "review",
|
||||
"model": "gpt-5-mini",
|
||||
"provider": "openai",
|
||||
"tier": "T3",
|
||||
"prompt_tokens": 1200,
|
||||
"output_tokens": 340,
|
||||
"cached_tokens": 512,
|
||||
"latency_ms": 123.75,
|
||||
"attempts": 1,
|
||||
"verification_result": "fail",
|
||||
"user_correction": false,
|
||||
"task_success": false,
|
||||
"estimated_cost": 0.0075
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "get_stats"
|
||||
},
|
||||
{
|
||||
"op": "get_stats",
|
||||
"family": "review"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "gpt-5-mini"
|
||||
}
|
||||
],
|
||||
"obs": [
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"timestamp": "2026-10-11T00:00:04+00:00",
|
||||
"task_family": "review",
|
||||
"model": "gpt-5-mini",
|
||||
"provider": "openai",
|
||||
"tier": "T3",
|
||||
"prompt_tokens": 1200,
|
||||
"output_tokens": 340,
|
||||
"cached_tokens": 512,
|
||||
"latency_ms": 123.75,
|
||||
"attempts": 1,
|
||||
"verification_result": "pass",
|
||||
"user_correction": false,
|
||||
"task_success": true,
|
||||
"estimated_cost": 0.0025
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 1.0,
|
||||
"avg_latency_ms": 123.75,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0.0,
|
||||
"total_cost": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 1.0,
|
||||
"avg_latency_ms": 123.75,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0.0,
|
||||
"total_cost": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 1.0,
|
||||
"avg_latency_ms": 123.75,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0.0,
|
||||
"total_cost": 0.01
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "s4_persistence_reopen",
|
||||
"ops": [
|
||||
{
|
||||
"op": "new"
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:01+00:00",
|
||||
"task_family": "port",
|
||||
"model": "qwen2.5:3b",
|
||||
"provider": "ollama",
|
||||
"tier": "T1",
|
||||
"task_success": true,
|
||||
"latency_ms": 42.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:02+00:00",
|
||||
"task_family": "port",
|
||||
"model": "qwen3:14b",
|
||||
"provider": "ollama",
|
||||
"tier": "T2",
|
||||
"task_success": false,
|
||||
"user_correction": true,
|
||||
"attempts": 3,
|
||||
"latency_ms": 900.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "new",
|
||||
"reuse": true
|
||||
},
|
||||
{
|
||||
"op": "get_stats"
|
||||
},
|
||||
{
|
||||
"op": "get_stats",
|
||||
"family": "port"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "qwen2.5:3b"
|
||||
}
|
||||
],
|
||||
"obs": [
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 2.0,
|
||||
"avg_latency_ms": 471.25,
|
||||
"corrections": 1,
|
||||
"correction_rate": 0.5,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 2.0,
|
||||
"avg_latency_ms": 471.25,
|
||||
"corrections": 1,
|
||||
"correction_rate": 0.5,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 1,
|
||||
"successes": 1,
|
||||
"success_rate": 1.0,
|
||||
"avg_attempts": 1.0,
|
||||
"avg_latency_ms": 42.5,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0.0,
|
||||
"total_cost": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "s5_averages_and_rates",
|
||||
"ops": [
|
||||
{
|
||||
"op": "new"
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:01+00:00",
|
||||
"task_family": "ops",
|
||||
"model": "m1",
|
||||
"task_success": true,
|
||||
"attempts": 1,
|
||||
"latency_ms": 10.5,
|
||||
"estimated_cost": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:02+00:00",
|
||||
"task_family": "ops",
|
||||
"model": "m1",
|
||||
"task_success": false,
|
||||
"user_correction": true,
|
||||
"attempts": 2,
|
||||
"latency_ms": 20.25,
|
||||
"estimated_cost": 1.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "record",
|
||||
"record": {
|
||||
"timestamp": "2026-10-11T00:00:03+00:00",
|
||||
"task_family": "ops",
|
||||
"model": "m2",
|
||||
"task_success": true,
|
||||
"attempts": 3,
|
||||
"latency_ms": 30.125,
|
||||
"user_correction": true,
|
||||
"estimated_cost": 2.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "get_stats"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "m1"
|
||||
},
|
||||
{
|
||||
"op": "get_model_stats",
|
||||
"model": "m2"
|
||||
},
|
||||
{
|
||||
"op": "get_stats",
|
||||
"family": "ops"
|
||||
}
|
||||
],
|
||||
"obs": [
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 3,
|
||||
"successes": 2,
|
||||
"success_rate": 0.6666666666666666,
|
||||
"avg_attempts": 2.0,
|
||||
"avg_latency_ms": 20.291666666666668,
|
||||
"corrections": 2,
|
||||
"correction_rate": 0.6666666666666666,
|
||||
"total_cost": 3.75
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 2,
|
||||
"successes": 1,
|
||||
"success_rate": 0.5,
|
||||
"avg_attempts": 1.5,
|
||||
"avg_latency_ms": 15.375,
|
||||
"corrections": 1,
|
||||
"correction_rate": 0.5,
|
||||
"total_cost": 1.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 1,
|
||||
"successes": 1,
|
||||
"success_rate": 1.0,
|
||||
"avg_attempts": 3.0,
|
||||
"avg_latency_ms": 30.125,
|
||||
"corrections": 1,
|
||||
"correction_rate": 1.0,
|
||||
"total_cost": 2.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"result": {
|
||||
"total": 3,
|
||||
"successes": 2,
|
||||
"success_rate": 0.6666666666666666,
|
||||
"avg_attempts": 2.0,
|
||||
"avg_latency_ms": 20.291666666666668,
|
||||
"corrections": 2,
|
||||
"correction_rate": 0.6666666666666666,
|
||||
"total_cost": 3.75
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,106 +1,407 @@
|
||||
//! outcome_ledger — persist cognitive-performance evidence (W6).
|
||||
//! PARITY port of src/rune/outcome_ledger.py.
|
||||
use serde::{Deserialize, Serialize};
|
||||
//!
|
||||
//! FULL parity port of src/rune/outcome_ledger.py (dichotomy wave, #2479
|
||||
//! tranche). The kernel is RUST-side per the intent ledger
|
||||
//! (store/scaffold/intent/outcome_ledger.json: sqlite3, storage-backed,
|
||||
//! deterministic/state domain). It owns the SAME SQL the python oracle
|
||||
//! runs — table DDL, the 14-column INSERT and the six-aggregate stats
|
||||
//! query — against the SYSTEM libsqlite3 (memory_storage precedent: a
|
||||
//! system library is not a crate dependency), so aggregation happens in
|
||||
//! the identical engine and the floats come back identical.
|
||||
//!
|
||||
//! FFI (doc-envelope law): `ol_run({op, db_path, record?, task_family?,
|
||||
//! model?})` → {"ok": true, "value": ...} | {"ok": false, "err": "..."}.
|
||||
//! ONE entry point (the intent's "Rust 1 fn, no delegation" ruling).
|
||||
//! The caller frees returned strings with capability_scoring's
|
||||
//! cs_free_string (shared process) — this crate deliberately exports NO
|
||||
//! free symbol of its own.
|
||||
//!
|
||||
//! Pure-kernel law: the kernel takes the db_path as input and owns no
|
||||
//! global state, no clock (timestamps ride in the record), no CLI and no
|
||||
//! makedirs — the facade keeps directory prep, the CLI and test_ledger.
|
||||
//!
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the op wire string
|
||||
//! parses into an `Op` enum with an EXHAUSTIVE dispatch match (adding an
|
||||
//! op is a compile error until handled), the record is a zero-copy
|
||||
//! borrowed `RecordView<'a>` over the wire doc (no owned-string round
|
||||
//! trip on the hot INSERT path), the stats WHERE fragment is a
|
||||
//! `Where<'a>` enum whose SQL is a `&'static str` const (the where clause
|
||||
//! is never interpolated from data), and binding walks an iterator.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct OutcomeRecord {
|
||||
#[serde(default)]
|
||||
pub timestamp: String,
|
||||
#[serde(default)]
|
||||
pub task_family: String,
|
||||
#[serde(default)]
|
||||
pub model: String,
|
||||
#[serde(default)]
|
||||
pub provider: String,
|
||||
#[serde(default)]
|
||||
pub tier: String,
|
||||
#[serde(default)]
|
||||
pub mod sqlite;
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use sqlite::{Conn, Stmt};
|
||||
|
||||
/// Zero-copy borrowed view of a cognitive-work outcome record — mirrors
|
||||
/// the oracle's OutcomeRecord field-for-field; python dataclass defaults
|
||||
/// apply on the wire (absent → default). All string fields borrow from
|
||||
/// the argument doc; nothing is allocated to drive an INSERT.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RecordView<'a> {
|
||||
pub timestamp: &'a str,
|
||||
pub task_family: &'a str,
|
||||
pub model: &'a str,
|
||||
pub provider: &'a str,
|
||||
pub tier: &'a str,
|
||||
pub prompt_tokens: i64,
|
||||
#[serde(default)]
|
||||
pub output_tokens: i64,
|
||||
#[serde(default)]
|
||||
pub cached_tokens: i64,
|
||||
#[serde(default)]
|
||||
pub latency_ms: f64,
|
||||
#[serde(default)]
|
||||
pub attempts: i64,
|
||||
#[serde(default)]
|
||||
pub verification_result: String,
|
||||
#[serde(default)]
|
||||
pub verification_result: &'a str,
|
||||
pub user_correction: bool,
|
||||
#[serde(default)]
|
||||
pub task_success: bool,
|
||||
#[serde(default)]
|
||||
pub estimated_cost: f64,
|
||||
}
|
||||
|
||||
pub struct OutcomeStats {
|
||||
pub total: usize,
|
||||
pub success_rate: f64,
|
||||
pub first_attempt_rate: f64,
|
||||
pub avg_latency_ms: f64,
|
||||
pub total_cost: f64,
|
||||
}
|
||||
|
||||
pub fn compute_stats(records: &[OutcomeRecord]) -> OutcomeStats {
|
||||
let total = records.len();
|
||||
if total == 0 {
|
||||
return OutcomeStats {
|
||||
total: 0,
|
||||
success_rate: 0.0,
|
||||
first_attempt_rate: 0.0,
|
||||
avg_latency_ms: 0.0,
|
||||
total_cost: 0.0,
|
||||
};
|
||||
}
|
||||
let successes = records.iter().filter(|r| r.task_success).count();
|
||||
let first_attempts = records.iter().filter(|r| r.attempts == 1).count();
|
||||
let total_latency: f64 = records.iter().map(|r| r.latency_ms).sum();
|
||||
let total_cost: f64 = records.iter().map(|r| r.estimated_cost).sum();
|
||||
OutcomeStats {
|
||||
total,
|
||||
success_rate: successes as f64 / total as f64,
|
||||
first_attempt_rate: first_attempts as f64 / total as f64,
|
||||
avg_latency_ms: total_latency / total as f64,
|
||||
total_cost,
|
||||
impl<'a> RecordView<'a> {
|
||||
pub fn from_value(v: &'a Value) -> Option<RecordView<'a>> {
|
||||
let o = v.as_object()?;
|
||||
let s = |k: &str| Some(o.get(k).and_then(Value::as_str).unwrap_or_default());
|
||||
let i = |k: &str, d: i64| o.get(k).and_then(Value::as_i64).unwrap_or(d);
|
||||
let f = |k: &str, d: f64| o.get(k).and_then(Value::as_f64).unwrap_or(d);
|
||||
let b = |k: &str| o.get(k).and_then(Value::as_bool).unwrap_or(false);
|
||||
Some(RecordView {
|
||||
timestamp: s("timestamp")?,
|
||||
task_family: s("task_family")?,
|
||||
model: s("model")?,
|
||||
provider: s("provider")?,
|
||||
tier: s("tier")?,
|
||||
prompt_tokens: i("prompt_tokens", 0),
|
||||
output_tokens: i("output_tokens", 0),
|
||||
cached_tokens: i("cached_tokens", 0),
|
||||
latency_ms: f("latency_ms", 0.0),
|
||||
attempts: i("attempts", 1),
|
||||
verification_result: s("verification_result")?,
|
||||
user_correction: b("user_correction"),
|
||||
task_success: b("task_success"),
|
||||
estimated_cost: f("estimated_cost", 0.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// OutcomeRecord.to_dict — same key order as the oracle.
|
||||
pub fn to_dict(r: &RecordView) -> Value {
|
||||
json!({
|
||||
"timestamp": r.timestamp,
|
||||
"task_family": r.task_family,
|
||||
"model": r.model,
|
||||
"provider": r.provider,
|
||||
"tier": r.tier,
|
||||
"prompt_tokens": r.prompt_tokens,
|
||||
"output_tokens": r.output_tokens,
|
||||
"cached_tokens": r.cached_tokens,
|
||||
"latency_ms": r.latency_ms,
|
||||
"attempts": r.attempts,
|
||||
"verification_result": r.verification_result,
|
||||
"user_correction": r.user_correction,
|
||||
"task_success": r.task_success,
|
||||
"estimated_cost": r.estimated_cost,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stats_empty() {
|
||||
let stats = compute_stats(&[]);
|
||||
assert_eq!(stats.total, 0);
|
||||
const CREATE_SQL: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS outcomes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
task_family TEXT,
|
||||
model TEXT,
|
||||
provider TEXT,
|
||||
tier TEXT,
|
||||
prompt_tokens INTEGER,
|
||||
output_tokens INTEGER,
|
||||
cached_tokens INTEGER,
|
||||
latency_ms REAL,
|
||||
attempts INTEGER,
|
||||
verification_result TEXT,
|
||||
user_correction BOOLEAN,
|
||||
task_success BOOLEAN,
|
||||
estimated_cost REAL
|
||||
)
|
||||
"#;
|
||||
|
||||
/// OutcomeLedger._init_db — identical DDL to the oracle.
|
||||
pub fn init_db(db_path: &str) -> Result<(), String> {
|
||||
let conn = Conn::open(db_path)?;
|
||||
conn.exec(CREATE_SQL)
|
||||
}
|
||||
|
||||
const INSERT_SQL: &str = r#"
|
||||
INSERT INTO outcomes (
|
||||
timestamp, task_family, model, provider, tier,
|
||||
prompt_tokens, output_tokens, cached_tokens,
|
||||
latency_ms, attempts, verification_result,
|
||||
user_correction, task_success, estimated_cost
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#;
|
||||
|
||||
/// OutcomeLedger.record — identical INSERT to the oracle, driven from the
|
||||
/// borrowed view: every bind reads straight out of the wire doc.
|
||||
pub fn record(db_path: &str, r: &RecordView) -> Result<(), String> {
|
||||
let conn = Conn::open(db_path)?;
|
||||
let mut stmt = conn.prepare(INSERT_SQL)?;
|
||||
let bind_err = [
|
||||
stmt.bind_text(1, r.timestamp),
|
||||
stmt.bind_text(2, r.task_family),
|
||||
stmt.bind_text(3, r.model),
|
||||
stmt.bind_text(4, r.provider),
|
||||
stmt.bind_text(5, r.tier),
|
||||
stmt.bind_int64(6, r.prompt_tokens),
|
||||
stmt.bind_int64(7, r.output_tokens),
|
||||
stmt.bind_int64(8, r.cached_tokens),
|
||||
stmt.bind_double(9, r.latency_ms),
|
||||
stmt.bind_int64(10, r.attempts),
|
||||
stmt.bind_text(11, r.verification_result),
|
||||
stmt.bind_int64(12, i64::from(r.user_correction)),
|
||||
stmt.bind_int64(13, i64::from(r.task_success)),
|
||||
stmt.bind_double(14, r.estimated_cost),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, res)| res.err().map(|e| format!("bind #{}: {e}", idx + 1)));
|
||||
if let Some(e) = bind_err {
|
||||
return Err(e);
|
||||
}
|
||||
let step_res = stmt.step();
|
||||
stmt.finalize();
|
||||
step_res.map(|_| ())
|
||||
}
|
||||
|
||||
const STATS_SQL_HEAD: &str = r#"
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN task_success THEN 1 ELSE 0 END) as successes,
|
||||
AVG(attempts) as avg_attempts,
|
||||
AVG(latency_ms) as avg_latency,
|
||||
SUM(CASE WHEN user_correction THEN 1 ELSE 0 END) as corrections,
|
||||
SUM(estimated_cost) as total_cost
|
||||
FROM outcomes
|
||||
"#;
|
||||
|
||||
/// The stats filter — each variant's SQL is a `&'static str` const, so
|
||||
/// the where clause is NEVER interpolated from data (the oracle's
|
||||
/// f-string shape, minus the injection-shaped surface).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Where<'a> {
|
||||
All,
|
||||
TaskFamily(&'a str),
|
||||
Model(&'a str),
|
||||
}
|
||||
|
||||
impl Where<'_> {
|
||||
fn sql(&self) -> &'static str {
|
||||
match self {
|
||||
Where::All => "",
|
||||
Where::TaskFamily(_) => "WHERE task_family = ?",
|
||||
Where::Model(_) => "WHERE model = ?",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stats_with_data() {
|
||||
let records = vec![
|
||||
OutcomeRecord {
|
||||
task_success: true,
|
||||
attempts: 1,
|
||||
latency_ms: 100.0,
|
||||
..Default::default()
|
||||
},
|
||||
OutcomeRecord {
|
||||
task_success: true,
|
||||
attempts: 2,
|
||||
latency_ms: 200.0,
|
||||
..Default::default()
|
||||
},
|
||||
OutcomeRecord {
|
||||
task_success: false,
|
||||
attempts: 3,
|
||||
latency_ms: 300.0,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let stats = compute_stats(&records);
|
||||
assert_eq!(stats.total, 3);
|
||||
assert!((stats.success_rate - 0.667).abs() < 0.01);
|
||||
assert!((stats.first_attempt_rate - 0.333).abs() < 0.01);
|
||||
fn param(&self) -> Option<&str> {
|
||||
match self {
|
||||
Where::All => None,
|
||||
Where::TaskFamily(v) | Where::Model(v) => Some(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OutcomeLedger._query_stats — identical aggregate SQL to the oracle.
|
||||
pub fn query_stats(db_path: &str, wc: Where) -> Result<Value, String> {
|
||||
let conn = Conn::open(db_path)?;
|
||||
let sql = format!("{STATS_SQL_HEAD}
|
||||
{}
|
||||
", wc.sql());
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
if let Some(p) = wc.param() {
|
||||
stmt.bind_text(1, p)?;
|
||||
}
|
||||
let has_row = stmt.step()?;
|
||||
let out = if has_row && stmt.column_int64(0) > 0 {
|
||||
let total = stmt.column_int64(0);
|
||||
let successes = nonzero_or_zero(&stmt, 1);
|
||||
let corrections = nonzero_or_zero(&stmt, 4);
|
||||
json!({
|
||||
"total": total,
|
||||
"successes": successes,
|
||||
"success_rate": successes as f64 / total as f64,
|
||||
"avg_attempts": nullable_double(&stmt, 2),
|
||||
"avg_latency_ms": nullable_double(&stmt, 3),
|
||||
"corrections": corrections,
|
||||
"correction_rate": corrections as f64 / total as f64,
|
||||
"total_cost": nullable_double(&stmt, 5),
|
||||
})
|
||||
} else {
|
||||
zero_stats()
|
||||
};
|
||||
stmt.finalize();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// SUM(CASE ...) over existing rows — never NULL, but read defensively
|
||||
/// the way python's sqlite3 would surface it.
|
||||
fn nonzero_or_zero(stmt: &Stmt, i: usize) -> i64 {
|
||||
if stmt.column_type(i) == sqlite::SQLITE_NULL {
|
||||
0
|
||||
} else {
|
||||
stmt.column_int64(i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an AVG/SUM column the way python's sqlite3 would surface it:
|
||||
/// REAL → float, NULL → None (the oracle's dicts carry whichever came back).
|
||||
fn nullable_double(stmt: &Stmt, i: usize) -> Value {
|
||||
if stmt.column_type(i) == sqlite::SQLITE_NULL {
|
||||
Value::Null
|
||||
} else {
|
||||
json!(stmt.column_double(i))
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's empty-result dict — note these are INTS in python
|
||||
/// (`"success_rate": 0`, not `0.0`), frozen verbatim in the truth battery.
|
||||
fn zero_stats() -> Value {
|
||||
json!({
|
||||
"total": 0,
|
||||
"successes": 0,
|
||||
"success_rate": 0,
|
||||
"avg_attempts": 0,
|
||||
"avg_latency_ms": 0,
|
||||
"corrections": 0,
|
||||
"correction_rate": 0,
|
||||
"total_cost": 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// OutcomeLedger.get_stats — the task_family branch split.
|
||||
pub fn get_stats(db_path: &str, task_family: Option<&str>) -> Result<Value, String> {
|
||||
match task_family {
|
||||
Some(f) => query_stats(db_path, Where::TaskFamily(f)),
|
||||
None => query_stats(db_path, Where::All),
|
||||
}
|
||||
}
|
||||
|
||||
/// OutcomeLedger.get_model_stats.
|
||||
pub fn get_model_stats(db_path: &str, model: &str) -> Result<Value, String> {
|
||||
query_stats(db_path, Where::Model(model))
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law): ol_run — the ONE entry point ────────────────
|
||||
|
||||
/// The wire ops — parsed once; dispatch is an exhaustive match, so adding
|
||||
/// an op is a compile error until it is handled here.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Op {
|
||||
InitDb,
|
||||
Record,
|
||||
GetStats,
|
||||
GetModelStats,
|
||||
ToDict,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
fn parse(s: &str) -> Option<Op> {
|
||||
match s {
|
||||
"init_db" => Some(Op::InitDb),
|
||||
"record" => Some(Op::Record),
|
||||
"get_stats" => Some(Op::GetStats),
|
||||
"get_model_stats" => Some(Op::GetModelStats),
|
||||
"to_dict" => Some(Op::ToDict),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zero-copy borrowed argument view over the wire doc.
|
||||
struct Args<'a> {
|
||||
db_path: &'a str,
|
||||
record: Option<&'a Value>,
|
||||
task_family: Option<&'a str>,
|
||||
model: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Args<'a> {
|
||||
fn from_value(v: &'a Value) -> Option<Args<'a>> {
|
||||
Some(Args {
|
||||
db_path: v.get("db_path")?.as_str()?,
|
||||
record: v.get("record"),
|
||||
task_family: v.get("task_family").and_then(Value::as_str),
|
||||
model: v.get("model").and_then(Value::as_str),
|
||||
})
|
||||
}
|
||||
|
||||
fn record(&self) -> Result<RecordView<'a>, String> {
|
||||
let v = self.record.ok_or_else(|| "arg.record required".to_string())?;
|
||||
RecordView::from_value(v).ok_or_else(|| "arg.record invalid".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const std::os::raw::c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {e}"))
|
||||
}
|
||||
|
||||
fn envelope(result: Result<Value, String>) -> *mut std::os::raw::c_char {
|
||||
let body = match result {
|
||||
Ok(value) => json!({"ok": true, "value": value}),
|
||||
Err(err) => json!({"ok": false, "err": err}),
|
||||
};
|
||||
match serde_json::to_string(&body)
|
||||
.ok()
|
||||
.and_then(|s| std::ffi::CString::new(s).ok())
|
||||
{
|
||||
Some(c) => c.into_raw(),
|
||||
None => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ol_run({"op", "db_path", "record"?, "task_family"?, "model"?}) —
|
||||
/// ops: init_db | record | get_stats | get_model_stats | to_dict.
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
// pinned: #[no_mangle] extern "C" — the ol_loader.py ABI requires this exact safe-fn signature; the raw pointer IS the FFI contract
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ol_run(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| serde_json::from_str::<Value>(&text).map_err(|e| e.to_string()))
|
||||
.and_then(|doc| {
|
||||
let op = doc
|
||||
.get("op")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(Op::parse)
|
||||
.ok_or_else(|| "arg.op must be one of init_db|record|get_stats|get_model_stats|to_dict".to_string())?;
|
||||
let args = Args::from_value(&doc).ok_or_else(|| "arg.db_path required".to_string())?;
|
||||
dispatch(op, &args)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
fn dispatch(op: Op, a: &Args) -> Result<Value, String> {
|
||||
match op {
|
||||
Op::InitDb => {
|
||||
init_db(a.db_path)?;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Op::Record => {
|
||||
let rec = a.record()?;
|
||||
record(a.db_path, &rec)?;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Op::GetStats => get_stats(a.db_path, a.task_family),
|
||||
Op::GetModelStats => {
|
||||
let model = a.model.ok_or_else(|| "arg.model required".to_string())?;
|
||||
get_model_stats(a.db_path, model)
|
||||
}
|
||||
Op::ToDict => {
|
||||
let rec = a.record()?;
|
||||
Ok(to_dict(&rec))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! sqlite.rs — minimal bindings to the SYSTEM libsqlite3 for the
|
||||
//! outcome_ledger kernel (dichotomy wave, #2479 tranche). Follows the
|
||||
//! memory_storage precedent (R2 W2.0, Nick's ruling): SQLite is retained
|
||||
//! and wired in; a system library is not a crate dependency, so the
|
||||
//! kernel's only cargo deps stay the wave's serde_json.
|
||||
//!
|
||||
//! READ-WRITE: outcome_ledger is a ledger — record() INSERTs and init_db
|
||||
//! CREATEs the table, mirroring the python oracle's sqlite3 usage exactly
|
||||
//! (same SQL text, same prepared-statement lifecycle).
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
|
||||
const SQLITE_OK: c_int = 0;
|
||||
const SQLITE_ROW: c_int = 100;
|
||||
const SQLITE_DONE: c_int = 101;
|
||||
const SQLITE_OPEN_READWRITE: c_int = 0x00000002;
|
||||
const SQLITE_OPEN_CREATE: c_int = 0x00000004;
|
||||
|
||||
pub const SQLITE_INTEGER: c_int = 1;
|
||||
pub const SQLITE_FLOAT: c_int = 2;
|
||||
pub const SQLITE_TEXT: c_int = 3;
|
||||
pub const SQLITE_NULL: c_int = 5;
|
||||
|
||||
const SQLITE_TRANSIENT: isize = -1;
|
||||
|
||||
#[link(name = "sqlite3")]
|
||||
extern "C" {
|
||||
fn sqlite3_open_v2(
|
||||
filename: *const c_char,
|
||||
db: *mut *mut c_void,
|
||||
flags: c_int,
|
||||
vfs: *const c_char,
|
||||
) -> c_int;
|
||||
fn sqlite3_prepare_v2(
|
||||
db: *mut c_void,
|
||||
sql: *const c_char,
|
||||
nbytes: c_int,
|
||||
stmt: *mut *mut c_void,
|
||||
tail: *mut *const c_char,
|
||||
) -> c_int;
|
||||
fn sqlite3_bind_int64(stmt: *mut c_void, idx: c_int, val: i64) -> c_int;
|
||||
fn sqlite3_bind_double(stmt: *mut c_void, idx: c_int, val: f64) -> c_int;
|
||||
fn sqlite3_bind_text(
|
||||
stmt: *mut c_void,
|
||||
idx: c_int,
|
||||
val: *const c_char,
|
||||
nbytes: c_int,
|
||||
destructor: isize,
|
||||
) -> c_int;
|
||||
fn sqlite3_step(stmt: *mut c_void) -> c_int;
|
||||
fn sqlite3_column_type(stmt: *mut c_void, i: c_int) -> c_int;
|
||||
fn sqlite3_column_int64(stmt: *mut c_void, i: c_int) -> i64;
|
||||
fn sqlite3_column_double(stmt: *mut c_void, i: c_int) -> f64;
|
||||
fn sqlite3_column_text(stmt: *mut c_void, i: c_int) -> *const u8;
|
||||
fn sqlite3_finalize(stmt: *mut c_void) -> c_int;
|
||||
fn sqlite3_close(db: *mut c_void) -> c_int;
|
||||
fn sqlite3_errmsg(db: *mut c_void) -> *const c_char;
|
||||
}
|
||||
|
||||
pub struct Conn(*mut c_void);
|
||||
|
||||
impl Conn {
|
||||
pub fn open(path: &str) -> Result<Conn, String> {
|
||||
let cpath = CString::new(path).map_err(|e| e.to_string())?;
|
||||
let mut db: *mut c_void = std::ptr::null_mut();
|
||||
let rc = unsafe {
|
||||
sqlite3_open_v2(cpath.as_ptr(), &mut db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, std::ptr::null())
|
||||
};
|
||||
if rc != SQLITE_OK {
|
||||
return Err(format!("sqlite3_open_v2 failed (rc={rc}) for {path}"));
|
||||
}
|
||||
Ok(Conn(db))
|
||||
}
|
||||
|
||||
fn errmsg(&self) -> String {
|
||||
unsafe {
|
||||
CStr::from_ptr(sqlite3_errmsg(self.0))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a constant SQL statement to completion (CREATE TABLE ...).
|
||||
pub fn exec(&self, sql: &str) -> Result<(), String> {
|
||||
let csql = CString::new(sql).map_err(|e| e.to_string())?;
|
||||
let mut stmt: *mut c_void = std::ptr::null_mut();
|
||||
let rc = unsafe {
|
||||
sqlite3_prepare_v2(self.0, csql.as_ptr(), -1, &mut stmt, std::ptr::null_mut())
|
||||
};
|
||||
if rc != SQLITE_OK {
|
||||
return Err(self.errmsg());
|
||||
}
|
||||
let rc = unsafe { sqlite3_step(stmt) };
|
||||
let step_err = if rc != SQLITE_ROW && rc != SQLITE_DONE {
|
||||
Some(self.errmsg())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
unsafe { sqlite3_finalize(stmt) };
|
||||
match step_err {
|
||||
Some(e) => Err(e),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare(&self, sql: &str) -> Result<Stmt, String> {
|
||||
let csql = CString::new(sql).map_err(|e| e.to_string())?;
|
||||
let mut stmt: *mut c_void = std::ptr::null_mut();
|
||||
let rc = unsafe {
|
||||
sqlite3_prepare_v2(self.0, csql.as_ptr(), -1, &mut stmt, std::ptr::null_mut())
|
||||
};
|
||||
if rc != SQLITE_OK {
|
||||
return Err(self.errmsg());
|
||||
}
|
||||
Ok(Stmt(stmt))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Conn {
|
||||
fn drop(&mut self) {
|
||||
unsafe { sqlite3_close(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Stmt(*mut c_void);
|
||||
|
||||
impl Stmt {
|
||||
pub fn bind_text(&mut self, idx: usize, val: &str) -> Result<(), String> {
|
||||
let cval = CString::new(val).map_err(|e| e.to_string())?;
|
||||
let rc = unsafe {
|
||||
sqlite3_bind_text(self.0, idx as c_int, cval.as_ptr(), val.len() as c_int, SQLITE_TRANSIENT)
|
||||
};
|
||||
if rc != SQLITE_OK {
|
||||
Err(format!("sqlite3_bind_text failed (rc={rc})"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_int64(&mut self, idx: usize, val: i64) -> Result<(), String> {
|
||||
let rc = unsafe { sqlite3_bind_int64(self.0, idx as c_int, val) };
|
||||
if rc != SQLITE_OK {
|
||||
Err(format!("sqlite3_bind_int64 failed (rc={rc})"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_double(&mut self, idx: usize, val: f64) -> Result<(), String> {
|
||||
let rc = unsafe { sqlite3_bind_double(self.0, idx as c_int, val) };
|
||||
if rc != SQLITE_OK {
|
||||
Err(format!("sqlite3_bind_double failed (rc={rc})"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind nothing at `idx` — the python sqlite3 NULL equivalent.
|
||||
pub fn bind_null(&mut self, idx: usize) -> Result<(), String> {
|
||||
// sqlite3_bind_null exists but a skipped parameter is also NULL;
|
||||
// bind explicitly to keep the statement fully bound.
|
||||
extern "C" {
|
||||
fn sqlite3_bind_null(stmt: *mut c_void, idx: c_int) -> c_int;
|
||||
}
|
||||
let rc = unsafe { sqlite3_bind_null(self.0, idx as c_int) };
|
||||
if rc != SQLITE_OK {
|
||||
Err(format!("sqlite3_bind_null failed (rc={rc})"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance one row. Ok(true) = row available, Ok(false) = done.
|
||||
pub fn step(&mut self) -> Result<bool, String> {
|
||||
match unsafe { sqlite3_step(self.0) } {
|
||||
SQLITE_ROW => Ok(true),
|
||||
SQLITE_DONE => Ok(false),
|
||||
rc => Err(format!("sqlite3_step failed (rc={rc})")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column_type(&self, i: usize) -> c_int {
|
||||
unsafe { sqlite3_column_type(self.0, i as c_int) }
|
||||
}
|
||||
|
||||
pub fn column_int64(&self, i: usize) -> i64 {
|
||||
unsafe { sqlite3_column_int64(self.0, i as c_int) }
|
||||
}
|
||||
|
||||
pub fn column_double(&self, i: usize) -> f64 {
|
||||
unsafe { sqlite3_column_double(self.0, i as c_int) }
|
||||
}
|
||||
|
||||
pub fn column_text(&self, i: usize) -> String {
|
||||
unsafe {
|
||||
let ptr = sqlite3_column_text(self.0, i as c_int);
|
||||
if ptr.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(ptr.cast::<c_char>()).to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize(self) {
|
||||
unsafe { sqlite3_finalize(self.0) };
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,25 @@
|
||||
# Contract BUILDER-2026-10-11T21:57:54Z-52c500-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/auto_context.py to FULL Go (dichotomy wave #2479, GO side verdict): name-preserving port of all 8 public functions to src/go/auto_context with a frozen oracle-truth battery (every function, every branch), then a Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
- `src/go/auto_context/`
|
||||
- `src/rune/auto_context.py`
|
||||
|
||||
## 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-11T23:43:42Z", "contract": "BUILDER-2026-10-11T21:57:54Z-52c500-factory-wave", "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": ["src/go/auto_context/", "src/rune/auto_context.py"], "out_of_scope": [], "verdict": "green"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- contract opened for auto_context FULL Go port; parity plan frozen
|
||||
- port + 52-row truth generator built; parity commit f129a6a4
|
||||
- battery 52/52 frozen rows exact-equal vs live oracle (truth regenerated twice, deterministic); go test+vet+gofmt clean;
|
||||
- closure-record
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Port evidence for the outcome_ledger dichotomy port (#2479 tranche).
|
||||
|
||||
Replays the frozen oracle truth (src/rust/outcome_ledger/src/
|
||||
frozen_to_truth.json — generated from the live python oracle with
|
||||
RUNE_OL_CORE=python pinned and timestamps pinned in the inputs, 5
|
||||
scenarios / 35 observations) through the wired facade TWICE:
|
||||
|
||||
* python path — RUNE_OL_CORE=python (the pinned fallback)
|
||||
* rust path — the built kernel via ol_loader (skipped when absent)
|
||||
|
||||
Every observation (result) must equal the frozen oracle exactly.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from rune.outcome_ledger import OutcomeLedger, OutcomeRecord
|
||||
|
||||
_TRUTH = Path(__file__).resolve().parents[1] / "src" / "rust" / "outcome_ledger" / "src" / "frozen_to_truth.json"
|
||||
|
||||
|
||||
class Driver:
|
||||
"""Drives op scripts against the live facade the way the oracle was
|
||||
driven when the truth was frozen."""
|
||||
|
||||
def __init__(self, workdir: Path, n: int):
|
||||
self.path = str(workdir / f"ol_{n}.db")
|
||||
self.ledger = None
|
||||
workdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def step(self, op: dict):
|
||||
kind = op["op"]
|
||||
if kind == "new":
|
||||
self.ledger = OutcomeLedger(self.path)
|
||||
return {"result": None}
|
||||
if kind == "record":
|
||||
self.ledger.record(OutcomeRecord(**op["record"]))
|
||||
return {"result": None}
|
||||
if kind == "to_dict":
|
||||
return {"result": OutcomeRecord(**op["record"]).to_dict()}
|
||||
if kind == "get_stats":
|
||||
fam = op.get("family")
|
||||
return {"result": self.ledger.get_stats() if fam is None else self.ledger.get_stats(fam)}
|
||||
if kind == "get_model_stats":
|
||||
return {"result": self.ledger.get_model_stats(op["model"])}
|
||||
raise ValueError(f"unknown op {kind}")
|
||||
|
||||
def run(self, ops):
|
||||
return [self.step(op) for op in ops]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def truth():
|
||||
with open(_TRUTH) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _replay(truth, tmp_path, monkeypatch, pin_python: bool):
|
||||
if pin_python:
|
||||
monkeypatch.setenv("RUNE_OL_CORE", "python")
|
||||
else:
|
||||
monkeypatch.delenv("RUNE_OL_CORE", raising=False)
|
||||
for n, s in enumerate(truth["scenarios"]):
|
||||
d = Driver(tmp_path, n)
|
||||
got = d.run(s["ops"])
|
||||
assert len(got) == len(s["obs"]), s["name"]
|
||||
for i, (g, w) in enumerate(zip(got, s["obs"])):
|
||||
assert g == w, f"{s['name']} op#{i} ({s['ops'][i].get('op')}) diverged"
|
||||
|
||||
|
||||
def test_python_fallback_path_replays_frozen_truth(truth, tmp_path, monkeypatch):
|
||||
_replay(truth, tmp_path / "py", monkeypatch, pin_python=True)
|
||||
|
||||
|
||||
def test_rust_kernel_path_replays_frozen_truth(truth, tmp_path, monkeypatch):
|
||||
from rune import ol_loader
|
||||
if not ol_loader.available():
|
||||
pytest.skip("outcome_ledger kernel not built")
|
||||
_replay(truth, tmp_path / "rust", monkeypatch, pin_python=False)
|
||||
|
||||
|
||||
def test_cli_test_ledger_still_green(monkeypatch):
|
||||
# the facade-level self-test stays python (CLI + test_ledger are not
|
||||
# kernel surface) and must stay green with the kernel built too
|
||||
monkeypatch.delenv("RUNE_OL_CORE", raising=False)
|
||||
from rune import outcome_ledger as ol
|
||||
ol.test_ledger()
|
||||
Reference in new issue
Block a user