feat(subconscious_pass): port to FULL Rust — dichotomy wave

This commit is contained in:
Nick committed 2026-10-12 19:07:02 +01:00
1 parent 62c05b695c
commit 4369c1a54e
9 files changed
+1547 -33

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""Live parity: subconscious_pass Rust kernel vs Python oracle (dichotomy
wave). Layers: FROZEN replay (22 cases through oracle floor AND kernel),
LIVE facade-vs-oracle, FUZZ (600 seeded random turns, seed 2479).
`sp.datetime` is frozen so report timestamps and save filenames are
byte-comparable.
"""
import copy
import datetime as _dt
import json
import os
import random
import string
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "src"))
os.environ["RUNE_LEDGER_CORE"] = "python"
import rune.subconscious_pass as sp # noqa: E402
from rune import sp_loader # noqa: E402
FIXED_DT = _dt.datetime(2026, 10, 12, 0, 0, 1, 1, tzinfo=_dt.timezone.utc)
class _FixedDatetime(_dt.datetime):
@classmethod
def now(cls, tz=None):
return FIXED_DT
sp.datetime = _FixedDatetime
TRUTH = json.loads(
(REPO / "src" / "rust" / "subconscious_pass" / "src"
/ "frozen_sp_truth.json").read_text())
fails = []
counts = {"frozen": 0, "live": 0, "fuzz": 0}
WORDS = ("build fix cache index deploy planning drift frustrated confused "
"loader test guard topic refactor shipping agent task".split())
def rand_text(rnd, n=None):
n = n if n is not None else rnd.randint(1, 40)
txt = " ".join(rnd.choice(WORDS) for _ in range(n))
if rnd.random() < 0.2:
txt += "?"
return txt
def fail(layer, name, detail):
fails.append((layer, name, detail))
print(f"FAIL[{layer}] {name}: {detail}")
def frozen():
for group, cases in TRUTH.items():
if group in ("module", "now"):
continue
for c in cases:
counts["frozen"] += 1
args = copy.deepcopy(c["args"])
fn = c["fn"]
if fn == "reflect":
s = sp.SubconsciousPass()
want = c["expect"]
got_o = s.reflect(args["user"], args["agent"],
args.get("context"))
got_k = sp_loader.reflect(args["user"], args["agent"],
args.get("context"), args["now"])
elif fn == "extract_topic":
s = sp.SubconsciousPass()
want = c["expect"]
got_o = s._extract_topic(args["text"])
got_k = sp_loader.extract_topic(args["text"])
elif fn == "recent_findings":
s = sp.SubconsciousPass()
s.findings = list(args["reports"])
want = c["expect"]
got_o = s.get_recent_findings(args.get("limit", 10))
got_k = sp_loader.recent_findings(args["reports"],
args.get("limit", 10))
elif fn == "save_reflection":
want = c["expect"]
got_o = json.dumps(args["report"], indent=2)
got_k = sp_loader.save_report(args["report"])
else: # pragma: no cover
continue
for tag, got in (("oracle", got_o), ("kernel", got_k)):
if json.dumps(got, sort_keys=True) != json.dumps(
want, sort_keys=True):
fail("frozen", f"{fn}/{c['name']}/{tag}",
f"want={json.dumps(want, sort_keys=True)[:260]} "
f"got={json.dumps(got, sort_keys=True)[:260]}")
def compare(name, o, k, layer="live"):
counts[layer] += 1
jo, jk = json.dumps(o, sort_keys=True), json.dumps(k, sort_keys=True)
if jo != jk:
fail(layer, name, f"oracle={jo[:260]} kernel={jk[:260]}")
def run_both(user, agent, ctx, findings):
so = sp.SubconsciousPass()
sk = sp.SubconsciousPass()
so.findings = list(findings)
sk.findings = list(findings)
os.environ["RUNE_SP_CORE"] = "python"
o = {"reflect": so.reflect(user, agent, ctx),
"extract": so._extract_topic(user),
"recent10": so.get_recent_findings(),
"recent0": so.get_recent_findings(0),
"recent3": so.get_recent_findings(3),
"recent_neg2": so.get_recent_findings(-2)}
os.environ["RUNE_SP_CORE"] = "auto"
k = {"reflect": sk.reflect(user, agent, ctx),
"extract": sk._extract_topic(user),
"recent10": sk.get_recent_findings(),
"recent0": sk.get_recent_findings(0),
"recent3": sk.get_recent_findings(3),
"recent_neg2": sk.get_recent_findings(-2)}
return o, k
def live_and_fuzz():
rnd = random.Random(2479)
findings = [{"findings": [{"type": "a", "detail": "d", "severity": "i"}]},
{"findings": []},
{"other": 1}]
# live on the frozen inputs
for group, cases in TRUTH.items():
if group in ("module", "now") or group == "save_reflection_cases":
continue
for c in cases:
args = c["args"]
o, k = run_both(args.get("user", args.get("text", "")),
args.get("agent", "ok"),
args.get("context"), findings)
for key in o:
compare(f"{c['name']}/{key}", o[key], k[key])
# fuzz
with tempfile.TemporaryDirectory(prefix="sp_parity_") as tmp:
for i in range(600):
user = rand_text(rnd, rnd.choice([1, 3, 30, 45]))
agent = rand_text(rnd, rnd.choice([1, 5, 12]))
ctx = None
if rnd.random() < 0.6:
ctx = {}
if rnd.random() < 0.7:
ctx["previous_topic"] = rnd.choice(
["", "index tuning", rand_text(rnd, 4)])
o, k = run_both(user, agent, ctx, findings)
for key in o:
compare(f"fuzz{i}/{key}", o[key], k[key], layer="fuzz")
# save_reflection byte parity through the real oracle path
report = o["reflect"]
so = sp.SubconsciousPass()
sk = sp.SubconsciousPass()
orig_dir = sp.REFLECTIONS_DIR
try:
for tag, inst in (("o", so), ("k", sk)):
d = Path(tmp) / f"{i}{tag}"
d.mkdir()
sp.REFLECTIONS_DIR = str(d)
inst.save_reflection(report)
counts["fuzz"] += 1
fo = sorted((Path(tmp) / f"{i}o").iterdir())[0]
fk = sorted((Path(tmp) / f"{i}k").iterdir())[0]
if fo.name != fk.name or fo.read_text() != fk.read_text():
fail("fuzz", f"fuzz{i}/save", "file bytes differ")
finally:
sp.REFLECTIONS_DIR = orig_dir
def main():
if not sp_loader.available():
print("kernel not built — build src/rust/subconscious_pass first")
return 1
os.environ["RUNE_SP_CORE"] = "python"
frozen()
live_and_fuzz()
os.environ["RUNE_SP_CORE"] = "auto"
total = counts["frozen"] + counts["live"] + counts["fuzz"]
print(f"parity {total - len(fails)}/{total} "
f"(frozen {counts['frozen']}, live {counts['live']}, "
f"fuzz {counts['fuzz']})")
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Freeze the dichotomy-wave truth corpus for src/rune/subconscious_pass.py.
LAW (poisoned-truth): RUNE_SP_CORE=python is pinned at the top — truth is
generated by the PYTHON ORACLE ONLY.
Every public function is exercised across ALL branches:
reflect 10 cases (topic_shift hit/same-topic/no-context/
empty-context; unresolved_question both; action
words; emotional words; short_response; combo)
_extract_topic 4 (multiword / single / empty / whitespace)
get_recent_findings 4 (default limit / limit>len / limit=0 / sparse)
save_reflection 2 (the exact json.dump(indent=2) file bytes — the
timestamped FILENAME and the write stay in the
facade; the byte law is the kernel's)
Timestamps: the report `timestamp` and the save filename clock are facade
ARGUMENTS (`now`), monkeypatched to a FIXED value so the corpus is
byte-stable.
"""
import json
import os
import sys
from pathlib import Path
os.environ["RUNE_SP_CORE"] = "python" # poisoned-truth law
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "src"))
import rune.subconscious_pass as sp # noqa: E402
FIXED_NOW = "2026-10-12T00:00:01.000001+00:00"
OUT = (REPO / "src" / "rust" / "subconscious_pass" / "src"
/ "frozen_sp_truth.json")
cases = []
def case(fn, name, args, expect):
cases.append({"fn": fn, "name": name,
"args": args, "expect": expect})
class _FixedDatetime(sp.datetime):
@classmethod
def now(cls, tz=None):
return cls(2026, 10, 12, 0, 0, 1, 1, tzinfo=tz)
def main():
sp.datetime = _FixedDatetime
s = sp.SubconsciousPass()
# ── reflect ──────────────────────────────────────────────────────
for name, user, agent, ctx in [
("topic_shift_hit", "how do we deploy the cache layer",
"Sure, let's plan it.", {"previous_topic": "index tuning"}),
("topic_same", "more on index tuning please",
"The index tuning continues.", {"previous_topic": "index tuning"}),
("topic_none_extracted", " ",
"Empty message.", {"previous_topic": "old topic"}),
("no_context", "build the thing", "Building it.", None),
("empty_context", "build the thing", "Building it.", {}),
("unresolved_question", "What about the cache?",
"The cache is handled.", {"previous_topic": "cache"}),
("question_answered", "What about the cache?",
"Good question — the cache is handled.", {}),
("action_requested", "please fix and update the loader",
"On it.", {}),
("emotional_signal", "I'm frustrated and confused by the drift",
"Let's look at it.", {}),
("short_response", "x" * 201,
"ok"[:10], {}),
("combo_all", "build and fix it — I'm frustrated. What now?",
"ok", {"previous_topic": "planning"}),
]:
r = s.reflect(user, agent, ctx)
case("reflect", name,
{"user": user, "agent": agent, "context": ctx,
"now": FIXED_NOW}, r)
# ── _extract_topic ───────────────────────────────────────────────
for name, text, want in [
("multiword", "the quick brown fox jumps over the dog",
"the quick brown fox jumps"),
("single", "hello", "hello"),
("empty", "", None),
("whitespace", " \t\n ", None),
]:
got = s._extract_topic(text)
case("extract_topic", name, {"text": text}, got)
# ── get_recent_findings ──────────────────────────────────────────
s2 = sp.SubconsciousPass()
r1 = s2.reflect("build it", "ok" * 30, {})
r2 = s2.reflect("What?", "Nothing.", {})
r3 = s2.reflect("no findings here", "x" * 60, {})
r4 = s2.reflect("frustrated", "y" * 60, {})
case("recent_findings", "default_limit",
{"reports": [r1, r2, r3, r4]}, s2.get_recent_findings())
case("recent_findings", "limit_2",
{"reports": [r1, r2, r3, r4], "limit": 2}, s2.get_recent_findings(2))
case("recent_findings", "limit_0",
{"reports": [r1, r2, r3, r4], "limit": 0}, s2.get_recent_findings(0))
sparse = [{"findings": [{"type": "a", "detail": "d", "severity": "low"}]},
{"no_findings_key": True},
{"findings": []}]
s5 = sp.SubconsciousPass()
s5.findings = list(sparse)
case("recent_findings", "sparse_reports",
{"reports": sparse}, s5.get_recent_findings(10))
case("recent_findings", "empty",
{"reports": []}, _recent([], 10))
# ── save_reflection (exact file bytes; naming/write is facade) ───
for i, report in enumerate([r1, {"a": 1, "list": [1, 2.5], "s": "é"}]):
s3 = sp.SubconsciousPass()
# call the real oracle save path with a redirected dir
orig_dir = sp.REFLECTIONS_DIR
tmp = Path(__import__("tempfile").mkdtemp(prefix="sp_truth_"))
sp.REFLECTIONS_DIR = str(tmp)
try:
s3.save_reflection(report)
files = sorted(tmp.iterdir())
assert len(files) == 1
content = files[0].read_text()
finally:
sp.REFLECTIONS_DIR = orig_dir
import shutil
shutil.rmtree(tmp, ignore_errors=True)
# the frozen corpus round-trips the report sort_keys=True, so the
# replayable byte law is json.dump over THAT dict order
sorted_report = json.loads(json.dumps(report, sort_keys=True))
case("save_reflection", f"report{i}", {"report": sorted_report},
json.dumps(sorted_report, indent=2))
OUT.parent.mkdir(parents=True, exist_ok=True)
groups = {}
for c in cases:
groups.setdefault(f"{c['fn']}_cases", []).append(c)
OUT.write_text(json.dumps(
{"module": "subconscious_pass", "now": FIXED_NOW, **groups},
indent=1, sort_keys=True) + "\n")
print(f"frozen {len(cases)} cases -> {OUT}")
def _recent(reports, limit):
all_findings = []
for r in reports[-limit:]:
all_findings.extend(r.get("findings", []))
return all_findings
if __name__ == "__main__":
main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust subconscious_pass core (dichotomy wave,
#2479 tranche). Mirrors broker_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* sp_reflect({user, agent, context?, now}) -> report dict
* sp_extract_topic({text}) -> str | null
* sp_recent_findings({reports, limit?}) -> [findings]
* sp_save_report({report}) -> json.dump(indent=2) bytes
The report timestamp and save filename clock are facade ARGUMENTS (now).
Free: capability_scoring's cs_free_string (shared process). Override the
.so with SP_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" / "subconscious_pass"
_LIB_PATH = Path(
os.environ.get("SP_RUST_LIB", _CRATE_DIR / "target" / "release" / "libsubconscious_pass.so")
)
_ERR_TYPES = {
"ValueError": ValueError,
"KeyError": KeyError,
"AttributeError": AttributeError,
"TypeError": TypeError,
}
_lib = None
class SubconsciousCoreUnavailable(FileNotFoundError):
"""The Rust subconscious-pass 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 SubconsciousCoreUnavailable(str(_LIB_PATH))
lib = ctypes.CDLL(str(_LIB_PATH))
for sym in ("sp_reflect", "sp_extract_topic", "sp_recent_findings",
"sp_save_report"):
getattr(lib, sym).argtypes = [ctypes.c_void_p]
getattr(lib, sym).restype = ctypes.c_void_p
try:
lib.cs_free_string
except AttributeError:
scoring = ctypes.CDLL(str(
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
))
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
lib._free = scoring.cs_free_string
_lib = lib
return lib
def _call(sym: str, payload: dict):
lib = _load()
data = json.dumps(payload).encode()
buf = ctypes.create_string_buffer(data)
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
if not ptr:
raise RuntimeError(f"{sym} returned null")
out = ctypes.string_at(ptr).decode()
free = getattr(lib, "_free", None)
if free is not None:
free(ptr)
doc = json.loads(out)
if not doc.get("ok"):
etype = doc.get("type", "RuntimeError")
raise _ERR_TYPES.get(etype, RuntimeError)(doc.get("err", f"{sym} failed"))
return doc["value"]
def reflect(user: str, agent: str, context, now: str) -> dict:
return _call("sp_reflect", {"user": user, "agent": agent,
"context": context, "now": now})
def extract_topic(text: str):
return _call("sp_extract_topic", {"text": text})
def recent_findings(reports: list, limit: int = 10) -> list:
return _call("sp_recent_findings", {"reports": reports, "limit": limit})
def save_report(report: dict) -> str:
return _call("sp_save_report", {"report": report})
+50 -1
View File
@@ -38,6 +38,22 @@ class SubconsciousPass:
os.makedirs(REFLECTIONS_DIR, exist_ok=True)
self.findings: List[dict] = []
@staticmethod
def _kernel():
"""The Rust subconscious-pass core (sp_loader) when RUNE_SP_CORE
allows it, else None — public functions delegate there first and
fall back to the Python body below (same semantics)."""
if os.environ.get("RUNE_SP_CORE", "auto") == "python":
return None
try:
from rune import sp_loader
if sp_loader.available():
return sp_loader
except Exception: # noqa: BLE001 — kernel failure → Python floor
return None
return None
def reflect(self, user_message: str, agent_response: str, context: dict = None) -> dict:
"""Run a post-turn reflection.
@@ -49,6 +65,16 @@ class SubconsciousPass:
Returns a reflection report.
"""
k = self._kernel()
if k is not None:
try:
report = k.reflect(
user_message, agent_response, context,
datetime.now(timezone.utc).isoformat())
self.findings.append(report)
return report
except Exception: # noqa: BLE001 — kernel failure → Python floor
pass
findings = []
# Check for rapid topic shifts
@@ -120,19 +146,42 @@ class SubconsciousPass:
def _extract_topic(self, text: str) -> Optional[str]:
"""Extract a rough topic from text (first noun phrase)."""
k = self._kernel()
if k is not None:
try:
return k.extract_topic(text)
except Exception: # noqa: BLE001
pass
# Simple heuristic: first few words
words = text.split()[:5]
return " ".join(words) if words else None
def save_reflection(self, report: dict):
"""Save a reflection report to disk."""
k = self._kernel()
if k is not None:
try:
content = k.save_report(report)
except Exception: # noqa: BLE001
content = None
else:
content = None
filename = f"reflection_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = os.path.join(REFLECTIONS_DIR, filename)
with open(filepath, "w") as f:
json.dump(report, f, indent=2)
if content is not None:
f.write(content)
else:
json.dump(report, f, indent=2)
def get_recent_findings(self, limit: int = 10) -> List[dict]:
"""Get recent findings across reflections."""
k = self._kernel()
if k is not None:
try:
return k.recent_findings(self.findings, limit)
except Exception: # noqa: BLE001
pass
all_findings = []
for r in self.findings[-limit:]:
all_findings.extend(r.get("findings", []))
+29 -1
View File
@@ -2,6 +2,32 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "evidence_provenance"
version = "0.1.0"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -67,6 +93,7 @@ version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"indexmap",
"itoa",
"memchr",
"serde",
@@ -76,8 +103,9 @@ dependencies = [
[[package]]
name = "subconscious_pass"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"evidence_provenance",
"serde_json",
]
+4 -2
View File
@@ -1,10 +1,12 @@
[package]
name = "subconscious_pass"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
description = "FULL parity port of src/rune/subconscious_pass.py (dichotomy wave, #2479 tranche)"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
serde_json = { version = "1", features = ["preserve_order", "float_roundtrip"] }
evidence_provenance = { path = "../evidence_provenance" }
@@ -0,0 +1,160 @@
//! branch_battery — replay the FROZEN oracle truth VERBATIM
//! (frozen_sp_truth.json, generated by scripts/gen_subconscious_pass_truth.py
//! against the LIVE Python oracle with RUNE_SP_CORE=python pinned).
use crate::{extract_topic, reflect, recent_findings, save_report};
use evidence_provenance::pyjson::{self, Json};
const TRUTH: &str = include_str!("frozen_sp_truth.json");
fn truth() -> Json {
pyjson::parse(TRUTH).expect("frozen truth parses")
}
fn cases(t: &Json, group: &str) -> Vec<Json> {
match t.get(group) {
Some(Json::Arr(items)) => items.clone(),
_ => panic!("truth group {group} missing"),
}
}
fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
let mut cur = v;
for k in path {
cur = match cur {
Json::Obj(pairs) => pairs
.iter()
.find(|(k2, _)| k2 == k)
.map(|(_, x)| x)?,
_ => return None,
};
}
Some(cur)
}
fn jstr(v: Option<&Json>) -> String {
match v {
Some(Json::Str(s)) => s.clone(),
_ => String::new(),
}
}
fn jnum(v: Option<&Json>, default: f64) -> f64 {
match v {
Some(Json::Num(n)) => *n,
_ => default,
}
}
/// Python-value equality on Json (num compares by f64).
fn peq(a: &Json, b: &Json) -> bool {
match (a, b) {
(Json::Null, Json::Null) => true,
(Json::Bool(x), Json::Bool(y)) => x == y,
(Json::Num(x), Json::Num(y)) => x == y,
(Json::Str(x), Json::Str(y)) => x == y,
(Json::Arr(x), Json::Arr(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(a, b)| peq(a, b))
}
(Json::Obj(x), Json::Obj(y)) => {
x.len() == y.len()
&& x.iter().all(|(k, v)| {
y.iter()
.find(|(k2, _)| k2 == k)
.map(|(_, v2)| peq(v, v2))
.unwrap_or(false)
})
}
_ => false,
}
}
fn assert_eq(case_name: &str, want: &Json, got: &Json) {
assert!(
peq(want, got),
"case {case_name} diverged:\n want={}\n got={}",
pyjson::dumps(want),
pyjson::dumps(got),
);
}
#[test]
fn replay_reflect() {
let t = truth();
for c in cases(&t, "reflect_cases") {
let got = reflect(
&jstr(at(&c, &["args", "user"])),
&jstr(at(&c, &["args", "agent"])),
at(&c, &["args", "context"]).unwrap_or(&Json::Null),
&jstr(at(&c, &["args", "now"])),
);
assert_eq(
&format!("reflect/{}", jstr(at(&c, &["name"]))),
at(&c, &["expect"]).expect("expect"),
&got,
);
}
}
#[test]
fn replay_extract_topic() {
let t = truth();
for c in cases(&t, "extract_topic_cases") {
let text = jstr(at(&c, &["args", "text"]));
let got = match extract_topic(&text) {
Some(x) => Json::Str(x),
None => Json::Null,
};
assert_eq(
&format!("extract_topic/{}", jstr(at(&c, &["name"]))),
at(&c, &["expect"]).expect("expect"),
&got,
);
}
}
#[test]
fn replay_recent_findings() {
let t = truth();
for c in cases(&t, "recent_findings_cases") {
let got = recent_findings(
at(&c, &["args", "reports"]).expect("reports"),
jnum(at(&c, &["args", "limit"]), 10.0) as i64,
);
assert_eq(
&format!("recent_findings/{}", jstr(at(&c, &["name"]))),
at(&c, &["expect"]).expect("expect"),
&got,
);
}
}
#[test]
fn replay_save_report() {
let t = truth();
for c in cases(&t, "save_reflection_cases") {
let got = save_report(at(&c, &["args", "report"]).expect("report"));
assert_eq(
&format!("save_reflection/{}", jstr(at(&c, &["name"]))),
at(&c, &["expect"]).expect("expect"),
&Json::Str(got),
);
}
}
#[test]
fn topic_extracted_is_the_public_law() {
// the reflect → _extract_topic coupling, exercised directly
let got = reflect("deploy the cache", "sure", &truth(), "now");
// context here is the truth doc (an object without previous_topic):
// no topic_shift may fire
match got.get("findings") {
Some(Json::Arr(items)) => assert!(
!items.iter().any(|f| {
matches!(f.get("type"), Some(Json::Str(x)) if x == "topic_shift")
}),
"no previous_topic → no topic_shift"
),
_ => panic!("findings"),
}
}
@@ -0,0 +1,599 @@
{
"extract_topic_cases": [
{
"args": {
"text": "the quick brown fox jumps over the dog"
},
"expect": "the quick brown fox jumps",
"fn": "extract_topic",
"name": "multiword"
},
{
"args": {
"text": "hello"
},
"expect": "hello",
"fn": "extract_topic",
"name": "single"
},
{
"args": {
"text": ""
},
"expect": null,
"fn": "extract_topic",
"name": "empty"
},
{
"args": {
"text": " \t\n "
},
"expect": null,
"fn": "extract_topic",
"name": "whitespace"
}
],
"module": "subconscious_pass",
"now": "2026-10-12T00:00:01.000001+00:00",
"recent_findings_cases": [
{
"args": {
"reports": [
{
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 8
},
{
"agent_response_length": 8,
"finding_count": 1,
"findings": [
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 5
},
{
"agent_response_length": 60,
"finding_count": 0,
"findings": [],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 16
},
{
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 10
}
]
},
"expect": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
},
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
},
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"fn": "recent_findings",
"name": "default_limit"
},
{
"args": {
"limit": 2,
"reports": [
{
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 8
},
{
"agent_response_length": 8,
"finding_count": 1,
"findings": [
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 5
},
{
"agent_response_length": 60,
"finding_count": 0,
"findings": [],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 16
},
{
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 10
}
]
},
"expect": [
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"fn": "recent_findings",
"name": "limit_2"
},
{
"args": {
"limit": 0,
"reports": [
{
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 8
},
{
"agent_response_length": 8,
"finding_count": 1,
"findings": [
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 5
},
{
"agent_response_length": 60,
"finding_count": 0,
"findings": [],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 16
},
{
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 10
}
]
},
"expect": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
},
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
},
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"fn": "recent_findings",
"name": "limit_0"
},
{
"args": {
"reports": [
{
"findings": [
{
"detail": "d",
"severity": "low",
"type": "a"
}
]
},
{
"no_findings_key": true
},
{
"findings": []
}
]
},
"expect": [
{
"detail": "d",
"severity": "low",
"type": "a"
}
],
"fn": "recent_findings",
"name": "sparse_reports"
},
{
"args": {
"reports": []
},
"expect": [],
"fn": "recent_findings",
"name": "empty"
}
],
"reflect_cases": [
{
"args": {
"agent": "Sure, let's plan it.",
"context": {
"previous_topic": "index tuning"
},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "how do we deploy the cache layer"
},
"expect": {
"agent_response_length": 20,
"finding_count": 1,
"findings": [
{
"detail": "Topic shifted from 'index tuning' to 'how do we deploy the'",
"severity": "info",
"type": "topic_shift"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 32
},
"fn": "reflect",
"name": "topic_shift_hit"
},
{
"args": {
"agent": "The index tuning continues.",
"context": {
"previous_topic": "index tuning"
},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "more on index tuning please"
},
"expect": {
"agent_response_length": 27,
"finding_count": 1,
"findings": [
{
"detail": "Topic shifted from 'index tuning' to 'more on index tuning please'",
"severity": "info",
"type": "topic_shift"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 27
},
"fn": "reflect",
"name": "topic_same"
},
{
"args": {
"agent": "Empty message.",
"context": {
"previous_topic": "old topic"
},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": " "
},
"expect": {
"agent_response_length": 14,
"finding_count": 0,
"findings": [],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 3
},
"fn": "reflect",
"name": "topic_none_extracted"
},
{
"args": {
"agent": "Building it.",
"context": null,
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "build the thing"
},
"expect": {
"agent_response_length": 12,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 15
},
"fn": "reflect",
"name": "no_context"
},
{
"args": {
"agent": "Building it.",
"context": {},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "build the thing"
},
"expect": {
"agent_response_length": 12,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 15
},
"fn": "reflect",
"name": "empty_context"
},
{
"args": {
"agent": "The cache is handled.",
"context": {
"previous_topic": "cache"
},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "What about the cache?"
},
"expect": {
"agent_response_length": 21,
"finding_count": 2,
"findings": [
{
"detail": "Topic shifted from 'cache' to 'What about the cache?'",
"severity": "info",
"type": "topic_shift"
},
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 21
},
"fn": "reflect",
"name": "unresolved_question"
},
{
"args": {
"agent": "Good question \u2014 the cache is handled.",
"context": {},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "What about the cache?"
},
"expect": {
"agent_response_length": 37,
"finding_count": 1,
"findings": [
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 21
},
"fn": "reflect",
"name": "question_answered"
},
{
"args": {
"agent": "On it.",
"context": {},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "please fix and update the loader"
},
"expect": {
"agent_response_length": 6,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: fix, update",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 32
},
"fn": "reflect",
"name": "action_requested"
},
{
"args": {
"agent": "Let's look at it.",
"context": {},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "I'm frustrated and confused by the drift"
},
"expect": {
"agent_response_length": 17,
"finding_count": 1,
"findings": [
{
"detail": "User expressed: frustrated, confused",
"severity": "info",
"type": "emotional_signal"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 40
},
"fn": "reflect",
"name": "emotional_signal"
},
{
"args": {
"agent": "ok",
"context": {},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"expect": {
"agent_response_length": 2,
"finding_count": 1,
"findings": [
{
"detail": "Response is very short relative to user message \u2014 may have been truncated or incomplete",
"severity": "low",
"type": "short_response"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 201
},
"fn": "reflect",
"name": "short_response"
},
{
"args": {
"agent": "ok",
"context": {
"previous_topic": "planning"
},
"now": "2026-10-12T00:00:01.000001+00:00",
"user": "build and fix it \u2014 I'm frustrated. What now?"
},
"expect": {
"agent_response_length": 2,
"finding_count": 4,
"findings": [
{
"detail": "Topic shifted from 'planning' to 'build and fix it \u2014'",
"severity": "info",
"type": "topic_shift"
},
{
"detail": "User asked a question but response doesn't contain a question mark \u2014 may have been answered or may have been missed",
"severity": "low",
"type": "unresolved_question"
},
{
"detail": "User requested actions: build, fix",
"severity": "info",
"type": "action_requested"
},
{
"detail": "User expressed: frustrated",
"severity": "info",
"type": "emotional_signal"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 44
},
"fn": "reflect",
"name": "combo_all"
}
],
"save_reflection_cases": [
{
"args": {
"report": {
"agent_response_length": 60,
"finding_count": 1,
"findings": [
{
"detail": "User requested actions: build",
"severity": "info",
"type": "action_requested"
}
],
"timestamp": "2026-10-12T00:00:01.000001+00:00",
"user_message_length": 8
}
},
"expect": "{\n \"agent_response_length\": 60,\n \"finding_count\": 1,\n \"findings\": [\n {\n \"detail\": \"User requested actions: build\",\n \"severity\": \"info\",\n \"type\": \"action_requested\"\n }\n ],\n \"timestamp\": \"2026-10-12T00:00:01.000001+00:00\",\n \"user_message_length\": 8\n}",
"fn": "save_reflection",
"name": "report0"
},
{
"args": {
"report": {
"a": 1,
"list": [
1,
2.5
],
"s": "\u00e9"
}
},
"expect": "{\n \"a\": 1,\n \"list\": [\n 1,\n 2.5\n ],\n \"s\": \"\\u00e9\"\n}",
"fn": "save_reflection",
"name": "report1"
}
]
}
+250 -29
View File
@@ -1,37 +1,258 @@
//! subconscious_pass — post-turn reflection (W6).
//! PARITY port of src/rune/subconscious_pass.py reflect.
pub fn reflect(
user_message: &str,
agent_response: &str,
previous_topic: Option<&str>,
) -> Vec<serde_json::Value> {
let mut findings = Vec::new();
if let Some(prev) = previous_topic {
let current = extract_topic(user_message);
if !current.is_empty() && !prev.contains(&current) {
findings.push(serde_json::json!({"type": "topic_shift", "detail": format!("topic shifted from {} to {}", prev, current)}));
//! subconscious_pass — post-turn reflection inspired by Souveraine.
//!
//! FULL parity port of src/rune/subconscious_pass.py (dichotomy wave,
//! #2479 tranche). The oracle is a stateful class; over FFI the stateless
//! law is the kernel's (reflect/extract_topic/recent_findings/save bytes)
//! and the instance state (self.findings) + filesystem stay in the facade.
//!
//! FFI (doc-envelope law): every call takes one JSON doc and returns
//! {"ok": true, "value": ...} | {"ok": false, "err": ..., "type": ...}.
//! * sp_reflect({user, agent, context?, now}) -> report dict
//! * sp_extract_topic({text}) -> str | null
//! * sp_recent_findings({reports, limit?}) -> [findings]
//! * sp_save_report({report}) -> json.dump(indent=2) bytes
//!
//! Determinism seams: the report `timestamp` (datetime.now(utc).isoformat)
//! and the save FILENAME clock are facade ARGUMENTS (`now`) — the
//! frozen-truth battery replays them verbatim. The makedirs + file write
//! stay in the facade: filesystem is Python's job, byte law is the
//! kernel's.
//!
//! NOTE: no sp_free_string export — callers free through capability_
//! scoring's cs_free_string (shared process allocator).
use evidence_provenance::pyjson::{self, Json};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
const ACTION_WORDS: [&str; 8] = [
"build", "create", "fix", "update", "change", "add", "remove", "delete",
];
const EMOTIONAL_WORDS: [&str; 7] = [
"frustrated", "confused", "happy", "annoyed", "excited", "worried",
"scared",
];
fn obj(pairs: Vec<(&str, Json)>) -> Json {
Json::Obj(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
fn s(v: &Json, key: &str) -> String {
match v.get(key) {
Some(Json::Str(x)) => x.clone(),
_ => String::new(),
}
}
fn finding(kind: &str, detail: String, severity: &str) -> Json {
obj(vec![
("type", Json::Str(kind.to_string())),
("detail", Json::Str(detail)),
("severity", Json::Str(severity.to_string())),
])
}
/// Python str length is CHARS, not bytes.
fn pylen(x: &str) -> f64 {
x.chars().count() as f64
}
fn extract_topic(text: &str) -> Option<String> {
let words: Vec<&str> = text.split_whitespace().take(5).collect();
if words.is_empty() {
None
} else {
Some(words.join(" "))
}
}
pub fn reflect(user_message: &str, agent_response: &str,
context: &Json, now: &str) -> Json {
let mut findings: Vec<Json> = Vec::new();
// if context and context.get("previous_topic") — {} and "" are falsy
if let Json::Obj(pairs) = context {
if !pairs.is_empty() {
if let Some(Json::Str(prev)) = context.get("previous_topic") {
if !prev.is_empty() {
let current = extract_topic(user_message);
if let Some(cur) = current {
if cur != *prev {
findings.push(finding(
"topic_shift",
format!(
"Topic shifted from '{}' to '{}'",
prev, cur
),
"info",
));
}
}
}
}
}
}
if agent_response.len() < 50 {
findings.push(
serde_json::json!({"type": "brief_response", "detail": "response was unusually short"}),
);
if user_message.contains('?') && !agent_response.contains('?') {
findings.push(finding(
"unresolved_question",
"User asked a question but response doesn't contain a question \
mark — may have been answered or may have been missed"
.to_string(),
"low",
));
}
findings
let user_lower = user_message.to_lowercase();
let user_actions: Vec<&str> = ACTION_WORDS
.iter()
.copied()
.filter(|w| user_lower.contains(w))
.collect();
if !user_actions.is_empty() {
findings.push(finding(
"action_requested",
format!("User requested actions: {}", user_actions.join(", ")),
"info",
));
}
let user_emotions: Vec<&str> = EMOTIONAL_WORDS
.iter()
.copied()
.filter(|w| user_lower.contains(w))
.collect();
if !user_emotions.is_empty() {
findings.push(finding(
"emotional_signal",
format!("User expressed: {}", user_emotions.join(", ")),
"info",
));
}
if pylen(agent_response) < 50.0 && pylen(user_message) > 200.0 {
findings.push(finding(
"short_response",
"Response is very short relative to user message — may have \
been truncated or incomplete"
.to_string(),
"low",
));
}
let count = findings.len() as f64;
obj(vec![
("timestamp", Json::Str(now.to_string())),
("user_message_length", Json::Num(pylen(user_message))),
("agent_response_length", Json::Num(pylen(agent_response))),
("findings", Json::Arr(findings)),
("finding_count", Json::Num(count)),
])
}
fn extract_topic(text: &str) -> String {
let words: Vec<&str> = text.split_whitespace().take(5).collect();
words.join(" ")
pub fn recent_findings(reports: &Json, limit: i64) -> Json {
let items: Vec<Json> = match reports {
Json::Arr(items) => items.clone(),
_ => Vec::new(),
};
// Python r[-limit:] slice law: limit>0 → last `limit`; limit==0 →
// the WHOLE list ([-0:] == [0:]); limit<0 → drop first |limit|.
let len = items.len() as i64;
let start = if limit > 0 {
(len - limit).max(0)
} else if limit == 0 {
0
} else {
(-limit).min(len).max(0)
};
let mut out: Vec<Json> = Vec::new();
for r in &items[start as usize..] {
if let Some(Json::Arr(f)) = r.get("findings") {
out.extend(f.iter().cloned());
}
}
Json::Arr(out)
}
/// The exact json.dump(report, indent=2) bytes (no trailing newline).
pub fn save_report(report: &Json) -> String {
pyjson::dump_indent2(report)
}
// ── FFI (doc-envelope) ───────────────────────────────────────────────────
fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
let body = match result {
Ok(value) => obj(vec![("ok", Json::Bool(true)), ("value", value)]),
Err((err, ty)) => obj(vec![
("ok", Json::Bool(false)),
("err", Json::Str(err)),
("type", Json::Str(ty)),
]),
};
let out = pyjson::dumps(&body);
match CString::new(out) {
Ok(c) => c.into_raw(),
Err(_) => CString::new(
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
)
.unwrap()
.into_raw(),
}
}
fn parse_doc(raw: &str) -> Result<Json, (String, String)> {
pyjson::parse(raw).map_err(|e| (e, "ValueError".to_string()))
}
fn read_arg(ptr: *const c_char) -> Result<String, (String, String)> {
if ptr.is_null() {
return Ok(String::new());
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.map(|s| s.to_string())
.map_err(|e| (e.to_string(), "ValueError".to_string()))
}
macro_rules! ffi {
($name:ident, $body:expr) => {
#[no_mangle]
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
let out: Result<Json, (String, String)> =
read_arg(arg).and_then(|raw| $body(&raw));
envelope(out)
}
};
}
ffi!(sp_reflect, |raw: &str| -> Result<Json, (String, String)> {
let doc = parse_doc(raw)?;
let user = s(&doc, "user");
let agent = s(&doc, "agent");
let context = doc.get("context").cloned().unwrap_or(Json::Null);
let now = s(&doc, "now");
Ok(reflect(&user, &agent, &context, &now))
});
ffi!(sp_extract_topic, |raw: &str| -> Result<Json, (String, String)> {
let doc = parse_doc(raw)?;
let text = s(&doc, "text");
Ok(match extract_topic(&text) {
Some(t) => Json::Str(t),
None => Json::Null,
})
});
ffi!(sp_recent_findings, |raw: &str| -> Result<Json, (String, String)> {
let doc = parse_doc(raw)?;
let reports = doc.get("reports").cloned().unwrap_or(Json::Arr(vec![]));
let limit = match doc.get("limit") {
Some(Json::Num(n)) => *n as i64,
_ => 10,
};
Ok(recent_findings(&reports, limit))
});
ffi!(sp_save_report, |raw: &str| -> Result<Json, (String, String)> {
let doc = parse_doc(raw)?;
let report = doc.get("report").cloned().unwrap_or(Json::Null);
Ok(Json::Str(save_report(&report)))
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_topic_shift() {
let findings = reflect("new topic about cooking", "response", Some("deployment"));
assert!(findings.iter().any(|f| f["type"] == "topic_shift"));
}
}
mod branch_battery;