feat(subconscious_pass): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
62c05b695c
commit
4369c1a54e
9 files changed
+1547
-33
No files matched your search
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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})
|
||||
@@ -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", []))
|
||||
|
||||
Generated
+29
-1
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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(¤t) {
|
||||
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;
|
||||
Reference in new issue
Block a user