Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3e1e3113 | ||
|
|
f11006c457 | ||
|
|
b8b1ba66ad | ||
|
|
7807e7eea5 | ||
|
|
c42936b959 | ||
|
|
681312c69f | ||
|
|
d7a5a99bab | ||
|
|
cef1e953ea | ||
|
|
d4c9ce560f | ||
|
|
64911ab782 | ||
|
|
8286361804 | ||
|
|
6be2b0a94a | ||
|
|
efee6c5784 | ||
|
|
819525c602 | ||
|
|
d1efed0eee |
No files matched your search
@@ -294,6 +294,10 @@ jobs:
|
||||
run: |
|
||||
cd src/go/message-queue && go build -o /tmp/message-queue-${{ gitea.run_id }} .
|
||||
/tmp/message-queue-${{ gitea.run_id }} replay truth.json
|
||||
- name: message_queue parity gate (HARD — #2479 dichotomy wave: FULL port, 55 frozen rows)
|
||||
run: |
|
||||
cd src/go/message_queue && go test ./... -race -count=1
|
||||
cd src/go/message_queue && go build -o /tmp/message_queue-${{ gitea.run_id }} . && /tmp/message_queue-${{ gitea.run_id }} replay truth.json
|
||||
- name: outcome-ledger parity gate (HARD — #2479 batches 49+151: stats + live fixture)
|
||||
run: |
|
||||
cd src/go/outcome-ledger && go build -o /tmp/outcome-ledger-${{ gitea.run_id }} .
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.206.0**
|
||||
**Current release: v0.210.0**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.206.0"
|
||||
version = "0.210.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: Rust build_basis kernel vs the Python oracle (dichotomy
|
||||
wave #2479). Replays the frozen truth corpus PLUS seeded fuzz over every
|
||||
public function and branch. Timestamps the kernel generates are masked
|
||||
on both sides (wall clock, not logic).
|
||||
|
||||
The facade reads RUNE_BB_CORE per call, so one process can flip between
|
||||
the pinned oracle (RUNE_BB_CORE=python) and the kernel (unset).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
MASKED = ("decided_at", "created_at", "revised_at", "plan_generated_at")
|
||||
|
||||
|
||||
def mask(doc):
|
||||
if isinstance(doc, dict):
|
||||
return {k: mask(v) for k, v in doc.items() if k not in MASKED}
|
||||
if isinstance(doc, list):
|
||||
return [mask(v) for v in doc]
|
||||
return doc
|
||||
|
||||
|
||||
def call(fn, *args, **kwargs):
|
||||
"""Call through the facade under the given core pin."""
|
||||
return getattr(bb, fn)(*args, **kwargs)
|
||||
|
||||
|
||||
def call_core(core, fn, *args, **kwargs):
|
||||
old = os.environ.get("RUNE_BB_CORE")
|
||||
os.environ["RUNE_BB_CORE"] = core
|
||||
try:
|
||||
return call(fn, *args, **kwargs)
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_BB_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_BB_CORE"] = old
|
||||
|
||||
|
||||
def mutator_case(fn, basis_in, *args, **kwargs):
|
||||
"""Run a mutating function on both cores over deep copies; compare
|
||||
(return value, post-mutation basis) with time fields masked."""
|
||||
py = call_core("python", fn, copy.deepcopy(basis_in), *args, **kwargs)
|
||||
rs = call_core("rust", fn, copy.deepcopy(basis_in), *args, **kwargs)
|
||||
ok = (mask(py) == mask(rs)
|
||||
and mask(basis_in_py := None or basis_in) is not None)
|
||||
return ok, py, rs
|
||||
|
||||
|
||||
# ── fuzz pools ─────────────────────────────────────────────────────────
|
||||
ROUTES = ["LOCAL_FIX", "STANDARD_CHANGE", "RESEARCHED_CHANGE", "WEIRD",
|
||||
"", "RESEARCHED_CHANGE ", "local_fix", "SPIKE"]
|
||||
TIERS = ["NONE", "INWARD", "OUTWARD", "FULL", "WEIRD", None]
|
||||
JUSTS = ["", " ", "tiny fix", "OUTWARD: precedent needed",
|
||||
"FULL: x:y", " INWARD ", "resolved already",
|
||||
"не-mátter 🎉", "prior-art question answered elsewhere",
|
||||
"NONE: skip", "discovery resolved it", "unnecessary"]
|
||||
WORDS = ["existing", "prior art", "framework", "typo", "schema",
|
||||
"architecture", "watchdog", "delete", "", "u" * 90, "café ☕",
|
||||
"tool", "standard", "breaking", "deploy"]
|
||||
DISPOSITIONS = ["ADOPT", "ADAPT", "COMBINE", "REJECT", "INNOVATE", "MAYBE", "", None]
|
||||
|
||||
|
||||
def rand_basis(rng):
|
||||
b = bb.new_basis(question=rng.choice(WORDS),
|
||||
depth_decision=bb.research_depth(rng.choice(ROUTES),
|
||||
justification=rng.choice(JUSTS)),
|
||||
issue_ref=rng.choice(["research/issue-1", "", "gitea/2479"]))
|
||||
b["created_at"] = "2026-10-11T00:00:00Z"
|
||||
return b
|
||||
|
||||
|
||||
def fuzz_cases(rng):
|
||||
out = []
|
||||
for _ in range(120):
|
||||
out.append(("research_depth",
|
||||
(rng.choice(ROUTES), rng.choice(["", "rune-internal", "cli"]),
|
||||
rng.choice(JUSTS)), {}))
|
||||
for _ in range(60):
|
||||
d = bb.research_depth(rng.choice(ROUTES), justification=rng.choice(JUSTS))
|
||||
d["decided_at"] = "FIXED"
|
||||
out.append(("new_basis", (rng.choice(WORDS), d,
|
||||
rng.choice(["research/x", ""])), {}))
|
||||
for _ in range(120):
|
||||
disp = [{"item": rng.choice(WORDS), "disposition": rng.choice(DISPOSITIONS),
|
||||
"why": rng.choice(JUSTS), "ref": rng.choice(["r1", ""]) }
|
||||
for _ in range(rng.randrange(0, 4))]
|
||||
out.append(("set_dispositions", (None,), {"basis": None, "dispositions": disp}))
|
||||
for _ in range(120):
|
||||
b = rand_basis(rng)
|
||||
if rng.random() < 0.5:
|
||||
b["schema_version"] = rng.choice([1, 2, None, True])
|
||||
if rng.random() < 0.4:
|
||||
b["depth"] = rng.choice([{"depth": rng.choice(TIERS), "why": rng.choice(["", "w"])},
|
||||
{}, None])
|
||||
if rng.random() < 0.3:
|
||||
b["dispositions"] = [{"item": rng.choice(WORDS),
|
||||
"disposition": rng.choice(DISPOSITIONS)}]
|
||||
out.append(("validate_basis", (), {"basis": b}))
|
||||
for _ in range(60):
|
||||
out.append(("attach_evidence_ref", (),
|
||||
{"basis": rng.choice([rand_basis(rng),
|
||||
{"created_at": None},
|
||||
{"issue_ref": "", "created_at": "T"},
|
||||
{}])}))
|
||||
for _ in range(60):
|
||||
b = rand_basis(rng)
|
||||
if rng.random() < 0.4:
|
||||
b["implications"] = rng.choice([None, {}, {"architecture": ["a"], "x": "yy"}])
|
||||
b["uncertainty"] = rng.choice([[], ["u1"], ["u1", "u2"]])
|
||||
b["risks"] = rng.choice([[], ["r1"]])
|
||||
out.append(("summary", (), {"basis": b}))
|
||||
for _ in range(120):
|
||||
b = rand_basis(rng)
|
||||
out.append(("revise_depth", (),
|
||||
{"basis": b, "new_depth": rng.choice(TIERS),
|
||||
"reason": rng.choice(JUSTS + ["skip it", "x" * 250, "r"])}))
|
||||
for _ in range(80):
|
||||
b = rand_basis(rng)
|
||||
if rng.random() < 0.3:
|
||||
b["question"] = "héllo 🎉 \"q\" \\n\x01"
|
||||
out.append(("to_json", (), {"basis": b}))
|
||||
for _ in range(60):
|
||||
out.append(("from_json", (rng.choice([
|
||||
json.dumps({"a": [1, 2.5, True, None], "é": "ü"}),
|
||||
"[1,2,3]", "{}", "null", "\"str\"",
|
||||
"{bad", "{'single': 1}", ""]),), {}))
|
||||
for _ in range(150):
|
||||
kw = dict(intent=rng.choice(WORDS + ["migrate the schema", "deploy it"]),
|
||||
target=rng.choice(["", "wr-bridge"]),
|
||||
current_reality=rng.choice(["", "c" * 250, "réalité"]),
|
||||
unknowns=[rng.choice(WORDS) for _ in range(rng.randrange(0, 3))],
|
||||
decisions=[rng.choice(WORDS) for _ in range(rng.randrange(0, 2))],
|
||||
risks=[rng.choice(WORDS) for _ in range(rng.randrange(0, 2))],
|
||||
depth=rng.choice([None] + TIERS[:4]))
|
||||
out.append(("generate_research_plan", (), kw))
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global bb
|
||||
from rune import build_basis as bb_mod
|
||||
from rune import bb_loader
|
||||
bb = bb_mod
|
||||
if not bb_loader.available():
|
||||
print("Rust core not built — run cargo build --release in src/rust/build_basis")
|
||||
return 2
|
||||
|
||||
total = passed = 0
|
||||
failures: list[str] = []
|
||||
|
||||
def check(label, ok, detail=""):
|
||||
nonlocal total, passed
|
||||
total += 1
|
||||
if ok:
|
||||
passed += 1
|
||||
else:
|
||||
failures.append(f"{label}: {detail}")
|
||||
|
||||
# 1. frozen truth corpus, replayed live
|
||||
truth = json.loads((Path(__file__).resolve().parents[1] / "src" / "rust" /
|
||||
"build_basis" / "src" / "frozen_bb_truth.json").read_text())
|
||||
for fn, cases in truth["cases"].items():
|
||||
for case in cases:
|
||||
name = f"truth:{fn}:{case['name']}"
|
||||
inp = case["input"]
|
||||
if "error" in case and fn == "revise_depth":
|
||||
# error path: oracle raises; kernel must refuse identically
|
||||
try:
|
||||
py_err = None
|
||||
call_core("python", fn, copy.deepcopy(inp["basis"]),
|
||||
new_depth=inp["new_depth"], reason=inp["reason"])
|
||||
except ValueError as e:
|
||||
py_err = str(e)
|
||||
try:
|
||||
bb_loader.revise_depth(copy.deepcopy(inp["basis"]),
|
||||
inp["new_depth"], inp["reason"])
|
||||
check(name, False, "kernel accepted a refused revision")
|
||||
except RuntimeError as e:
|
||||
check(name, str(e) == py_err,
|
||||
f"kernel err {e!r} != oracle {py_err!r}")
|
||||
continue
|
||||
if "error_kind" in case: # from_json invalid → both must refuse
|
||||
try:
|
||||
call_core("python", fn, **inp)
|
||||
py_ok, rs_ok = True, True
|
||||
except ValueError:
|
||||
py_ok = False
|
||||
try:
|
||||
call_core("rust", fn, **inp)
|
||||
rs_ok = True
|
||||
except ValueError:
|
||||
rs_ok = False
|
||||
check(name, py_ok == rs_ok == False, f"py={py_ok} rs={rs_ok}")
|
||||
continue
|
||||
if fn in ("set_dispositions", "revise_depth"):
|
||||
b1, b2 = copy.deepcopy(inp["basis"]), copy.deepcopy(inp["basis"])
|
||||
kw = dict(inp)
|
||||
kw["basis"] = b1
|
||||
py = call_core("python", fn, **kw)
|
||||
kw["basis"] = b2
|
||||
rs = call_core("rust", fn, **kw)
|
||||
ok = mask(py) == mask(rs) and mask(b1) == mask(b2)
|
||||
check(name, ok, f"py={py!r} rs={rs!r}")
|
||||
else:
|
||||
if fn == "new_basis":
|
||||
py = call_core("python", fn, question=inp["question"],
|
||||
depth_decision=inp["depth_decision"],
|
||||
issue_ref=inp.get("issue_ref", ""))
|
||||
rs = call_core("rust", fn, question=inp["question"],
|
||||
depth_decision=inp["depth_decision"],
|
||||
issue_ref=inp.get("issue_ref", ""))
|
||||
else:
|
||||
py = call_core("python", fn, **inp)
|
||||
rs = call_core("rust", fn, **inp)
|
||||
check(name, mask(py) == mask(rs), f"py={py!r} rs={rs!r}")
|
||||
|
||||
# 2. seeded fuzz
|
||||
rng = random.Random(2479)
|
||||
for fn, args, kwargs in fuzz_cases(rng):
|
||||
name = f"fuzz:{fn}"
|
||||
if fn in ("set_dispositions", "revise_depth"):
|
||||
b = rand_basis(rng)
|
||||
b1, b2 = copy.deepcopy(b), copy.deepcopy(b)
|
||||
kw_clean = {k: v for k, v in kwargs.items() if k != "basis"}
|
||||
try:
|
||||
py = call_core("python", fn, b1, **kw_clean)
|
||||
except ValueError as e:
|
||||
py = f"ValueError: {e}"
|
||||
try:
|
||||
rs = call_core("rust", fn, b2, **kw_clean)
|
||||
except ValueError as e:
|
||||
rs = f"ValueError: {e}"
|
||||
check(name, mask(py) == mask(rs) and mask(b1) == mask(b2),
|
||||
f"kwargs={kwargs} py={py!r} rs={rs!r}")
|
||||
elif fn == "new_basis":
|
||||
py = call_core("python", fn, question=args[0],
|
||||
depth_decision=args[1], issue_ref=args[2])
|
||||
rs = call_core("rust", fn, question=args[0],
|
||||
depth_decision=args[1], issue_ref=args[2])
|
||||
check(name, mask(py) == mask(rs), f"args={args}")
|
||||
elif fn in ("validate_basis", "attach_evidence_ref", "summary",
|
||||
"to_json", "from_json", "generate_research_plan"):
|
||||
try:
|
||||
py = call_core("python", fn, *args, **kwargs)
|
||||
except ValueError as e:
|
||||
py = f"ValueError: {e}"
|
||||
try:
|
||||
rs = call_core("rust", fn, *args, **kwargs)
|
||||
except ValueError as e:
|
||||
rs = f"ValueError: {e}"
|
||||
check(name, mask(py) == mask(rs), f"args={args} kwargs={kwargs}")
|
||||
elif fn == "research_depth":
|
||||
py = call_core("python", fn, args[0], mode=args[1],
|
||||
justification=args[2])
|
||||
rs = call_core("rust", fn, args[0], mode=args[1],
|
||||
justification=args[2])
|
||||
check(name, mask(py) == mask(rs), f"args={args}")
|
||||
|
||||
print(f"parity {passed}/{total}")
|
||||
if failures:
|
||||
for f in failures[:12]:
|
||||
print("FAIL", f[:400])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze build_basis oracle truth for the Rust port battery (dichotomy
|
||||
wave #2479 tranche, batch: build_basis — RUST side verdict).
|
||||
|
||||
Exercises EVERY public function across EVERY branch:
|
||||
research_depth (all route defaults, unknown-route floor, tier-selecting
|
||||
vs annotating justifications, blank justification), new_basis,
|
||||
set_dispositions (legal/illegal/missing keys), validate_basis (schema,
|
||||
depth, WHY rule D, OUTWARD/FULL prior-art gate, INWARD evidence gate,
|
||||
illegal disposition, item truncation), is_qualified,
|
||||
attach_evidence_ref (issue_ref/adhoc/missing created_at), summary
|
||||
(empty/qualified/None implications/counts), revise_depth (legal
|
||||
revision, blocked downgrade, no-op, unknown depth, reason truncation),
|
||||
to_json (Python json.dumps indent=1 sort_keys incl. non-ASCII escaping),
|
||||
from_json, generate_research_plan (topic kinds, outward keywords,
|
||||
depth heuristics FULL/OUTWARD/INWARD/NONE, blank filtering, caller
|
||||
depth, truncations).
|
||||
|
||||
Output: src/rust/build_basis/src/frozen_bb_truth.json — replayed
|
||||
VERBATIM by src/rust/build_basis/src/branch_battery.rs.
|
||||
|
||||
Timestamps the kernel generates (decided_at/created_at/revised_at/
|
||||
plan_generated_at) are masked (removed on both sides) — they are wall
|
||||
clock, not logic. Fixed created_at values are INJECTED into basis inputs
|
||||
so kernel pass-through fields compare exactly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_BB_CORE"] = "python" # POISONED-TRUTH LAW: freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import build_basis as bb # noqa: E402
|
||||
|
||||
MASKED = ("decided_at", "created_at", "revised_at", "plan_generated_at")
|
||||
|
||||
|
||||
def mask(doc):
|
||||
"""Remove kernel-generated wall-clock fields wherever they appear."""
|
||||
if isinstance(doc, dict):
|
||||
return {k: mask(v) for k, v in doc.items() if k not in MASKED}
|
||||
if isinstance(doc, list):
|
||||
return [mask(v) for v in doc]
|
||||
return doc
|
||||
|
||||
|
||||
def fixed_basis(depth_route="RESEARCHED_CHANGE", question="q",
|
||||
issue_ref="research/issue-1"):
|
||||
b = bb.new_basis(question=question,
|
||||
depth_decision=bb.research_depth(depth_route),
|
||||
issue_ref=issue_ref)
|
||||
b["created_at"] = "2026-10-11T00:00:00Z" # fixed: pass-through, exact-compare
|
||||
return b
|
||||
|
||||
|
||||
cases: dict[str, list] = {}
|
||||
|
||||
|
||||
def run(fn: str, name: str, **input):
|
||||
out = getattr(bb, fn)(**input)
|
||||
cases.setdefault(fn, []).append(
|
||||
{"name": name, "input": input, "expected": mask(out)})
|
||||
|
||||
|
||||
def run_err(fn: str, name: str, **input):
|
||||
try:
|
||||
getattr(bb, fn)(**input)
|
||||
except ValueError as e:
|
||||
cases.setdefault(fn, []).append(
|
||||
{"name": name, "input": input, "error": str(e)})
|
||||
return
|
||||
raise AssertionError(f"{fn}({name}) did not raise")
|
||||
|
||||
|
||||
# ── research_depth: every branch ──────────────────────────────────────
|
||||
for route in ("LOCAL_FIX", "STANDARD_CHANGE", "RESEARCHED_CHANGE"):
|
||||
run("research_depth", f"route-{route}", route=route)
|
||||
run("research_depth", "unknown-route-floor", route="MYSTERY_ROUTE")
|
||||
run("research_depth", "annotation-justification", route="RESEARCHED_CHANGE",
|
||||
justification="tiny config fix; full pipeline disproportionate")
|
||||
run("research_depth", "tier-select-justification", route="STANDARD_CHANGE",
|
||||
justification="OUTWARD: new provider integration, precedent needed")
|
||||
run("research_depth", "tier-select-downgrade-NONE", route="RESEARCHED_CHANGE",
|
||||
justification="NONE: nothing to research")
|
||||
run("research_depth", "tier-bare-word", route="STANDARD_CHANGE",
|
||||
justification=" OUTWARD ")
|
||||
run("research_depth", "tier-multi-colon", route="STANDARD_CHANGE",
|
||||
justification="FULL: architectural; check heuristics first")
|
||||
run("research_depth", "blank-justification-default", route="LOCAL_FIX",
|
||||
justification=" ")
|
||||
run("research_depth", "blank-justification-unknown", route="WEIRD",
|
||||
justification=" ")
|
||||
run("research_depth", "unknown-route-with-justification", route="WEIRD",
|
||||
justification="check the spike notes")
|
||||
run("research_depth", "mode-recorded", route="RESEARCHED_CHANGE",
|
||||
mode="rune-internal")
|
||||
|
||||
# ── new_basis ─────────────────────────────────────────────────────────
|
||||
b = bb.new_basis(question="structural model for project scaffolds",
|
||||
depth_decision=bb.research_depth("RESEARCHED_CHANGE"),
|
||||
issue_ref="research/structural-model")
|
||||
b["created_at"] = "2026-10-11T00:00:00Z"
|
||||
cases.setdefault("new_basis", []).append(
|
||||
{"name": "full", "input": {"question": b["question"],
|
||||
"depth_decision": mask(b["depth"]),
|
||||
"issue_ref": b["issue_ref"]},
|
||||
"expected": mask(b)})
|
||||
b2 = bb.new_basis(question="q", depth_decision=bb.research_depth("LOCAL_FIX"))
|
||||
b2["created_at"] = "2026-10-11T00:00:00Z"
|
||||
cases["new_basis"].append(
|
||||
{"name": "no-issue-ref", "input": {"question": "q",
|
||||
"depth_decision": mask(b2["depth"])},
|
||||
"expected": mask(b2)})
|
||||
|
||||
# ── set_dispositions ──────────────────────────────────────────────────
|
||||
for nm, disp in (
|
||||
("legal", [{"item": "Backstage catalog entity model", "disposition": "ADAPT",
|
||||
"why": "nesting fits", "ref": "backstage.io"},
|
||||
{"item": "own graph database", "disposition": "REJECT",
|
||||
"why": "structural_store is adequate", "ref": "repo"}]),
|
||||
("illegal", [{"item": "x", "disposition": "MAYBE"}]),
|
||||
("missing-keys", [{}]),
|
||||
("empty-list", []),
|
||||
):
|
||||
basis = fixed_basis()
|
||||
before = json.loads(json.dumps(basis))
|
||||
errs = bb.set_dispositions(basis, disp)
|
||||
cases.setdefault("set_dispositions", []).append(
|
||||
{"name": nm, "input": {"basis": before, "dispositions": disp},
|
||||
"expected": {"basis": mask(basis), "errors": errs}})
|
||||
|
||||
# ── validate_basis: every error branch ────────────────────────────────
|
||||
def val_case(nm, basis):
|
||||
cases.setdefault("validate_basis", []).append(
|
||||
{"name": nm, "input": {"basis": basis},
|
||||
"expected": bb.validate_basis(basis)})
|
||||
|
||||
b = fixed_basis(); b["schema_version"] = 2
|
||||
val_case("schema-mismatch", b)
|
||||
b = fixed_basis(); b["schema_version"] = None
|
||||
val_case("schema-none", b)
|
||||
b = fixed_basis(); b["depth"] = {}
|
||||
val_case("depth-missing", b)
|
||||
b = fixed_basis(); b["depth"] = {"depth": "WEIRD", "why": "w"}
|
||||
val_case("depth-illegal", b)
|
||||
b = fixed_basis(); b["depth"] = {"depth": "FULL"}
|
||||
val_case("why-missing-rule-D", b)
|
||||
val_case("outward-no-evidence", fixed_basis(depth_route="STANDARD_CHANGE",
|
||||
issue_ref="",
|
||||
question="q") | {"depth": {"depth": "OUTWARD", "why": "w"}})
|
||||
b = fixed_basis(); b["depth"] = {"depth": "FULL", "why": "w"}
|
||||
val_case("full-no-evidence", b)
|
||||
val_case("inward-empty", bb.new_basis(question="q",
|
||||
depth_decision={"depth": "INWARD", "why": "w"}))
|
||||
b = bb.new_basis(question="q", depth_decision={"depth": "INWARD", "why": "w"})
|
||||
b["inward"]["findings"] = ["f"]
|
||||
val_case("inward-with-findings", b)
|
||||
b = fixed_basis()
|
||||
b["dispositions"] = [{"item": "x" * 60, "disposition": "MAYBE"}]
|
||||
val_case("illegal-disposition-item-truncated-40", b)
|
||||
b = fixed_basis(); b.pop("dispositions")
|
||||
val_case("dispositions-key-absent", b)
|
||||
b = fixed_basis(); b["outward"] = None
|
||||
val_case("outward-null-inward-ok", b)
|
||||
val_case("qualified-basis-zero-errors", fixed_basis() | {
|
||||
"outward": {"prior_art_questions_answered": ["q1"], "findings": [], "refs": []},
|
||||
"dispositions": [{"item": "i", "disposition": "ADOPT", "why": "w", "ref": "r"}]})
|
||||
|
||||
# ── is_qualified ──────────────────────────────────────────────────────
|
||||
q = fixed_basis()
|
||||
q["outward"] = {"prior_art_questions_answered": ["q1"], "findings": [], "refs": []}
|
||||
q["dispositions"] = [{"item": "i", "disposition": "ADOPT", "why": "w", "ref": "r"}]
|
||||
cases.setdefault("is_qualified", []).append(
|
||||
{"name": "qualified", "input": {"basis": q}, "expected": bb.is_qualified(q)})
|
||||
cases["is_qualified"].append(
|
||||
{"name": "empty-unqualified", "input": {"basis": fixed_basis()},
|
||||
"expected": False})
|
||||
|
||||
# ── attach_evidence_ref ───────────────────────────────────────────────
|
||||
cases.setdefault("attach_evidence_ref", []).append(
|
||||
{"name": "with-issue-ref", "input": {"basis": fixed_basis()},
|
||||
"expected": bb.attach_evidence_ref(fixed_basis())})
|
||||
b = fixed_basis(); b["issue_ref"] = ""
|
||||
cases["attach_evidence_ref"].append(
|
||||
{"name": "adhoc", "input": {"basis": b}, "expected": bb.attach_evidence_ref(b)})
|
||||
b = fixed_basis(); b.pop("created_at")
|
||||
cases["attach_evidence_ref"].append(
|
||||
{"name": "missing-created-at", "input": {"basis": b},
|
||||
"expected": bb.attach_evidence_ref(b)})
|
||||
|
||||
# ── summary ───────────────────────────────────────────────────────────
|
||||
b = fixed_basis()
|
||||
cases.setdefault("summary", []).append(
|
||||
{"name": "empty", "input": {"basis": json.loads(json.dumps(b))},
|
||||
"expected": mask(bb.summary(b))})
|
||||
q2 = fixed_basis()
|
||||
q2["outward"] = {"prior_art_questions_answered": [], "findings": ["f"], "refs": ["r"]}
|
||||
q2["dispositions"] = [{"item": "x" * 100, "disposition": "ADOPT", "why": "w", "ref": "r"},
|
||||
{"item": "short", "disposition": "ADAPT"}]
|
||||
q2["uncertainty"] = ["u1", "u2"]
|
||||
q2["risks"] = ["r1"]
|
||||
q2["implications"] = {"architecture": ["a1", "a2"], "migration": [], "verification": ["v"]}
|
||||
q2s = json.loads(json.dumps(q2))
|
||||
cases["summary"].append(
|
||||
{"name": "qualified-truncation-counts",
|
||||
"input": {"basis": q2s}, "expected": mask(bb.summary(q2))})
|
||||
b = fixed_basis(); b["implications"] = None
|
||||
cases["summary"].append(
|
||||
{"name": "implications-null", "input": {"basis": b},
|
||||
"expected": mask(bb.summary(b))})
|
||||
|
||||
# ── revise_depth ──────────────────────────────────────────────────────
|
||||
def rev_case(nm, basis, new_depth, reason, error_expected=False):
|
||||
before = json.loads(json.dumps(basis))
|
||||
if error_expected:
|
||||
try:
|
||||
bb.revise_depth(basis, new_depth=new_depth, reason=reason)
|
||||
except ValueError as e:
|
||||
cases.setdefault("revise_depth", []).append(
|
||||
{"name": nm, "input": {"basis": before, "new_depth": new_depth,
|
||||
"reason": reason}, "error": str(e)})
|
||||
return
|
||||
raise AssertionError(f"revise_depth {nm} did not raise")
|
||||
rev = bb.revise_depth(basis, new_depth=new_depth, reason=reason)
|
||||
cases.setdefault("revise_depth", []).append(
|
||||
{"name": nm, "input": {"basis": before, "new_depth": new_depth,
|
||||
"reason": reason},
|
||||
"expected": {"basis": mask(basis), "revision": mask(rev)}})
|
||||
|
||||
rev_case("full-to-inward-justified", fixed_basis(), "INWARD",
|
||||
"prior-art question resolved as non-material")
|
||||
rev_case("full-to-inward-blocked", fixed_basis(), "INWARD",
|
||||
"skip it, not relevant", error_expected=True)
|
||||
rev_case("no-op", fixed_basis("INWARD"), "INWARD", "same", error_expected=True)
|
||||
rev_case("unknown-depth", fixed_basis(), "WEIRD", "why not", error_expected=True)
|
||||
rev_case("outward-to-none-unnecessary", fixed_basis(), "NONE",
|
||||
"prior-art question unnecessary: internal solution exists")
|
||||
rev_case("upgrade-full-to-outward", fixed_basis(), "OUTWARD", "scope narrowed")
|
||||
rev_case("none-to-inward", fixed_basis("LOCAL_FIX"), "INWARD", "repo code matters")
|
||||
rev_case("reason-truncated-200", fixed_basis(), "OUTWARD", "r" * 300)
|
||||
rev_case("inward-to-none-keyword", fixed_basis("INWARD"), "NONE",
|
||||
"discovery showed nothing to record")
|
||||
|
||||
# ── to_json (Python json.dumps indent=1 sort_keys semantics) ──────────
|
||||
cases.setdefault("to_json", []).append(
|
||||
{"name": "basis", "input": {"basis": fixed_basis()},
|
||||
"expected": bb.to_json(fixed_basis())})
|
||||
uni = fixed_basis()
|
||||
uni["question"] = "héllo 日本語 🎉 quoted \"x\" back\\slash \n\t ctrl\x01"
|
||||
cases["to_json"].append(
|
||||
{"name": "unicode-escapes", "input": {"basis": uni},
|
||||
"expected": bb.to_json(uni)})
|
||||
cases["to_json"].append(
|
||||
{"name": "empty-dict", "input": {"basis": {}}, "expected": bb.to_json({})})
|
||||
mixed = {"b": 1, "a": [1, 2.5, True, None, "x"], "c": {"z": [], "y": {}},
|
||||
"é": "café", "Z": -17, "nested": {"deep": {"deeper": [{}]},
|
||||
"flag": False}}
|
||||
cases["to_json"].append(
|
||||
{"name": "mixed-sorted", "input": {"basis": mixed},
|
||||
"expected": bb.to_json(mixed)})
|
||||
|
||||
# ── from_json ─────────────────────────────────────────────────────────
|
||||
cases.setdefault("from_json", []).append(
|
||||
{"name": "object", "input": {"text": bb.to_json(fixed_basis())},
|
||||
"expected": mask(json.loads(bb.to_json(fixed_basis())))})
|
||||
cases["from_json"].append(
|
||||
{"name": "array", "input": {"text": "[1, \"two\", null, {\"k\": true}]"},
|
||||
"expected": [1, "two", None, {"k": True}]})
|
||||
try:
|
||||
bb.from_json("{bad json")
|
||||
raise AssertionError("from_json did not raise")
|
||||
except ValueError as e:
|
||||
cases["from_json"].append(
|
||||
{"name": "invalid", "input": {"text": "{bad json"},
|
||||
"error_kind": "ValueError"})
|
||||
|
||||
# ── generate_research_plan: every branch ──────────────────────────────
|
||||
for nm, kw in (
|
||||
("no-args-none", dict(intent="fix a typo in the README")),
|
||||
("unknowns-inward", dict(intent="i", unknowns=["which code path renders tables?"])),
|
||||
("outward-prior-art", dict(intent="i",
|
||||
unknowns=["is there an existing Python PDF library?"])),
|
||||
("full-consequential", dict(intent="migrate the auth schema",
|
||||
unknowns=["which user tables hold credential hashes?"],
|
||||
risks=["breaking change to login flow"])),
|
||||
("consequential-but-no-topics", dict(intent="deploy and delete the old cluster")),
|
||||
("blank-filtered", dict(intent="i", unknowns=["", " ", "real unknown u"],
|
||||
decisions=[" ", "d1"], risks=["", "r1"])),
|
||||
("decisions-and-risks-kinds", dict(intent="i", unknowns=["u1"],
|
||||
decisions=["d1"], risks=["r1"])),
|
||||
("truncation-80", dict(intent="i", unknowns=["u" * 120])),
|
||||
("depth-supplied", dict(intent="i", unknowns=["big unknown"], depth="INWARD")),
|
||||
("multiword-prior-art", dict(intent="i",
|
||||
unknowns=["prior art on vector stores?"])),
|
||||
("target-and-reality", dict(intent="i", target="wr-bridge",
|
||||
current_reality="c" * 300,
|
||||
unknowns=["u"], depth="NONE")),
|
||||
("tool-framework-keywords", dict(intent="i",
|
||||
unknowns=["which framework/tool convention applies?"])),
|
||||
):
|
||||
plan = bb.generate_research_plan(**kw)
|
||||
cases.setdefault("generate_research_plan", []).append(
|
||||
{"name": nm, "input": kw, "expected": mask(plan)})
|
||||
|
||||
truth = {
|
||||
"frozen": "build_basis full oracle truth (dichotomy wave #2479)",
|
||||
"oracle": "src/rune/build_basis.py",
|
||||
"note": ("Timestamp fields the kernel GENERATES (decided_at, created_at, "
|
||||
"revised_at, plan_generated_at) are masked on both sides — wall "
|
||||
"clock, not logic. created_at values inside basis INPUTS are "
|
||||
"fixed pass-through and compare exactly. error = the exact "
|
||||
"Python ValueError message."),
|
||||
"cases": cases,
|
||||
}
|
||||
out = Path(__file__).resolve().parents[1] / "src" / "rust" / "build_basis" / "src" / "frozen_bb_truth.json"
|
||||
out.write_text(json.dumps(truth, indent=1, sort_keys=True) + "\n")
|
||||
total = sum(len(v) for v in cases.values())
|
||||
print(f"frozen {total} cases across {len(cases)} functions -> {out}")
|
||||
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze construction_pattern oracle truth for the Rust port battery
|
||||
(dichotomy wave #2479 tranche, batch: construction_pattern).
|
||||
|
||||
Exercises EVERY public function and branch of
|
||||
src/rune/construction_pattern.py:
|
||||
|
||||
ConstructionPattern.__init__ (defaults, sorting, ext, all validation errors)
|
||||
ConstructionPattern.to_dict / from_dict (roundtrip incl. ext)
|
||||
ConstructionPattern.revise (changes, history accumulation, 200-char
|
||||
reason truncation, sorted evidence/changed)
|
||||
record_pattern_proposal (bounds errors, evidence sorting, 0/1 bounds)
|
||||
evaluate_applicability (exclusions by objective-substring and by
|
||||
constraint membership, first-match-wins,
|
||||
no-signals UNKNOWN, no-groups UNKNOWN,
|
||||
surface_kinds via surface lists AND via
|
||||
resolutions, short-token vs containment path
|
||||
and entity matching, ':'-prefix entities,
|
||||
objective_any '/' splitting (dashes NOT
|
||||
split), contracts_match via authorities
|
||||
(implements/exposes/provides/serves),
|
||||
APPLIES / PARTIAL / DOES_NOT_APPLY,
|
||||
evidence [:5] truncation)
|
||||
derive_pattern_obligations (when None, always True vs truthy-not-True,
|
||||
path_contains hit/miss + [:3] truncation,
|
||||
entities_match via paths/ui/regions,
|
||||
contracts_touched via need constraints and
|
||||
via authority relationships, unrecognized
|
||||
shapes, EXCEPTED with defaults,
|
||||
accepted/rejected/unresolved routing)
|
||||
combine_pattern_obligations (duplicate collapse + also_from, same-id
|
||||
different-text conflicts, id sort, empty)
|
||||
|
||||
Each case records its INPUT and the oracle's EXPECTED output/error; replayed
|
||||
VERBATIM by src/rust/construction_pattern/src/branch_battery.rs.
|
||||
|
||||
Output: src/rust/construction_pattern/src/frozen_cp_truth.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_CP_CORE"] = "python" # POISONED-TRUTH LAW: freeze the ORACLE
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import construction_pattern as cp # noqa: E402
|
||||
|
||||
OUT = Path(__file__).resolve().parents[1] / "src" / "rust" / "construction_pattern" / "src" / "frozen_cp_truth.json"
|
||||
|
||||
cases = []
|
||||
|
||||
|
||||
def need(objective="repair the deploy promotion ordering",
|
||||
affected_surface=("scripts/deploy_stage.py", "scripts/deploy_promotion.py"),
|
||||
constraints=None):
|
||||
class N:
|
||||
pass
|
||||
n = N()
|
||||
n.objective = objective
|
||||
n.affected_surface = list(affected_surface)
|
||||
n.constraints = dict(constraints if constraints is not None else {"contracts": ["contract:monotonic_promotion"]})
|
||||
return n
|
||||
|
||||
|
||||
def serialize_need(n):
|
||||
return {"objective": n.objective, "affected_surface": n.affected_surface, "constraints": n.constraints}
|
||||
|
||||
|
||||
SPINE = {
|
||||
"entities": [
|
||||
{"id": "module:deploy_stage", "kind": "module"},
|
||||
{"id": "contract:monotonic_promotion", "kind": "contract"},
|
||||
{"id": "module:deploy_qualify", "kind": "module"},
|
||||
{"id": "contract:fail_closed_deployment", "kind": "contract"},
|
||||
],
|
||||
"relationships": [
|
||||
{"type": "implements", "from": "module:deploy_stage", "to": "contract:monotonic_promotion"},
|
||||
{"type": "exposes", "from": "module:deploy_stage", "to": "contract:exposed_api"},
|
||||
{"type": "serves", "from": "module:deploy_stage", "to": "contract:served_edge"},
|
||||
{"type": "implements", "from": "module:deploy_qualify", "to": "contract:fail_closed_deployment"},
|
||||
{"type": "provides", "from": "module:deploy_qualify", "to": "contract:provided_gate"},
|
||||
{"type": "serves", "from": "module:deploy_qualify", "to": "contract:second_edge"},
|
||||
{"type": "depends_on", "from": "module:deploy_stage", "to": "module:deploy_qualify"},
|
||||
],
|
||||
}
|
||||
AUTH = {"rune_spine": SPINE}
|
||||
|
||||
SURFACE = {
|
||||
"affected_paths": [
|
||||
"scripts/deploy_stage.py", "scripts/deploy_promotion.py",
|
||||
"src/rune/rune_tui.py", "src/rune/builtin.py",
|
||||
],
|
||||
"affected_existing_entities": ["module:deploy_stage", "module:deploy_qualify", "ui:chat"],
|
||||
"affected_ui_surfaces": ["ui:version_display"],
|
||||
"affected_regions": ["region:left"],
|
||||
"workflow_config_surfaces": [".gitea/workflows/deploy.yml"],
|
||||
"resolutions": [
|
||||
{"id": "module:deploy_stage", "kind": "source_path", "verdict": "FOUND"},
|
||||
{"id": "ui:version_display", "kind": "ui_component", "verdict": "FOUND"},
|
||||
{"id": "region:left", "kind": "layout_region", "verdict": "FOUND"},
|
||||
{"id": "cfg:deploy", "kind": "workflow_config", "verdict": "FOUND"},
|
||||
{"id": "module:deploy_stage", "kind": "structural_entity", "verdict": "FOUND"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def case(name, op, inp, fn):
|
||||
import copy as _copy
|
||||
# Deep-copy the input BEFORE running the oracle: to_dict() shares
|
||||
# nested references (ext) with the live object, and revise() mutates
|
||||
# revision_history in place — without the copy the recorded input
|
||||
# aliases oracle-mutated state.
|
||||
inp = _copy.deepcopy(inp)
|
||||
try:
|
||||
expected = fn()
|
||||
except ValueError as e:
|
||||
expected = {"error": str(e)}
|
||||
cases.append({"name": name, "op": op, "input": inp, "expected": expected})
|
||||
|
||||
|
||||
# ── ConstructionPattern.__init__ / to_dict / from_dict ──────────────────────
|
||||
|
||||
FULL_KW = dict(
|
||||
revision=3, maturity="EXERCISED",
|
||||
signals={"path_contains": ["deploy"], "objective_any": ["promotion"]},
|
||||
obligations=[
|
||||
{"id": "o2", "obligation": "second duty", "when": {"always": True}},
|
||||
{"id": "o1", "obligation": "first duty", "when": {"path_contains": ["deploy"]}},
|
||||
],
|
||||
provenance=["incident:zeta", "incident:alpha"],
|
||||
exclusions=["queue-only-project", "no-deploy-zone"],
|
||||
toolchain_notes=["zinc", "amber"],
|
||||
runtime_effects=["r2", "r1"],
|
||||
)
|
||||
|
||||
|
||||
def construct_case(name, kwargs):
|
||||
case(name, "construct", {"kwargs": kwargs},
|
||||
lambda: cp.ConstructionPattern.from_dict(kwargs).to_dict())
|
||||
|
||||
|
||||
construct_case("construct-full", {"pattern_id": "p.full", "purpose": "full pattern",
|
||||
"wibble": "future-data", **FULL_KW})
|
||||
construct_case("construct-minimal", {"pattern_id": "p.min", "purpose": "minimal pattern"})
|
||||
construct_case("construct-ext-open-world", {"pattern_id": "p.ext", "purpose": "open world",
|
||||
"wibble": "future-data", "count": 7})
|
||||
construct_case("construct-empty-id", {"pattern_id": "", "purpose": "purpose"})
|
||||
construct_case("construct-empty-purpose", {"pattern_id": "p", "purpose": ""})
|
||||
construct_case("construct-obl-missing-id",
|
||||
{"pattern_id": "p", "purpose": "purpose",
|
||||
"obligations": [{"obligation": "x", "when": {}}]})
|
||||
construct_case("construct-obl-missing-text",
|
||||
{"pattern_id": "p", "purpose": "purpose", "obligations": [{"id": "x", "when": {}}]})
|
||||
construct_case("construct-obl-missing-when",
|
||||
{"pattern_id": "p", "purpose": "purpose", "obligations": [{"id": "x", "obligation": "x"}]})
|
||||
construct_case("construct-from-dict-roundtrip",
|
||||
cp.ConstructionPattern("p.rt", "roundtrip", wibble={"deep": [1, 2]}, **FULL_KW).to_dict())
|
||||
|
||||
# ── revise ───────────────────────────────────────────────────────────────────
|
||||
|
||||
REV_BASE = dict(revision=2, maturity="UNIT-PROVEN",
|
||||
provenance=["contract:c9"], exclusions=["b-ex", "a-ex"])
|
||||
|
||||
|
||||
def revise_case(name, base_kwargs, reason, evidence, changes, ext=None):
|
||||
base = cp.ConstructionPattern("p.rev", "revisable", **(ext or {}), **base_kwargs)
|
||||
case(name, "revise",
|
||||
{"pattern": base.to_dict(), "reason": reason, "evidence": evidence, "changes": changes},
|
||||
lambda: base.revise(reason=reason, evidence=evidence, changes=changes).to_dict())
|
||||
|
||||
|
||||
revise_case("revise-with-changes", REV_BASE, "new incident evidence",
|
||||
["z-evidence", "a-evidence"], {"maturity": "EXERCISED"})
|
||||
revise_case("revise-history-accumulates", {"revision": 2, "revision_history": [
|
||||
{"prior_revision": 1, "reason": "initial correction", "evidence": ["old-evidence"], "changed": ["signals"]}]},
|
||||
"second revision", ["new-evidence"], {})
|
||||
revise_case("revise-reason-truncated-200", {}, "r" * 250, ["e"], None)
|
||||
revise_case("revise-open-world-ext-carried", {}, "carries ext", ["e"], {"purpose": "revised purpose"},
|
||||
ext={"wibble": "kept"})
|
||||
|
||||
# ── record_pattern_proposal ─────────────────────────────────────────────────
|
||||
|
||||
def propose_case(name, pattern_id="x", provider="p", model="m", evidence=(), confidence=0.5):
|
||||
case(name, "propose",
|
||||
{"pattern_id": pattern_id, "provider": provider, "model": model,
|
||||
"evidence": list(evidence), "confidence": confidence},
|
||||
lambda: cp.record_pattern_proposal(pattern_id, provider=provider, model=model,
|
||||
evidence=list(evidence), confidence=confidence))
|
||||
|
||||
|
||||
propose_case("propose-valid", pattern_id="some-pattern", provider="local", model="m1",
|
||||
evidence=["z-hunch", "a-hunch"], confidence=0.5)
|
||||
case("propose-bounds-zero-one", "propose",
|
||||
{"pattern_id": "x", "provider": "p", "model": "m", "evidence": [], "confidence": [0.0, 1.0]},
|
||||
lambda: [cp.record_pattern_proposal("x", provider="p", model="m", evidence=[], confidence=0.0),
|
||||
cp.record_pattern_proposal("x", provider="p", model="m", evidence=[], confidence=1.0)])
|
||||
propose_case("propose-empty-provider", provider="")
|
||||
propose_case("propose-empty-model", model="")
|
||||
propose_case("propose-confidence-above-1", confidence=1.5)
|
||||
propose_case("propose-confidence-below-0", confidence=-0.1)
|
||||
|
||||
# ── evaluate_applicability ──────────────────────────────────────────────────
|
||||
|
||||
def eval_case(name, pattern_kwargs, n=None, surface=None, authorities=None):
|
||||
p = cp.ConstructionPattern("p.eval", "evaluated pattern", **pattern_kwargs)
|
||||
nn = n or need()
|
||||
s = surface or SURFACE
|
||||
a = authorities or AUTH
|
||||
case(name, "evaluate",
|
||||
{"pattern": p.to_dict(), "need": serialize_need(nn), "surface_result": s, "authorities": a},
|
||||
lambda: cp.evaluate_applicability(p, nn, s, a))
|
||||
|
||||
|
||||
eval_case("eval-all-groups-applies",
|
||||
{"signals": {"surface_kinds": ["source_path", "ui_component", "layout_region",
|
||||
"workflow_config", "structural_entity"],
|
||||
"path_contains": ["deploy"], "entities_match": ["module:"],
|
||||
"objective_any": ["promotion"],
|
||||
"contracts_match": ["contract:monotonic_promotion"]}})
|
||||
eval_case("eval-partial",
|
||||
{"signals": {"path_contains": ["deploy"], "entities_match": ["nope-never", "zzz"],
|
||||
"objective_any": ["zzzneverword"]}})
|
||||
eval_case("eval-none-declared-does-not-apply",
|
||||
{"signals": {"path_contains": ["/nonexistent/xyz/"], "objective_any": ["zzznever"]}})
|
||||
eval_case("eval-no-signals-unknown", {})
|
||||
eval_case("eval-no-groups-declared-unknown", {"signals": {"surface_kinds": [], "path_contains": []}})
|
||||
eval_case("eval-exclusion-objective-substring",
|
||||
{"signals": {"path_contains": ["deploy"]}, "exclusions": ["no deployment system in this project"]})
|
||||
eval_case("eval-exclusion-constraint-membership",
|
||||
{"signals": {"path_contains": ["deploy"]}, "exclusions": ["message-queue-only"]},
|
||||
n=need(constraints={"contracts": [], "pattern_exclusions": ["message-queue-only"]}))
|
||||
eval_case("eval-exclusion-first-match-wins",
|
||||
{"signals": {"path_contains": ["deploy"]},
|
||||
"exclusions": ["no deployment system in this project", "message-queue-only"]},
|
||||
n=need(constraints={"contracts": [], "pattern_exclusions": ["message-queue-only"]}))
|
||||
eval_case("eval-exclusion-declared-but-unmatched",
|
||||
{"signals": {"path_contains": ["deploy"]}, "exclusions": ["totally-unrelated-exclusion"]},
|
||||
n=need(constraints={"contracts": [], "pattern_exclusions": ["another-unrelated"]}))
|
||||
eval_case("eval-surface-kinds-via-resolutions",
|
||||
{"signals": {"surface_kinds": ["structural_entity", "nope_kind"]}})
|
||||
eval_case("eval-path-short-token",
|
||||
{"signals": {"path_contains": ["ui", "tui", "cfg", "deploy"]}})
|
||||
eval_case("eval-entity-prefix-and-tokens",
|
||||
{"signals": {"entities_match": ["ui:", "module:", "ui", "spine", "zzz"]}})
|
||||
eval_case("eval-objective-slash-split",
|
||||
{"signals": {"objective_any": ["deploy", "release", "widget"]}},
|
||||
n=need(objective="adjust the release/deploy widget", constraints={"contracts": []}))
|
||||
eval_case("eval-objective-dash-not-split",
|
||||
{"signals": {"objective_any": ["deploy", "pre-deploy"]}},
|
||||
n=need(objective="verify the pre-deploy check", constraints={"contracts": []}))
|
||||
eval_case("eval-contracts-all-rel-kinds",
|
||||
{"signals": {"contracts_match": ["contract:monotonic_promotion", "contract:exposed_api",
|
||||
"contract:served_edge", "contract:fail_closed_deployment",
|
||||
"contract:provided_gate", "contract:second_edge"]}})
|
||||
eval_case("eval-contracts-miss-non-rel-type",
|
||||
{"signals": {"contracts_match": ["module:deploy_qualify"]}})
|
||||
eval_case("eval-contracts-second-authority",
|
||||
{"signals": {"contracts_match": ["contract:scaffold_only"]}},
|
||||
authorities={"rune_spine": SPINE,
|
||||
"project_scaffold": {"relationships": [
|
||||
{"type": "implements", "from": "module:deploy_stage", "to": "contract:scaffold_only"}]}})
|
||||
eval_case("eval-empty-entity-id-quirk",
|
||||
{"signals": {"contracts_match": ["contract:monotonic_promotion"]}},
|
||||
surface={**SURFACE, "affected_existing_entities": ["", "module:deploy_qualify"]})
|
||||
|
||||
# ── derive_pattern_obligations ──────────────────────────────────────────────
|
||||
|
||||
O = lambda oid, text, when: {"id": oid, "obligation": text, "when": when}
|
||||
|
||||
|
||||
def derive_case(name, obligations, n=None, exceptions=None, surface=None, authorities=None):
|
||||
p = cp.ConstructionPattern("p.derive", "deriving pattern",
|
||||
provenance=["incident:prov-1", "contract:prov-0"],
|
||||
obligations=obligations)
|
||||
nn = n or need()
|
||||
s = surface or SURFACE
|
||||
a = authorities or AUTH
|
||||
case(name, "derive",
|
||||
{"pattern": p.to_dict(), "need": serialize_need(nn), "surface_result": s,
|
||||
"authorities": a, "project_exceptions": exceptions},
|
||||
lambda: cp.derive_pattern_obligations(p, nn, s, a, project_exceptions=exceptions))
|
||||
|
||||
|
||||
derive_case("derive-when-none-unknown", [O("mystery", "do the unconditioned thing", None)])
|
||||
derive_case("derive-always-required", [O("always-ob", "regression proof", {"always": True})])
|
||||
derive_case("derive-always-truthy-not-true",
|
||||
[O("truthy-ob", "truthy always is not always", {"always": 1})])
|
||||
derive_case("derive-path-hit-truncation",
|
||||
[O("path-ob", "touch the deploy paths", {"path_contains": ["deploy", "rune"]})])
|
||||
derive_case("derive-path-miss", [O("path-ob", "touch nothing real", {"path_contains": ["/absent/xyz/"]})])
|
||||
derive_case("derive-entities-via-ui-and-regions",
|
||||
[O("ui-ob", "respect the ui surface", {"entities_match": ["version_display"]}),
|
||||
O("region-ob", "respect the region", {"entities_match": ["left"]}),
|
||||
O("entity-ob", "respect the module", {"entities_match": ["module:"]}),
|
||||
O("miss-ob", "matches nothing", {"entities_match": ["zzz-never"]})])
|
||||
derive_case("derive-contracts-from-need-constraints",
|
||||
[O("ct-ob", "honor the explicit contract",
|
||||
{"contracts_touched": ["contract:explicit_need", "contract:absent"]})],
|
||||
n=need(constraints={"contracts": ["contract:explicit_need"]}))
|
||||
derive_case("derive-contracts-from-authorities",
|
||||
[O("ct-ob", "honor the authority-implied contract",
|
||||
{"contracts_touched": ["contract:provided_gate", "contract:served_edge", "contract:nowhere"]})])
|
||||
derive_case("derive-contracts-miss",
|
||||
[O("ct-ob", "cannot honor the unknown contract", {"contracts_touched": ["contract:not_anywhere"]})])
|
||||
derive_case("derive-unrecognized-shape", [O("weird-ob", "do the unknown thing", {"unsupported_shape": True})])
|
||||
derive_case("derive-mixed-routing",
|
||||
[O("acc-ob", "always applies", {"always": True}),
|
||||
O("rej-ob", "path never matches", {"path_contains": ["/absent/xyz/"]}),
|
||||
O("unres-ob", "unknown shape", {"weird": 1})])
|
||||
derive_case("derive-exception-with-defaults",
|
||||
[O("exc-ob", "suppressed obligation", {"always": True}),
|
||||
O("kept-ob", "surviving obligation", {"always": True})],
|
||||
exceptions=[{"obligation": "exc-ob"}])
|
||||
derive_case("derive-exception-full",
|
||||
[O("exc-ob", "suppressed obligation", {"always": True})],
|
||||
exceptions=[{"obligation": "exc-ob", "reason": "project policy documented",
|
||||
"evidence": "policy doc", "authority": "user"}])
|
||||
|
||||
# ── combine_pattern_obligations ─────────────────────────────────────────────
|
||||
|
||||
def derivation(pattern_id, rows, revision=1):
|
||||
"""rows: (id, verdict, text) — mirrors derive_pattern_obligations output."""
|
||||
out = {"pattern": pattern_id, "revision": revision, "accepted": [], "rejected": [], "unresolved": []}
|
||||
for oid, verdict, text in rows:
|
||||
entry = {"id": oid, "obligation": text, "pattern": pattern_id, "revision": revision,
|
||||
"provenance": "PATTERN", "pattern_provenance": [f"prov:{pattern_id}"],
|
||||
"verdict": verdict, "reason": "reason"}
|
||||
out[{"REQUIRED": "accepted", "APPLICABLE": "accepted",
|
||||
"NOT_APPLICABLE": "rejected"}.get(verdict, "unresolved")].append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def combine_case(name, derivs):
|
||||
case(name, "combine", {"derivations": derivs}, lambda: cp.combine_pattern_obligations(derivs))
|
||||
|
||||
|
||||
combine_case("combine-empty", [])
|
||||
combine_case("combine-single", [derivation("a", [("shared", "REQUIRED", "regression proof"),
|
||||
("z-only", "APPLICABLE", "z duty")])])
|
||||
combine_case("combine-duplicate-collapse", [
|
||||
derivation("a", [("shared", "REQUIRED", "regression proof")]),
|
||||
derivation("b", [("shared", "REQUIRED", "regression proof")]),
|
||||
derivation("c", [("shared", "REQUIRED", "regression proof")])])
|
||||
combine_case("combine-conflict-different-text", [
|
||||
derivation("x", [("shared", "REQUIRED", "version A of the rule")]),
|
||||
derivation("y", [("shared", "REQUIRED", "version B of the rule")])])
|
||||
combine_case("combine-sort-by-id", [
|
||||
derivation("p1", [("zz", "REQUIRED", "last"), ("aa", "REQUIRED", "first")]),
|
||||
derivation("p2", [("mm", "APPLICABLE", "middle")])])
|
||||
combine_case("combine-conflict-then-continue", [
|
||||
derivation("x", [("bad", "REQUIRED", "text one"), ("ok", "REQUIRED", "fine duty")]),
|
||||
derivation("y", [("bad", "REQUIRED", "text two")])])
|
||||
|
||||
# ── write truth ──────────────────────────────────────────────────────────────
|
||||
|
||||
OUT.write_text(json.dumps({"generator": "scripts/gen_construction_pattern_truth.py",
|
||||
"module": "src/rune/construction_pattern.py", "cases": cases}, indent=1) + "\n")
|
||||
print(f"{len(cases)} cases -> {OUT}")
|
||||
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_drift_observability_truth.py — freeze/replay parity truth for
|
||||
drift_observability (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/drift_observability.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/drift_observability/src/frozen_do_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle methods delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_DO_CORE=python BEFORE any import.
|
||||
No-clock law: the wall clock is frozen by patching the datetime symbol in
|
||||
BOTH drift_observability and drift_core, so every kernel payload carries
|
||||
its own `now` and replay is deterministic regardless of run date.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(
|
||||
REPO, "src", "rust", "drift_observability", "src", "frozen_do_truth.json"
|
||||
)
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_DO_CORE"] = "python" # poisoned-truth law
|
||||
os.environ["RUNE_MEMORY_CORE"] = "python"
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import drift_core # noqa: E402
|
||||
import drift_observability as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
import do_loader # noqa: E402 (kernel-level replay side)
|
||||
|
||||
FROZEN_UTC = datetime(2026, 10, 11, 12, 0, 0, 123456, tzinfo=timezone.utc)
|
||||
FROZEN_LOCAL = datetime(2026, 10, 11, 13, 0, 0, 123456) # +01:00 wall clock
|
||||
FROZEN_NOW = FROZEN_UTC.isoformat()
|
||||
FROZEN_LOCAL_ISO = FROZEN_LOCAL.isoformat()
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
if tz is not None:
|
||||
return FROZEN_UTC.astimezone(tz)
|
||||
return FROZEN_LOCAL
|
||||
|
||||
|
||||
oracle_mod.datetime = FrozenDatetime
|
||||
drift_core.datetime = FrozenDatetime
|
||||
|
||||
|
||||
def ago(**kw):
|
||||
return (FROZEN_UTC - timedelta(**kw)).isoformat()
|
||||
|
||||
|
||||
def row(rock="washable", created=None, washed=None, accessed=None, status="active"):
|
||||
return (status, rock, created, washed, accessed)
|
||||
|
||||
|
||||
def make_db(rows):
|
||||
db = sqlite3.connect(":memory:")
|
||||
db.execute(
|
||||
"CREATE TABLE entries (status TEXT, rock_status TEXT, created_at TEXT,"
|
||||
" last_washed TEXT, last_accessed TEXT)"
|
||||
)
|
||||
db.executemany("INSERT INTO entries VALUES (?,?,?,?,?)", rows)
|
||||
return db
|
||||
|
||||
|
||||
_MAX_LAST_WASHED_SQL = (
|
||||
"SELECT MAX(last_washed) FROM entries "
|
||||
"WHERE rock_status != 'washable' AND status = 'active'"
|
||||
)
|
||||
|
||||
|
||||
class Case:
|
||||
def __init__(self, name, fn):
|
||||
self.name = name
|
||||
self.fn = fn
|
||||
|
||||
|
||||
def fresh_obs(alerts_pre=None):
|
||||
tmp = tempfile.mkdtemp(prefix="do_truth_")
|
||||
oracle_mod.ALERTS_FILE = os.path.join(tmp, "drift_alerts.json")
|
||||
oracle_mod.RETROSPECTIVES_DIR = os.path.join(tmp, "retrospectives")
|
||||
obs = oracle_mod.DriftObservability()
|
||||
if alerts_pre is not None:
|
||||
obs.alerts = copy.deepcopy(alerts_pre)
|
||||
return obs
|
||||
|
||||
|
||||
def _stamped(alert_type, message, severity):
|
||||
return {
|
||||
"type": alert_type,
|
||||
"message": message,
|
||||
"severity": severity,
|
||||
"timestamp": FROZEN_NOW,
|
||||
"acknowledged": False,
|
||||
}
|
||||
|
||||
|
||||
# ── add_alert ────────────────────────────────────────────────────────────────
|
||||
|
||||
def c_add_default(rec):
|
||||
obs = fresh_obs()
|
||||
rec("add-default-severity", {"alerts": [], "alert_type": "high_velocity",
|
||||
"message": "v up", "severity": "info", "now": FROZEN_NOW},
|
||||
obs.add_alert("high_velocity", "v up"), {"alerts": obs.alerts})
|
||||
|
||||
|
||||
def c_add_warning(rec):
|
||||
obs = fresh_obs()
|
||||
rec("add-warning-severity", {"alerts": [], "alert_type": "negative_drift",
|
||||
"message": "m", "severity": "warning", "now": FROZEN_NOW},
|
||||
obs.add_alert("negative_drift", "m", "warning"), {"alerts": obs.alerts})
|
||||
|
||||
|
||||
def c_add_critical(rec):
|
||||
obs = fresh_obs()
|
||||
rec("add-critical-severity", {"alerts": [], "alert_type": "promotion_stall",
|
||||
"message": "stalled", "severity": "critical", "now": FROZEN_NOW},
|
||||
obs.add_alert("promotion_stall", "stalled", "critical"), {"alerts": obs.alerts})
|
||||
|
||||
|
||||
def c_add_onto_existing(rec):
|
||||
pre = [{"type": "old", "message": "m0", "severity": "info",
|
||||
"timestamp": ago(days=1), "acknowledged": True}]
|
||||
obs = fresh_obs(pre)
|
||||
rec("add-onto-existing", {"alerts": pre, "alert_type": "velocity",
|
||||
"message": "second", "severity": "info", "now": FROZEN_NOW},
|
||||
obs.add_alert("velocity", "second"), {"alerts": obs.alerts})
|
||||
|
||||
|
||||
# ── get_unacknowledged_alerts ────────────────────────────────────────────────
|
||||
|
||||
def _pre_mixed():
|
||||
return [
|
||||
{"type": "a", "message": "m1", "severity": "info",
|
||||
"timestamp": ago(days=3), "acknowledged": True},
|
||||
{"type": "b", "message": "m2", "severity": "warning",
|
||||
"timestamp": ago(days=2), "acknowledged": False},
|
||||
{"type": "c", "message": "m3", "severity": "critical",
|
||||
"timestamp": ago(days=1)}, # missing key → unacknowledged
|
||||
]
|
||||
|
||||
|
||||
def c_unacked_empty(rec):
|
||||
obs = fresh_obs([])
|
||||
rec("unacked-empty", {"alerts": []},
|
||||
obs.get_unacknowledged_alerts(), {"unacked": []})
|
||||
|
||||
|
||||
def c_unacked_mixed(rec):
|
||||
pre = _pre_mixed()
|
||||
obs = fresh_obs(pre)
|
||||
rec("unacked-mixed", {"alerts": pre},
|
||||
obs.get_unacknowledged_alerts(), {"unacked": obs.get_unacknowledged_alerts()})
|
||||
|
||||
|
||||
def c_unacked_all_open(rec):
|
||||
pre = [dict(_stamped("t", "m", "info"), timestamp=ago(days=1)) for _ in range(3)]
|
||||
obs = fresh_obs(pre)
|
||||
rec("unacked-all-open", {"alerts": pre},
|
||||
obs.get_unacknowledged_alerts(), {"unacked": obs.get_unacknowledged_alerts()})
|
||||
|
||||
|
||||
# ── acknowledge_alert ────────────────────────────────────────────────────────
|
||||
|
||||
def _ack_case(rec, name, index):
|
||||
pre = _pre_mixed()
|
||||
obs = fresh_obs(pre)
|
||||
obs.acknowledge_alert(index)
|
||||
changed = 0 <= index < len(pre)
|
||||
rec(name, {"alerts": pre, "index": index},
|
||||
None, {"alerts": obs.alerts, "changed": changed})
|
||||
|
||||
|
||||
def c_ack_first(rec):
|
||||
_ack_case(rec, "ack-first", 0)
|
||||
|
||||
|
||||
def c_ack_second(rec):
|
||||
_ack_case(rec, "ack-second", 1)
|
||||
|
||||
|
||||
def c_ack_negative(rec):
|
||||
_ack_case(rec, "ack-negative", -1)
|
||||
|
||||
|
||||
def c_ack_at_len(rec):
|
||||
_ack_case(rec, "ack-at-len", 3)
|
||||
|
||||
|
||||
def c_ack_beyond(rec):
|
||||
_ack_case(rec, "ack-beyond", 8)
|
||||
|
||||
|
||||
# ── check_alerts ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _check_case(rec, name, rows):
|
||||
db = make_db(rows)
|
||||
velocity = drift_core.compute_drift_velocity(db)
|
||||
direction = drift_core.compute_drift_direction(db)
|
||||
last = db.execute(_MAX_LAST_WASHED_SQL).fetchone()[0]
|
||||
obs = fresh_obs([])
|
||||
new = obs.check_alerts(db)
|
||||
rec(name,
|
||||
{"alerts": [], "velocity": velocity, "direction": direction,
|
||||
"last_washed": last, "now": FROZEN_NOW},
|
||||
new, {"new_alerts": new, "alerts": obs.alerts})
|
||||
|
||||
|
||||
def c_check_empty_db(rec):
|
||||
# Empty db: stability_ratio 0.0 < 0.1 — the negative-drift alert FIRES.
|
||||
_check_case(rec, "check-empty-db-negative-drift", [])
|
||||
|
||||
|
||||
def c_check_all_clear(rec):
|
||||
rows = [row(rock="established", created=ago(days=30), washed=ago(days=1),
|
||||
accessed=ago(hours=1)) for _ in range(10)]
|
||||
_check_case(rec, "check-all-clear", rows)
|
||||
|
||||
|
||||
def c_check_velocity_only(rec):
|
||||
rows = [row(created=ago(days=1), washed=ago(days=1)) for _ in range(71)]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=1))
|
||||
for _ in range(30)]
|
||||
_check_case(rec, "check-velocity-only", rows)
|
||||
|
||||
|
||||
def c_check_negative_drift_only(rec):
|
||||
rows = [row(rock="established", created=ago(days=30), washed=ago(days=1))]
|
||||
rows += [row(created=ago(days=1), washed=ago(days=1)) for _ in range(19)]
|
||||
_check_case(rec, "check-negative-drift-only", rows)
|
||||
|
||||
|
||||
def c_check_stall_only(rec):
|
||||
rows = [row(rock="established", created=ago(days=40), washed=ago(days=20))
|
||||
for _ in range(10)]
|
||||
_check_case(rec, "check-stall-only", rows)
|
||||
|
||||
|
||||
def c_check_stall_boundary(rec):
|
||||
rows = [row(rock="established", created=ago(days=40), washed=ago(days=14))
|
||||
for _ in range(10)]
|
||||
_check_case(rec, "check-stall-boundary-14d-no-alert", rows)
|
||||
|
||||
|
||||
def c_check_stall_15(rec):
|
||||
rows = [row(rock="established", created=ago(days=40), washed=ago(days=15))
|
||||
for _ in range(10)]
|
||||
_check_case(rec, "check-stall-15d-fires", rows)
|
||||
|
||||
|
||||
def c_check_malformed_last_washed(rec):
|
||||
# MAX picks "garbage" ('g' > '2'); oracle fromisoformat → ValueError skip.
|
||||
rows = [row(rock="established", created=ago(days=30), washed="garbage")]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=30))
|
||||
for _ in range(9)]
|
||||
_check_case(rec, "check-malformed-last-washed-skipped", rows)
|
||||
|
||||
|
||||
def c_check_naive_last_washed(rec):
|
||||
# Naive (no offset) MAX(last_washed): aware − naive → TypeError skip.
|
||||
naive = (FROZEN_UTC - timedelta(days=14)).replace(tzinfo=None).isoformat()
|
||||
rows = [row(rock="established", created=ago(days=30), washed=naive)]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=30))
|
||||
for _ in range(9)]
|
||||
_check_case(rec, "check-naive-last-washed-skipped", rows)
|
||||
|
||||
|
||||
def c_check_null_last_washed(rec):
|
||||
# All-NULL MAX → falsy → skip.
|
||||
rows = [row(rock="established", created=ago(days=30)) for _ in range(10)]
|
||||
_check_case(rec, "check-null-last-washed-skipped", rows)
|
||||
|
||||
|
||||
def c_check_empty_string_last_washed(rec):
|
||||
# MAX = "" → falsy → the oracle's `if last_promo and last_promo[0]` skips.
|
||||
rows = [row(rock="established", created=ago(days=30), washed="")
|
||||
for _ in range(10)]
|
||||
_check_case(rec, "check-empty-string-last-washed-skipped", rows)
|
||||
|
||||
|
||||
def c_check_all_three(rec):
|
||||
rows = [row(created=ago(days=1), washed=ago(days=1)) for _ in range(100)]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=20))
|
||||
for _ in range(3)]
|
||||
_check_case(rec, "check-all-three", rows)
|
||||
|
||||
|
||||
# ── run_retrospective ────────────────────────────────────────────────────────
|
||||
|
||||
def _retro_case(rec, name, rows, days):
|
||||
db = make_db(rows)
|
||||
cutoff = (FROZEN_UTC - timedelta(days=days)).isoformat()
|
||||
counts = {
|
||||
"new_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE created_at > ? AND status = 'active'",
|
||||
(cutoff,)).fetchone()[0],
|
||||
"washed_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'",
|
||||
(cutoff,)).fetchone()[0],
|
||||
"promoted_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE rock_status != 'washable'"
|
||||
" AND last_washed > ? AND status = 'active'",
|
||||
(cutoff,)).fetchone()[0],
|
||||
"decayed_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE status = 'deleted'"
|
||||
" AND last_accessed > ?", (cutoff,)).fetchone()[0],
|
||||
}
|
||||
velocity = drift_core.compute_drift_velocity(db, days)
|
||||
direction = drift_core.compute_drift_direction(db)
|
||||
obs = fresh_obs([])
|
||||
report = obs.run_retrospective(db, days)
|
||||
inp = dict(counts, velocity=velocity, direction=direction, days=days,
|
||||
now=FROZEN_NOW, now_local=FROZEN_LOCAL_ISO)
|
||||
rec(name, inp, report,
|
||||
{"report": report, "filename": FROZEN_LOCAL.strftime("%Y%m%d_%H%M%S")})
|
||||
|
||||
|
||||
def c_retro_positive(rec):
|
||||
rows = [row(rock="established", created=ago(days=30), washed=ago(days=1))
|
||||
for _ in range(10)]
|
||||
rows.append(row(created=ago(days=30), status="deleted", accessed=ago(hours=2)))
|
||||
_retro_case(rec, "retro-positive", rows, 7)
|
||||
|
||||
|
||||
def c_retro_neutral_high_velocity(rec):
|
||||
rows = [row(created=ago(days=1), washed=ago(days=1)) for _ in range(71)]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=1))
|
||||
for _ in range(40)]
|
||||
_retro_case(rec, "retro-neutral-high-velocity", rows, 7)
|
||||
|
||||
|
||||
def c_retro_neutral_low_velocity(rec):
|
||||
rows = [row(rock="established", created=ago(days=30), washed=ago(days=30))
|
||||
for _ in range(2)]
|
||||
rows += [row(created=ago(days=30), washed=ago(days=30)) for _ in range(8)]
|
||||
rows.append(row(status="deleted", accessed=ago(days=1)))
|
||||
_retro_case(rec, "retro-neutral-low-velocity", rows, 7)
|
||||
|
||||
|
||||
def c_retro_concerning(rec):
|
||||
rows = [row(rock="established", created=ago(days=30), washed=ago(days=30))]
|
||||
rows += [row(created=ago(days=30), washed=ago(days=30)) for _ in range(19)]
|
||||
_retro_case(rec, "retro-concerning", rows, 7)
|
||||
|
||||
|
||||
def c_retro_concerning_boundary(rec):
|
||||
rows = [row(rock="established", created=ago(days=30), washed=ago(days=30))]
|
||||
rows += [row(created=ago(days=30), washed=ago(days=30)) for _ in range(9)]
|
||||
_retro_case(rec, "retro-concerning-boundary-ratio-0.1", rows, 7)
|
||||
|
||||
|
||||
def c_retro_velocity_boundary(rec):
|
||||
rows = [row(created=ago(days=1), washed=ago(days=1)) for _ in range(70)]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=30))
|
||||
for _ in range(40)]
|
||||
_retro_case(rec, "retro-positive-velocity-boundary-10", rows, 7)
|
||||
|
||||
|
||||
def c_retro_custom_days(rec):
|
||||
rows = [row(created=ago(days=1), washed=ago(days=1)) for _ in range(5)]
|
||||
rows += [row(rock="established", created=ago(days=30), washed=ago(days=2))
|
||||
for _ in range(2)]
|
||||
rows.append(row(status="deleted", accessed=ago(days=2)))
|
||||
_retro_case(rec, "retro-custom-days-3", rows, 3)
|
||||
|
||||
|
||||
def c_retro_empty_db(rec):
|
||||
_retro_case(rec, "retro-empty-db-concerning", [], 7)
|
||||
|
||||
|
||||
def c_retro_default_days(rec):
|
||||
rows = [row(created=ago(days=2), washed=ago(days=2)) for _ in range(3)]
|
||||
_retro_case(rec, "retro-default-days-7", rows, 7)
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
ADD = [c_add_default, c_add_warning, c_add_critical, c_add_onto_existing]
|
||||
UNACKED = [c_unacked_empty, c_unacked_mixed, c_unacked_all_open]
|
||||
ACK = [c_ack_first, c_ack_second, c_ack_negative, c_ack_at_len, c_ack_beyond]
|
||||
CHECK = [c_check_empty_db, c_check_all_clear, c_check_velocity_only,
|
||||
c_check_negative_drift_only, c_check_stall_only, c_check_stall_boundary,
|
||||
c_check_stall_15, c_check_malformed_last_washed, c_check_naive_last_washed,
|
||||
c_check_null_last_washed, c_check_empty_string_last_washed,
|
||||
c_check_all_three]
|
||||
RETRO = [c_retro_positive, c_retro_neutral_high_velocity,
|
||||
c_retro_neutral_low_velocity, c_retro_concerning,
|
||||
c_retro_concerning_boundary, c_retro_velocity_boundary,
|
||||
c_retro_custom_days, c_retro_empty_db, c_retro_default_days]
|
||||
|
||||
GROUPS = [("add_alert", ADD), ("get_unacknowledged_alerts", UNACKED),
|
||||
("acknowledge_alert", ACK), ("check_alerts", CHECK),
|
||||
("run_retrospective", RETRO)]
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, oracle_ret, expected, _r=recs):
|
||||
entry = {"name": name, "input": inp, "return": oracle_ret,
|
||||
"expected": expected}
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if json.dumps(got, sort_keys=True) != json.dumps(want, sort_keys=True):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_failures = sum(1 for v in groups.values() for c in v if "diverged" in c)
|
||||
|
||||
# Kernel-level: the Rust core must reproduce the frozen expected
|
||||
# from the frozen input even WITHOUT the facade in the loop. The
|
||||
# loader unwraps the doc-envelope for the list-returning calls.
|
||||
unwrap_want = {
|
||||
"add_alert": lambda w: w["alerts"],
|
||||
"get_unacknowledged_alerts": lambda w: w["unacked"],
|
||||
"acknowledge_alert": lambda w: w,
|
||||
"check_alerts": lambda w: w,
|
||||
"run_retrospective": lambda w: w,
|
||||
}
|
||||
failures = 0
|
||||
for group, cases in frozen.items():
|
||||
for case in cases:
|
||||
got = getattr(do_loader, group)(**copy.deepcopy(case["input"]))
|
||||
want = unwrap_want[group](case["expected"])
|
||||
if json.dumps(got, sort_keys=True) != json.dumps(want, sort_keys=True):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {total - failures}/{total}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/drift_observability.py",
|
||||
"pin": "RUNE_DO_CORE=python",
|
||||
"note": "Frozen via scripts/gen_drift_observability_truth.py against "
|
||||
"the live Python oracle; wall clock frozen at "
|
||||
+ FROZEN_NOW + ". Battery embeds this VERBATIM.",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1, sort_keys=True)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/message_queue
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,633 @@
|
||||
// Package main — the FULL GO port of src/rune/message_queue.py (#2479
|
||||
// dichotomy tranche, GO side verdict). Every public Python function has a
|
||||
// name-preserving counterpart: MessageQueue (__init__ → NewMessageQueue,
|
||||
// _load_queue → loadQueue, _save_queue → saveQueue, detect_task_addition,
|
||||
// add_message, get_pending, approve, reject, approve_all, clear,
|
||||
// get_approved, mark_executed, get_status) and the module CLI main()
|
||||
// (exact stdout, incl. the ✅/❌ lines). IO (queue file, home dir, clock)
|
||||
// stays behind injectable seams so the frozen truth battery replays
|
||||
// deterministically.
|
||||
//
|
||||
// Supersedes the batch-61/148 partial in ../message-queue (which carried
|
||||
// only the detect_task_addition scoring kernel); that dir stays as the
|
||||
// scored-kernel record, this module is the FULL surface.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── configuration ────────────────────────────────────────────────────────
|
||||
|
||||
// TASK_ADDITION_PATTERNS — the oracle's 25-row table, 1:1.
|
||||
var taskAdditionPatterns = []string{
|
||||
`\balso\b`,
|
||||
`\band can you\b`,
|
||||
`\bplus\b`,
|
||||
`\badditionally\b`,
|
||||
`\bafter that\b`,
|
||||
`\bthen\b`,
|
||||
`\balso need\b`,
|
||||
`\balso want\b`,
|
||||
`\bcan you also\b`,
|
||||
`\bcan you add\b`,
|
||||
`\bcan you update\b`,
|
||||
`\bcan you fix\b`,
|
||||
`\bcan you create\b`,
|
||||
`\bcan you implement\b`,
|
||||
`\bcan you build\b`,
|
||||
`\bmake sure\b`,
|
||||
// the oracle stores this as a RAW string — the backslash before the
|
||||
// apostrophe is part of the pattern AND of the emitted signal text
|
||||
`\bdon\'t forget\b`,
|
||||
`\bremember to\b`,
|
||||
`\binclude\b`,
|
||||
`\badd\b.*\bto\b`,
|
||||
`\bupdate\b.*\bto\b`,
|
||||
`\bfix\b.*\bto\b`,
|
||||
`\bcreate\b.*\bto\b`,
|
||||
`\bimplement\b.*\bto\b`,
|
||||
`\bbuild\b.*\bto\b`,
|
||||
}
|
||||
|
||||
var compiledPatterns = func() []*regexp.Regexp {
|
||||
out := make([]*regexp.Regexp, len(taskAdditionPatterns))
|
||||
for i, p := range taskAdditionPatterns {
|
||||
out[i] = regexp.MustCompile(p)
|
||||
}
|
||||
return out
|
||||
}()
|
||||
|
||||
var imperativeVerbs = []string{
|
||||
"add", "update", "fix", "create", "implement", "build", "make",
|
||||
"change", "remove", "delete", "refactor", "migrate",
|
||||
}
|
||||
|
||||
var fileRefRe = regexp.MustCompile(`\b\w+\.\w+\b`)
|
||||
|
||||
// ── status lattice (Phase B: typed, not string soup) ─────────────────────
|
||||
|
||||
// MessageStatus — the lifecycle state of a queued message. A typed lattice
|
||||
// replaces bare string comparisons: illegal states are unrepresentable at
|
||||
// assignment sites, and the JSON boundary converts explicitly.
|
||||
type MessageStatus string
|
||||
|
||||
const (
|
||||
StatusPending MessageStatus = "pending"
|
||||
StatusApproved MessageStatus = "approved"
|
||||
StatusRejected MessageStatus = "rejected"
|
||||
StatusExecuted MessageStatus = "executed"
|
||||
)
|
||||
|
||||
// parseMessageStatus — JSON-boundary conversion; unknown values pass
|
||||
// through verbatim (the store is load-bearing data, the oracle round-trips
|
||||
// whatever it wrote).
|
||||
func parseMessageStatus(s string) MessageStatus { return MessageStatus(s) }
|
||||
|
||||
// ── data structures ──────────────────────────────────────────────────────
|
||||
|
||||
// QueuedMessage — the QueuedMessage dataclass (field order is load-bearing
|
||||
// for the JSON store bytes: id, message, timestamp, channel, status,
|
||||
// task_id, confidence, signals).
|
||||
type QueuedMessage struct {
|
||||
ID string
|
||||
Message string
|
||||
Timestamp string
|
||||
Channel string
|
||||
Status MessageStatus // pending, approved, rejected, executed
|
||||
TaskID *string
|
||||
Confidence float64
|
||||
Signals []string
|
||||
}
|
||||
|
||||
// toDict — asdict(), Python key order; nil optionals emit like the
|
||||
// dataclass defaults (task_id: null, signals: []).
|
||||
func (m *QueuedMessage) toDict() *OrderedMap {
|
||||
signals := m.Signals
|
||||
if signals == nil {
|
||||
signals = []string{}
|
||||
}
|
||||
var taskID any
|
||||
if m.TaskID != nil {
|
||||
taskID = *m.TaskID
|
||||
}
|
||||
items := make([]any, 0, len(signals))
|
||||
for _, s := range signals {
|
||||
items = append(items, s)
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"id", m.ID},
|
||||
KV{"message", m.Message},
|
||||
KV{"timestamp", m.Timestamp},
|
||||
KV{"channel", m.Channel},
|
||||
KV{"status", string(m.Status)},
|
||||
KV{"task_id", taskID},
|
||||
KV{"confidence", m.Confidence},
|
||||
KV{"signals", items},
|
||||
)
|
||||
}
|
||||
|
||||
// queuedMessageFromDict rebuilds a QueuedMessage from a parsed JSON object
|
||||
// (absent optional fields behave like the dataclass defaults).
|
||||
func queuedMessageFromDict(om *OrderedMap) QueuedMessage {
|
||||
m := QueuedMessage{
|
||||
Status: StatusPending,
|
||||
Signals: []string{},
|
||||
}
|
||||
if v, ok := om.Get("id"); ok {
|
||||
m.ID, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("message"); ok {
|
||||
m.Message, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("timestamp"); ok {
|
||||
m.Timestamp, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("channel"); ok {
|
||||
m.Channel, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("status"); ok {
|
||||
if s, ok2 := v.(string); ok2 {
|
||||
m.Status = parseMessageStatus(s)
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("task_id"); ok {
|
||||
if s, ok2 := v.(string); ok2 {
|
||||
m.TaskID = &s
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("confidence"); ok {
|
||||
if f, ok2 := v.(float64); ok2 {
|
||||
m.Confidence = f
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("signals"); ok {
|
||||
if arr, ok2 := v.([]any); ok2 {
|
||||
m.Signals = make([]string, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
s, _ := item.(string)
|
||||
m.Signals = append(m.Signals, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ── seams (swapped per truth row by replay.go) ───────────────────────────
|
||||
|
||||
var (
|
||||
// defaultHomeDir — os.UserHomeDir() for the default queue root.
|
||||
defaultHomeDir = func() string {
|
||||
h, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return h
|
||||
}
|
||||
// defaultClockNow — datetime.now(timezone.utc); add_message calls it
|
||||
// twice (msg_id first, timestamp second), exactly like the oracle.
|
||||
defaultClockNow = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
)
|
||||
|
||||
// Seams — swapped per truth row by replay.go, restored after each row.
|
||||
var (
|
||||
HomeDir = defaultHomeDir
|
||||
ClockNow = defaultClockNow
|
||||
)
|
||||
|
||||
func defaultQueueDir() string {
|
||||
return filepath.Join(HomeDir(), ".rune", "message_queue")
|
||||
}
|
||||
|
||||
// isoformat — datetime.isoformat() on an aware UTC datetime: fractional
|
||||
// part only when microseconds are nonzero, offset always "+00:00" shape.
|
||||
func isoformat(t time.Time) string {
|
||||
base := t.Format("2006-01-02T15:04:05")
|
||||
if us := t.Nanosecond() / 1000; us != 0 {
|
||||
base += fmt.Sprintf(".%06d", us)
|
||||
}
|
||||
return base + t.Format("-07:00")
|
||||
}
|
||||
|
||||
// ── queue management (MessageQueue) ──────────────────────────────────────
|
||||
|
||||
// QueueStore — the persistence seam (Phase B: the interface is the
|
||||
// contract; the file implementation is swappable, in-memory stores slot in
|
||||
// for tests). Load/Save carry the RAW queue.json bytes — encoding parity
|
||||
// lives in saveQueue, not in the store.
|
||||
type QueueStore interface {
|
||||
Load() ([]byte, error)
|
||||
Save(data []byte) error
|
||||
}
|
||||
|
||||
// compile-time: the file store satisfies the seam
|
||||
var _ QueueStore = (*FileQueueStore)(nil)
|
||||
|
||||
// FileQueueStore — queue.json under the queue dir.
|
||||
type FileQueueStore struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (s *FileQueueStore) Load() ([]byte, error) { return os.ReadFile(s.Path) }
|
||||
|
||||
func (s *FileQueueStore) Save(data []byte) error {
|
||||
return os.WriteFile(s.Path, data, 0o644)
|
||||
}
|
||||
|
||||
// MessageQueue — manages the message queue for task additions. Safe for
|
||||
// concurrent use (Phase B: RWMutex — the TUI consumes this module from
|
||||
// multiple goroutines; the Python oracle relied on the GIL).
|
||||
type MessageQueue struct {
|
||||
Store QueueStore
|
||||
|
||||
mu sync.RWMutex
|
||||
queue []QueuedMessage
|
||||
}
|
||||
|
||||
// NewMessageQueue — MessageQueue.__init__: makedirs(QUEUE_DIR,
|
||||
// exist_ok=True) then _load_queue, over the default file store.
|
||||
func NewMessageQueue(queueDir string) *MessageQueue {
|
||||
if err := os.MkdirAll(queueDir, 0o777); err != nil {
|
||||
panic(fmt.Sprintf("makedirs %s: %v", queueDir, err))
|
||||
}
|
||||
return NewMessageQueueWithStore(&FileQueueStore{
|
||||
Path: filepath.Join(queueDir, "queue.json"),
|
||||
})
|
||||
}
|
||||
|
||||
// NewMessageQueueWithStore — constructor for injectable stores (tests,
|
||||
// embedded callers). Loads immediately, like __init__.
|
||||
func NewMessageQueueWithStore(store QueueStore) *MessageQueue {
|
||||
q := &MessageQueue{Store: store}
|
||||
q.queue = q.loadQueue()
|
||||
return q
|
||||
}
|
||||
|
||||
// loadQueue — _load_queue: missing file → empty; a malformed file raises
|
||||
// unhandled in the oracle (json.JSONDecodeError), mirrored as a panic.
|
||||
func (q *MessageQueue) loadQueue() []QueuedMessage {
|
||||
raw, err := q.Store.Load()
|
||||
if err != nil {
|
||||
return []QueuedMessage{}
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("queue.json: %v", err))
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '[' {
|
||||
panic("queue.json: not a JSON array")
|
||||
}
|
||||
out := []QueuedMessage{}
|
||||
for dec.More() {
|
||||
v, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("queue.json: %v", err))
|
||||
}
|
||||
om, ok := v.(*OrderedMap)
|
||||
if !ok {
|
||||
panic("queue.json: item not an object")
|
||||
}
|
||||
out = append(out, queuedMessageFromDict(om))
|
||||
}
|
||||
if _, err := dec.Token(); err != nil {
|
||||
panic(fmt.Sprintf("queue.json: %v", err))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// saveQueue — _save_queue: json.dump([asdict(msg) ...], indent=2) —
|
||||
// byte-parity via the Python JSON encoder, persisted through the store.
|
||||
func (q *MessageQueue) saveQueue() {
|
||||
items := make([]*OrderedMap, 0, len(q.queue))
|
||||
for i := range q.queue {
|
||||
items = append(items, q.queue[i].toDict())
|
||||
}
|
||||
if err := q.Store.Save([]byte(encodePythonJSON(items, 0))); err != nil {
|
||||
panic(fmt.Sprintf("save queue: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// snapshot — a private copy of the queue under RLock (callers outside the
|
||||
// mutex must not share the backing array).
|
||||
func (q *MessageQueue) snapshot() []QueuedMessage {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
out := make([]QueuedMessage, len(q.queue))
|
||||
copy(out, q.queue)
|
||||
return out
|
||||
}
|
||||
|
||||
// detectTaskAddition — returns (is_task_addition, confidence, signals);
|
||||
// Python-identical scoring: patterns +0.3 (table order), imperative-verb
|
||||
// prefix +0.4, file refs +0.2, "also"-first +0.5; cap 1.0; detected iff
|
||||
// confidence > 0.5.
|
||||
func (q *MessageQueue) detectTaskAddition(message string) (bool, float64, []string) {
|
||||
signals := []string{}
|
||||
confidence := 0.0
|
||||
messageLower := strings.ToLower(message)
|
||||
|
||||
for i, re := range compiledPatterns {
|
||||
if re.MatchString(messageLower) {
|
||||
signals = append(signals, fmt.Sprintf("Pattern: %s", taskAdditionPatterns[i]))
|
||||
confidence += 0.3
|
||||
}
|
||||
}
|
||||
for _, verb := range imperativeVerbs {
|
||||
if strings.HasPrefix(messageLower, verb) {
|
||||
signals = append(signals, fmt.Sprintf("Imperative verb: %s", verb))
|
||||
confidence += 0.4
|
||||
}
|
||||
}
|
||||
fileRefs := fileRefRe.FindAllString(message, -1)
|
||||
if len(fileRefs) > 0 {
|
||||
head := fileRefs
|
||||
if len(head) > 3 {
|
||||
head = head[:3]
|
||||
}
|
||||
signals = append(signals, fmt.Sprintf("File references: %s", strings.Join(head, ", ")))
|
||||
confidence += 0.2
|
||||
}
|
||||
if strings.HasPrefix(messageLower, "also") {
|
||||
signals = append(signals, "Starts with 'also'")
|
||||
confidence += 0.5
|
||||
}
|
||||
|
||||
if confidence > 1.0 {
|
||||
confidence = 1.0
|
||||
}
|
||||
isTaskAddition := confidence > 0.5
|
||||
return isTaskAddition, confidence, signals
|
||||
}
|
||||
|
||||
// addMessage — add_message: detect, id from the FIRST now() call
|
||||
// (%Y%m%d-%H%M%S-%f), timestamp from the SECOND (isoformat), append, save.
|
||||
func (q *MessageQueue) addMessage(message, channel string, taskID *string) QueuedMessage {
|
||||
_, confidence, signals := q.detectTaskAddition(message)
|
||||
|
||||
nowID := ClockNow()
|
||||
// strftime("%Y%m%d-%H%M%S-%f") — %f is zero-padded microseconds; Go's
|
||||
// time layout only understands fractional seconds after a '.', so the
|
||||
// µs field is appended explicitly.
|
||||
msgID := fmt.Sprintf("%s-%06d", nowID.Format("20060102-150405"), nowID.Nanosecond()/1000)
|
||||
nowTS := ClockNow()
|
||||
queued := QueuedMessage{
|
||||
ID: msgID,
|
||||
Message: message,
|
||||
Timestamp: isoformat(nowTS),
|
||||
Channel: channel,
|
||||
Status: StatusPending,
|
||||
TaskID: taskID,
|
||||
Confidence: confidence,
|
||||
Signals: signals,
|
||||
}
|
||||
q.mu.Lock()
|
||||
q.queue = append(q.queue, queued)
|
||||
q.saveQueue()
|
||||
q.mu.Unlock()
|
||||
return queued
|
||||
}
|
||||
|
||||
// getPending — get_pending: all messages with status "pending", in order.
|
||||
func (q *MessageQueue) getPending() []QueuedMessage {
|
||||
out := []QueuedMessage{}
|
||||
for _, m := range q.snapshot() {
|
||||
if m.Status == StatusPending {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// approve — approve(msg_id): found → status approved + save + true.
|
||||
func (q *MessageQueue) approve(msgID string) bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
for i := range q.queue {
|
||||
if q.queue[i].ID == msgID {
|
||||
q.queue[i].Status = StatusApproved
|
||||
q.saveQueue()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// reject — reject(msg_id): found → status rejected + save + true.
|
||||
func (q *MessageQueue) reject(msgID string) bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
for i := range q.queue {
|
||||
if q.queue[i].ID == msgID {
|
||||
q.queue[i].Status = StatusRejected
|
||||
q.saveQueue()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// approveAll — approve_all(): approve every pending, save once (even when
|
||||
// the queue is empty), return the count.
|
||||
func (q *MessageQueue) approveAll() int {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
count := 0
|
||||
for i := range q.queue {
|
||||
if q.queue[i].Status == StatusPending {
|
||||
q.queue[i].Status = StatusApproved
|
||||
count++
|
||||
}
|
||||
}
|
||||
q.saveQueue()
|
||||
return count
|
||||
}
|
||||
|
||||
// clear — clear(): drop all messages, save.
|
||||
func (q *MessageQueue) clear() {
|
||||
q.mu.Lock()
|
||||
q.queue = []QueuedMessage{}
|
||||
q.saveQueue()
|
||||
q.mu.Unlock()
|
||||
}
|
||||
|
||||
// getApproved — get_approved: all messages with status "approved", in order.
|
||||
func (q *MessageQueue) getApproved() []QueuedMessage {
|
||||
out := []QueuedMessage{}
|
||||
for _, m := range q.snapshot() {
|
||||
if m.Status == StatusApproved {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// markExecuted — mark_executed(msg_id): found → status executed + save + true.
|
||||
func (q *MessageQueue) markExecuted(msgID string) bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
for i := range q.queue {
|
||||
if q.queue[i].ID == msgID {
|
||||
q.queue[i].Status = StatusExecuted
|
||||
q.saveQueue()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getStatus — get_status: {total, pending, approved, executed}.
|
||||
func (q *MessageQueue) getStatus() *OrderedMap {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
pending, approved, executed := 0, 0, 0
|
||||
for _, m := range q.queue {
|
||||
switch m.Status {
|
||||
case StatusPending:
|
||||
pending++
|
||||
case StatusApproved:
|
||||
approved++
|
||||
case StatusExecuted:
|
||||
executed++
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"total", len(q.queue)},
|
||||
KV{"pending", pending},
|
||||
KV{"approved", approved},
|
||||
KV{"executed", executed},
|
||||
)
|
||||
}
|
||||
|
||||
// ── CLI (message_queue.py main(), byte-parity stdout) ────────────────────
|
||||
|
||||
// RunCLI executes message_queue.py main() against argv (program name at
|
||||
// index 0) and returns the process exit code.
|
||||
func RunCLI(argv []string, w io.Writer) int {
|
||||
if len(argv) < 2 {
|
||||
fmt.Fprintln(w, "Usage: message_queue.py [add|list|clear|approve|reject|status]")
|
||||
return 1
|
||||
}
|
||||
cmd := argv[1]
|
||||
q := NewMessageQueue(defaultQueueDir())
|
||||
|
||||
switch cmd {
|
||||
case "add":
|
||||
if len(argv) < 3 {
|
||||
fmt.Fprintln(w, "Usage: message_queue.py add <message>")
|
||||
return 1
|
||||
}
|
||||
message := strings.Join(argv[2:], " ")
|
||||
queued := q.addMessage(message, "chat", nil)
|
||||
fmt.Fprintf(w, "✅ Added to queue: %s\n", queued.ID)
|
||||
fmt.Fprintf(w, " Confidence: %.2f\n", queued.Confidence)
|
||||
fmt.Fprintf(w, " Signals: %s\n", strings.Join(queued.Signals, ", "))
|
||||
|
||||
case "list":
|
||||
pending := q.getPending()
|
||||
if len(pending) > 0 {
|
||||
fmt.Fprintf(w, "Pending messages: %d\n", len(pending))
|
||||
for _, m := range pending {
|
||||
fmt.Fprintf(w, " %s: %s... (confidence: %.2f)\n", m.ID, runeTrunc(m.Message, 50), m.Confidence)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(w, "No pending messages.")
|
||||
}
|
||||
|
||||
case "clear":
|
||||
q.clear()
|
||||
fmt.Fprintln(w, "✅ Queue cleared")
|
||||
|
||||
case "approve":
|
||||
if len(argv) < 3 {
|
||||
count := q.approveAll()
|
||||
fmt.Fprintf(w, "✅ Approved %d messages\n", count)
|
||||
} else {
|
||||
msgID := argv[2]
|
||||
if q.approve(msgID) {
|
||||
fmt.Fprintf(w, "✅ Approved: %s\n", msgID)
|
||||
} else {
|
||||
fmt.Fprintf(w, "❌ Message not found: %s\n", msgID)
|
||||
}
|
||||
}
|
||||
|
||||
case "reject":
|
||||
if len(argv) < 3 {
|
||||
fmt.Fprintln(w, "Usage: message_queue.py reject <msg_id>")
|
||||
return 1
|
||||
}
|
||||
msgID := argv[2]
|
||||
if q.reject(msgID) {
|
||||
fmt.Fprintf(w, "✅ Rejected: %s\n", msgID)
|
||||
} else {
|
||||
fmt.Fprintf(w, "❌ Message not found: %s\n", msgID)
|
||||
}
|
||||
|
||||
case "status":
|
||||
status := q.getStatus()
|
||||
fmt.Fprintln(w, "Queue status:")
|
||||
fmt.Fprintf(w, " Total: %d\n", status.Get2("total"))
|
||||
fmt.Fprintf(w, " Pending: %d\n", status.Get2("pending"))
|
||||
fmt.Fprintf(w, " Approved: %d\n", status.Get2("approved"))
|
||||
fmt.Fprintf(w, " Executed: %d\n", status.Get2("executed"))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(w, "Unknown command: %s\n", cmd)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// runeTrunc — Python's msg[:50] (codepoint slice).
|
||||
func runeTrunc(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n])
|
||||
}
|
||||
|
||||
// ── entry point: CLI + truth replay subcommand for the HARD CI gate ─────
|
||||
|
||||
func truthRowCount() int {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Fprintln(os.Stderr, f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d truth rows exact-equal vs the Python oracle\n", truthRowCount())
|
||||
return
|
||||
}
|
||||
os.Exit(RunCLI(os.Args, os.Stdout))
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
// Phase B battery — pins the language-advantage guarantees: the QueueStore
|
||||
// seam is swappable (compile-time asserted in main.go), mutations are
|
||||
// goroutine-safe under -race, and the typed MessageStatus lattice keeps
|
||||
// the store bytes byte-identical to the Phase A parity gate.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// memStore — in-memory QueueStore (the seam's whole point).
|
||||
type memStore struct {
|
||||
mu sync.Mutex
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (s *memStore) Load() ([]byte, error) {
|
||||
if s.data == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return s.data, nil
|
||||
}
|
||||
|
||||
func (s *memStore) Save(d []byte) error {
|
||||
s.mu.Lock()
|
||||
s.data = append([]byte(nil), d...)
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestPhaseBStoreSeamSwappable — the interface seam accepts an in-memory
|
||||
// store; under the same frozen clock both stores produce byte-identical
|
||||
// serialization.
|
||||
func TestPhaseBStoreSeamSwappable(t *testing.T) {
|
||||
defer func() { ClockNow = defaultClockNow }()
|
||||
base, _ := time.Parse(time.RFC3339, "2026-10-11T12:00:00+00:00")
|
||||
calls := 0
|
||||
ClockNow = func() time.Time {
|
||||
calls++
|
||||
return base.Add(time.Duration(calls) * time.Microsecond)
|
||||
}
|
||||
|
||||
memQ := NewMessageQueueWithStore(&memStore{})
|
||||
memQ.addMessage("also fix config.py to log properly", "chat", nil)
|
||||
|
||||
calls = 0
|
||||
fileQ := NewMessageQueue(t.TempDir())
|
||||
fileQ.addMessage("also fix config.py to log properly", "chat", nil)
|
||||
|
||||
want := memQ.queue[0].toDict()
|
||||
got := fileQ.queue[0].toDict()
|
||||
if memStr, fileStr := mustJSON(want), mustJSON(got); memStr != fileStr {
|
||||
t.Fatalf("store swap changed serialization:\n file: %s\n mem: %s", fileStr, memStr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBConcurrentMutations — concurrent add/approve/status calls must
|
||||
// not corrupt the store or lose messages (run under -race in CI).
|
||||
func TestPhaseBConcurrentMutations(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
q := NewMessageQueue(dir)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
q.addMessage(fmt.Sprintf("also add feature %d to config.py", i), "chat", nil)
|
||||
q.getStatus()
|
||||
q.getPending()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
pending := q.getPending()
|
||||
if len(pending) != 8 {
|
||||
t.Fatalf("concurrent adds lost messages: %d/8", len(pending))
|
||||
}
|
||||
// store file re-parses to the same count
|
||||
q2 := NewMessageQueue(dir)
|
||||
if len(q2.queue) != 8 {
|
||||
t.Fatalf("reloaded store lost messages: %d/8", len(q2.queue))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBStatusLattice — the typed statuses serialize exactly like the
|
||||
// oracle strings, and the parse round-trip is verbatim.
|
||||
func TestPhaseBStatusLattice(t *testing.T) {
|
||||
for _, s := range []MessageStatus{StatusPending, StatusApproved, StatusRejected, StatusExecuted} {
|
||||
if parseMessageStatus(string(s)) != s {
|
||||
t.Fatalf("status round-trip broken for %q", s)
|
||||
}
|
||||
}
|
||||
m := QueuedMessage{ID: "x", Status: StatusExecuted, Signals: []string{}}
|
||||
if got := mustJSON(m.toDict()); !strings.Contains(got, `"status":"executed"`) {
|
||||
t.Fatalf("typed status changed serialization: %s", got)
|
||||
}
|
||||
// unknown statuses in the store pass through verbatim (oracle behavior)
|
||||
om := NewOrderedMap(
|
||||
KV{"id", "weird"}, KV{"message", "m"}, KV{"timestamp", "t"},
|
||||
KV{"channel", "chat"}, KV{"status", "weird-state"},
|
||||
KV{"task_id", nil}, KV{"confidence", 0.0}, KV{"signals", []any{}},
|
||||
)
|
||||
parsed := queuedMessageFromDict(om)
|
||||
if parsed.Status != MessageStatus("weird-state") {
|
||||
t.Fatalf("unknown status not passed through: %q", parsed.Status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent=2, ensure_ascii=True) so _save_queue and CLI output are
|
||||
// byte-parity with the oracle (adapted from the goal port's pyjson.go; the
|
||||
// float renderer follows Python repr, which keeps ".0" on integral floats).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// KV — one ordered key/value pair.
|
||||
type KV struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
// OrderedMap — a dict with preserved insertion order.
|
||||
type OrderedMap struct {
|
||||
Keys []string
|
||||
Vals map[string]any
|
||||
}
|
||||
|
||||
// NewOrderedMap builds an ordered map from pairs.
|
||||
func NewOrderedMap(pairs ...KV) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for _, kv := range pairs {
|
||||
om.Set(kv.Key, kv.Value)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// OrderedMapFromMap wraps a plain map (order unspecified — canonical
|
||||
// comparisons sort keys anyway).
|
||||
func OrderedMapFromMap(m map[string]any) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// Set inserts or replaces, preserving first-insertion order.
|
||||
func (o *OrderedMap) Set(key string, value any) {
|
||||
if _, ok := o.Vals[key]; !ok {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.Vals[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a value.
|
||||
func (o *OrderedMap) Get(key string) (any, bool) {
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Get2 returns the value at key (nil when absent) for direct type asserts.
|
||||
func (o *OrderedMap) Get2(key string) any {
|
||||
return o.Vals[key]
|
||||
}
|
||||
|
||||
// Entries returns key/value pairs in insertion order.
|
||||
func (o *OrderedMap) Entries() []KV {
|
||||
out := make([]KV, 0, len(o.Keys))
|
||||
for _, k := range o.Keys {
|
||||
out = append(out, KV{k, o.Vals[k]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of entries.
|
||||
func (o *OrderedMap) Len() int { return len(o.Keys) }
|
||||
|
||||
// decodeJSONValue decodes one JSON value from the decoder, decoding objects
|
||||
// as *OrderedMap (order-preserving) and numbers as float64.
|
||||
func decodeJSONValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeFromToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeFromToken(dec *json.Decoder, tok json.Token) (any, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
om.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume '}'
|
||||
return nil, err
|
||||
}
|
||||
return om, nil
|
||||
case '[':
|
||||
arr := []any{}
|
||||
for dec.More() {
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume ']'
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
default:
|
||||
return tok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSON renders v exactly like Python json.dumps(v, indent=N)
|
||||
// with default ensure_ascii=True and (',', ': ') separators.
|
||||
func encodePythonJSON(v any, indent int) string {
|
||||
var sb strings.Builder
|
||||
encodeValue(&sb, v, indent)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeValue(sb *strings.Builder, v any, indent int) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
if t.Len() == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
writeIndent(sb, indent+1)
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, indent+1)
|
||||
if i < len(entries)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeValue(sb, OrderedMapFromMap(t), indent)
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func writeIndent(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr (json.dumps uses
|
||||
// float.__repr__): shortest round-trip digits, and integral floats keep
|
||||
// one decimal place (1.0 stays "1.0", not "1").
|
||||
func pythonFloat(f float64) string {
|
||||
s := strconv.FormatFloat(f, 'g', -1, 64)
|
||||
if !strings.ContainsAny(s, ".eE") &&
|
||||
!strings.Contains(s, "Inf") && !strings.Contains(s, "NaN") {
|
||||
s += ".0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// pythonEscapeString — Python json.dumps string escaping with
|
||||
// ensure_ascii=True: control chars via \uXXXX (with \b \f \n \r \t
|
||||
// shortcuts), non-ASCII via \uXXXX / surrogate pairs, lowercase hex.
|
||||
func pythonEscapeString(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
sb.WriteString(`\"`)
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\b':
|
||||
sb.WriteString(`\b`)
|
||||
case '\f':
|
||||
sb.WriteString(`\f`)
|
||||
default:
|
||||
if r < 0x20 || r > 0x7e {
|
||||
if r > 0xffff {
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
fmt.Fprintf(&sb, `\u%04x\u%04x`, r1, r2)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, `\u%04x`, r)
|
||||
}
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// /tmp/gen_message_queue_truth.py against the live Python oracle,
|
||||
// RUNE_MESSAGE_QUEUE_CORE pinned to python) and re-executes every row
|
||||
// through this Go port, comparing results. Exact rows (store-file bytes,
|
||||
// CLI stdout) compare byte-for-byte; everything else compares canonically
|
||||
// (sorted-key JSON).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on 100%).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(&doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
// compareOne replays one row and returns "" on parity, else a divergence
|
||||
// description. Shared by RunTruth and the per-function battery tests.
|
||||
func compareOne(doc *TruthDoc, row *TruthRow) string {
|
||||
got, exact, err := execRow(doc, row)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: %v", err)
|
||||
}
|
||||
var wantStr, gotStr string
|
||||
if exact {
|
||||
var w string
|
||||
if err := json.Unmarshal(row.Want, &w); err != nil {
|
||||
return fmt.Sprintf("want not a string: %v", err)
|
||||
}
|
||||
wantStr, gotStr = w, got.(string)
|
||||
} else {
|
||||
wantStr, err = canonicalFromRaw(row.Want)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize want: %v", err)
|
||||
}
|
||||
gotStr, err = canonicalFromRaw([]byte(mustJSON(got)))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize got: %v", err)
|
||||
}
|
||||
}
|
||||
if wantStr != gotStr {
|
||||
return fmt.Sprintf("\n want: %s\n got: %s", wantStr, gotStr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// execRow executes one truth row: returns got (any) and, for exact rows,
|
||||
// the exact string in got.
|
||||
func execRow(doc *TruthDoc, row *TruthRow) (got any, exact bool, err error) {
|
||||
setup := NewOrderedMap()
|
||||
if len(row.Setup) > 0 {
|
||||
dec := json.NewDecoder(bytes.NewReader(row.Setup))
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return nil, false, fmt.Errorf("setup not an object")
|
||||
}
|
||||
v, err := decodeFromToken(dec, tok)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
setup = v.(*OrderedMap)
|
||||
}
|
||||
|
||||
// freeze the seams per row
|
||||
home, err := os.MkdirTemp("", "mq_truth_row_")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer os.RemoveAll(home)
|
||||
|
||||
defer func() {
|
||||
HomeDir = defaultHomeDir
|
||||
ClockNow = defaultClockNow
|
||||
}()
|
||||
HomeDir = func() string { return home }
|
||||
base, err := time.Parse(time.RFC3339, doc.FrozenTS)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
calls := 0
|
||||
ClockNow = func() time.Time {
|
||||
calls++
|
||||
return base.Add(time.Duration(calls) * time.Microsecond)
|
||||
}
|
||||
|
||||
seedFile := func() {
|
||||
if f, ok := setup.Get("file"); ok {
|
||||
s, _ := f.(string)
|
||||
os.MkdirAll(filepath.Join(home, ".rune", "message_queue"), 0o777)
|
||||
os.WriteFile(filepath.Join(home, ".rune", "message_queue", "queue.json"), []byte(s), 0o644)
|
||||
}
|
||||
}
|
||||
readStore := func() (string, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(home, ".rune", "message_queue", "queue.json"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
newQ := func() *MessageQueue { return NewMessageQueue(defaultQueueDir()) }
|
||||
strSetup := func(key string) string {
|
||||
s, _ := setup.Get2(key).(string)
|
||||
return s
|
||||
}
|
||||
|
||||
switch row.Op {
|
||||
case "detect":
|
||||
q := newQ()
|
||||
detected, confidence, signals := q.detectTaskAddition(strSetup("message"))
|
||||
sig := make([]any, 0, len(signals))
|
||||
for _, s := range signals {
|
||||
sig = append(sig, s)
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"detected", detected},
|
||||
KV{"confidence", confidence},
|
||||
KV{"signals", sig},
|
||||
), false, nil
|
||||
|
||||
case "add":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
var taskID *string
|
||||
if t, ok := setup.Get("task_id"); ok {
|
||||
if s, ok2 := t.(string); ok2 {
|
||||
taskID = &s
|
||||
}
|
||||
}
|
||||
m := q.addMessage(strSetup("message"), strSetup("channel"), taskID)
|
||||
return m.toDict(), false, nil
|
||||
|
||||
case "pending":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
return queuedList(q.getPending()), false, nil
|
||||
|
||||
case "get_approved":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
return queuedList(q.getApproved()), false, nil
|
||||
|
||||
case "approve", "reject", "mark_executed":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
var ok bool
|
||||
switch row.Op {
|
||||
case "approve":
|
||||
ok = q.approve(strSetup("msg_id"))
|
||||
case "reject":
|
||||
ok = q.reject(strSetup("msg_id"))
|
||||
case "mark_executed":
|
||||
ok = q.markExecuted(strSetup("msg_id"))
|
||||
}
|
||||
file, err := readStore()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return NewOrderedMap(KV{"ok", ok}, KV{"file", file}), false, nil
|
||||
|
||||
case "approve_all":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
count := q.approveAll()
|
||||
file, err := readStore()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return NewOrderedMap(KV{"count", count}, KV{"file", file}), false, nil
|
||||
|
||||
case "clear":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
q.clear()
|
||||
file, err := readStore()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return NewOrderedMap(KV{"file", file}, KV{"status", q.getStatus()}), false, nil
|
||||
|
||||
case "status":
|
||||
seedFile()
|
||||
q := newQ()
|
||||
return q.getStatus(), false, nil
|
||||
|
||||
case "load":
|
||||
// _load_queue branches: missing file / exact file bytes / empty file
|
||||
seedFile()
|
||||
q := newQ()
|
||||
return queuedList(q.queue), false, nil
|
||||
|
||||
case "save":
|
||||
// _save_queue byte parity: rebuild the setup store, save, read bytes
|
||||
store, _ := setup.Get("store")
|
||||
arr, ok := store.([]any)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("save: no store")
|
||||
}
|
||||
q := newQ()
|
||||
for _, item := range arr {
|
||||
if om, ok := item.(*OrderedMap); ok {
|
||||
q.queue = append(q.queue, queuedMessageFromDict(om))
|
||||
}
|
||||
}
|
||||
q.saveQueue()
|
||||
file, err := readStore()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return file, true, nil
|
||||
|
||||
case "cli":
|
||||
seedFile()
|
||||
argvAny, _ := setup.Get2("argv").([]any)
|
||||
argv := []string{"message_queue.py"}
|
||||
for _, a := range argvAny {
|
||||
s, _ := a.(string)
|
||||
argv = append(argv, s)
|
||||
}
|
||||
var sb strings.Builder
|
||||
code := RunCLI(argv, &sb)
|
||||
return NewOrderedMap(KV{"stdout", sb.String()}, KV{"exit", code}), false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("unknown row op %s (%s)", row.Op, row.Name)
|
||||
}
|
||||
|
||||
// queuedList renders messages as Python-ordered dicts.
|
||||
func queuedList(ms []QueuedMessage) []any {
|
||||
out := make([]any, 0, len(ms))
|
||||
for i := range ms {
|
||||
out = append(out, ms[i].toDict())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// canonicalFromRaw — canonical string of a raw JSON document.
|
||||
func canonicalFromRaw(raw []byte) (string, error) {
|
||||
var v any
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonical(normalize(v)), nil
|
||||
}
|
||||
|
||||
// canonical — sorted-key deterministic rendering of a decoded JSON value.
|
||||
func canonical(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case string:
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return canonical(f)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = canonical(normalize(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
bk, _ := json.Marshal(k)
|
||||
parts = append(parts, string(bk)+": "+canonical(normalize(t[k])))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = kv.Value
|
||||
}
|
||||
return canonical(m)
|
||||
default:
|
||||
return fmt.Sprintf("«%v»", t)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize — convert OrderedMap/map trees into plain map[string]any with
|
||||
// json.Number→float64 so canonical() sees one representation.
|
||||
func normalize(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = normalize(kv.Value)
|
||||
}
|
||||
return m
|
||||
case map[string]any:
|
||||
m := map[string]any{}
|
||||
for k, val := range t {
|
||||
m[k] = normalize(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = normalize(item)
|
||||
}
|
||||
return out
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(normalize(v))
|
||||
if err != nil {
|
||||
return "null"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,881 @@
|
||||
{
|
||||
"frozen_ts": "2026-10-11T12:00:00+00:00",
|
||||
"frozen_id_base": "20261011-120000",
|
||||
"generator": "scripts truth battery \u2014 live oracle src/rune/message_queue.py",
|
||||
"rows": [
|
||||
{
|
||||
"name": "detect:also-full-combo",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "also add logging to the parser module config.py"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 1.0,
|
||||
"signals": [
|
||||
"Pattern: \\balso\\b",
|
||||
"Pattern: \\badd\\b.*\\bto\\b",
|
||||
"File references: config.py",
|
||||
"Starts with 'also'"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:and-can-you",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "and can you fix the bug in config.py"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.8,
|
||||
"signals": [
|
||||
"Pattern: \\band can you\\b",
|
||||
"Pattern: \\bcan you fix\\b",
|
||||
"File references: config.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:plus-update-to",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "plus update the docs to reflect the change"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.6,
|
||||
"signals": [
|
||||
"Pattern: \\bplus\\b",
|
||||
"Pattern: \\bupdate\\b.*\\bto\\b"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:remember-to-only",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "remember to update the docs"
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.3,
|
||||
"signals": [
|
||||
"Pattern: \\bremember to\\b"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:plain-chat",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "hello there friend"
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.0,
|
||||
"signals": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:additionally-implement",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "additionally implement the parser to support nested blocks"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 1.0,
|
||||
"signals": [
|
||||
"Pattern: \\badditionally\\b",
|
||||
"Pattern: \\bimplement\\b.*\\bto\\b",
|
||||
"Imperative verb: add"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:make-sure",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "make sure the build passes"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.7,
|
||||
"signals": [
|
||||
"Pattern: \\bmake sure\\b",
|
||||
"Imperative verb: make"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:imperative-only-below-boundary",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "delete the old file"
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.4,
|
||||
"signals": [
|
||||
"Imperative verb: delete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:add-and-filerefs",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "add config.py and tests to the runner"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.8999999999999999,
|
||||
"signals": [
|
||||
"Pattern: \\badd\\b.*\\bto\\b",
|
||||
"Imperative verb: add",
|
||||
"File references: config.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:then-migrate",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "then migrate the store to sqlite"
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.3,
|
||||
"signals": [
|
||||
"Pattern: \\bthen\\b"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:capital-also",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "Also add tests"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.8,
|
||||
"signals": [
|
||||
"Pattern: \\balso\\b",
|
||||
"Starts with 'also'"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:fix-to-fileref",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "fix auth.py to use the new provider"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.8999999999999999,
|
||||
"signals": [
|
||||
"Pattern: \\bfix\\b.*\\bto\\b",
|
||||
"Imperative verb: fix",
|
||||
"File references: auth.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:include-fileref-at-boundary",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "include config.py"
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.5,
|
||||
"signals": [
|
||||
"Pattern: \\binclude\\b",
|
||||
"File references: config.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:empty-message",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": ""
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.0,
|
||||
"signals": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:dont-forget",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "don't forget to add auth.py to config"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.8,
|
||||
"signals": [
|
||||
"Pattern: \\bdon\\'t forget\\b",
|
||||
"Pattern: \\badd\\b.*\\bto\\b",
|
||||
"File references: auth.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:can-you-create",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "can you also create a test for auth.py"
|
||||
},
|
||||
"want": {
|
||||
"detected": true,
|
||||
"confidence": 0.8,
|
||||
"signals": [
|
||||
"Pattern: \\balso\\b",
|
||||
"Pattern: \\bcan you also\\b",
|
||||
"File references: auth.py"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detect:build-to-pattern",
|
||||
"op": "detect",
|
||||
"setup": {
|
||||
"message": "we should build the parser to handle comments"
|
||||
},
|
||||
"want": {
|
||||
"detected": false,
|
||||
"confidence": 0.3,
|
||||
"signals": [
|
||||
"Pattern: \\bbuild\\b.*\\bto\\b"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "add:detected",
|
||||
"op": "add",
|
||||
"setup": {
|
||||
"message": "also fix config.py to log properly",
|
||||
"channel": "chat",
|
||||
"task_id": null
|
||||
},
|
||||
"want": {
|
||||
"id": "20261011-120000-000001",
|
||||
"message": "also fix config.py to log properly",
|
||||
"timestamp": "2026-10-11T12:00:00.000002+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": null,
|
||||
"confidence": 1.0,
|
||||
"signals": [
|
||||
"Pattern: \\balso\\b",
|
||||
"Pattern: \\bfix\\b.*\\bto\\b",
|
||||
"File references: config.py",
|
||||
"Starts with 'also'"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "add:with-task-id",
|
||||
"op": "add",
|
||||
"setup": {
|
||||
"message": "update docs to include the new flag",
|
||||
"channel": "chat",
|
||||
"task_id": "T42"
|
||||
},
|
||||
"want": {
|
||||
"id": "20261011-120000-000001",
|
||||
"message": "update docs to include the new flag",
|
||||
"timestamp": "2026-10-11T12:00:00.000002+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": "T42",
|
||||
"confidence": 1.0,
|
||||
"signals": [
|
||||
"Pattern: \\binclude\\b",
|
||||
"Pattern: \\bupdate\\b.*\\bto\\b",
|
||||
"Imperative verb: update"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "add:undetected",
|
||||
"op": "add",
|
||||
"setup": {
|
||||
"message": "just a note for later",
|
||||
"channel": "chat",
|
||||
"task_id": null
|
||||
},
|
||||
"want": {
|
||||
"id": "20261011-120000-000001",
|
||||
"message": "just a note for later",
|
||||
"timestamp": "2026-10-11T12:00:00.000002+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": null,
|
||||
"confidence": 0.0,
|
||||
"signals": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pending:mixed",
|
||||
"op": "pending",
|
||||
"setup": {
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": [
|
||||
{
|
||||
"id": "m1",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m2",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": "T1",
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pending:missing-file",
|
||||
"op": "pending",
|
||||
"setup": {},
|
||||
"want": []
|
||||
},
|
||||
{
|
||||
"name": "pending:missing-store",
|
||||
"op": "pending",
|
||||
"setup": {},
|
||||
"want": []
|
||||
},
|
||||
{
|
||||
"name": "approve:hit",
|
||||
"op": "approve",
|
||||
"setup": {
|
||||
"msg_id": "m1",
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"ok": true,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "approve:miss",
|
||||
"op": "approve",
|
||||
"setup": {
|
||||
"msg_id": "nope",
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"ok": false,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reject:hit",
|
||||
"op": "reject",
|
||||
"setup": {
|
||||
"msg_id": "m2",
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"ok": true,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reject:miss",
|
||||
"op": "reject",
|
||||
"setup": {
|
||||
"msg_id": "nope",
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"ok": false,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "approve_all:mixed",
|
||||
"op": "approve_all",
|
||||
"setup": {
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"count": 2,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "approve_all:empty",
|
||||
"op": "approve_all",
|
||||
"setup": {
|
||||
"file": "[]"
|
||||
},
|
||||
"want": {
|
||||
"count": 0,
|
||||
"file": "[]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "clear:nonempty",
|
||||
"op": "clear",
|
||||
"setup": {
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"file": "[]",
|
||||
"status": {
|
||||
"total": 0,
|
||||
"pending": 0,
|
||||
"approved": 0,
|
||||
"executed": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_approved:mixed",
|
||||
"op": "get_approved",
|
||||
"setup": {
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": [
|
||||
{
|
||||
"id": "m3",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "approved",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mark_executed:hit",
|
||||
"op": "mark_executed",
|
||||
"setup": {
|
||||
"msg_id": "m3",
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"ok": true,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mark_executed:miss",
|
||||
"op": "mark_executed",
|
||||
"setup": {
|
||||
"msg_id": "nope",
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"ok": false,
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status:mixed",
|
||||
"op": "status",
|
||||
"setup": {
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"total": 5,
|
||||
"pending": 2,
|
||||
"approved": 1,
|
||||
"executed": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "load:missing-file",
|
||||
"op": "load",
|
||||
"setup": {
|
||||
"missing": true
|
||||
},
|
||||
"want": []
|
||||
},
|
||||
{
|
||||
"name": "load:empty-file",
|
||||
"op": "load",
|
||||
"setup": {
|
||||
"file": "[]"
|
||||
},
|
||||
"want": []
|
||||
},
|
||||
{
|
||||
"name": "load:populated",
|
||||
"op": "load",
|
||||
"setup": {
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": [
|
||||
{
|
||||
"id": "m1",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m2",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": "T1",
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m3",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "approved",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m4",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "executed",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m5",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "rejected",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "save:empty",
|
||||
"op": "save",
|
||||
"setup": {
|
||||
"store": []
|
||||
},
|
||||
"want": "[]"
|
||||
},
|
||||
{
|
||||
"name": "save:unicode-null-emptysignals",
|
||||
"op": "save",
|
||||
"setup": {
|
||||
"store": [
|
||||
{
|
||||
"id": "u1",
|
||||
"message": "fix caf\u00e9 \u2615 support in config.py",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": null,
|
||||
"confidence": 0.7000000000000001,
|
||||
"signals": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"want": "[\n {\n \"id\": \"u1\",\n \"message\": \"fix caf\\u00e9 \\u2615 support in config.py\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.7000000000000001,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
{
|
||||
"name": "save:seed",
|
||||
"op": "save",
|
||||
"setup": {
|
||||
"store": [
|
||||
{
|
||||
"id": "m1",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m2",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "pending",
|
||||
"task_id": "T1",
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m3",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "approved",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m4",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "executed",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
},
|
||||
{
|
||||
"id": "m5",
|
||||
"message": "seed message",
|
||||
"timestamp": "2026-10-11T11:59:00.000001+00:00",
|
||||
"channel": "chat",
|
||||
"status": "rejected",
|
||||
"task_id": null,
|
||||
"confidence": 0.8,
|
||||
"signals": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"want": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
{
|
||||
"name": "cli:no-args",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": []
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Usage: message_queue.py [add|list|clear|approve|reject|status]\n",
|
||||
"exit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:add",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"add",
|
||||
"also",
|
||||
"fix",
|
||||
"config.py",
|
||||
"to",
|
||||
"log"
|
||||
]
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u2705 Added to queue: 20261011-120000-000001\n Confidence: 1.00\n Signals: Pattern: \\balso\\b, Pattern: \\bfix\\b.*\\bto\\b, File references: config.py, Starts with 'also'\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:add-no-message",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"add"
|
||||
]
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Usage: message_queue.py add <message>\n",
|
||||
"exit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:list-empty",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"list"
|
||||
]
|
||||
},
|
||||
"want": {
|
||||
"stdout": "No pending messages.\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:list-some",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"list"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Pending messages: 2\n m1: seed message... (confidence: 0.80)\n m2: seed message... (confidence: 0.80)\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:clear",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"clear"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u2705 Queue cleared\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:approve-all",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"approve"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u2705 Approved 2 messages\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:approve-id",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"approve",
|
||||
"m2"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u2705 Approved: m2\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:approve-miss",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"approve",
|
||||
"nope"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u274c Message not found: nope\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:reject-no-id",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"reject"
|
||||
]
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Usage: message_queue.py reject <msg_id>\n",
|
||||
"exit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:reject-id",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"reject",
|
||||
"m1"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u2705 Rejected: m1\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:reject-miss",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"reject",
|
||||
"nope"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "\u274c Message not found: nope\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:status",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"status"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"m1\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m2\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": \"T1\",\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m3\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"approved\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m4\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"executed\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n },\n {\n \"id\": \"m5\",\n \"message\": \"seed message\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"rejected\",\n \"task_id\": null,\n \"confidence\": 0.8,\n \"signals\": []\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Queue status:\n Total: 5\n Pending: 2\n Approved: 1\n Executed: 1\n",
|
||||
"exit": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:unknown-cmd",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"frobnicate"
|
||||
]
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Unknown command: frobnicate\n",
|
||||
"exit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cli:list-long-truncation",
|
||||
"op": "cli",
|
||||
"setup": {
|
||||
"argv": [
|
||||
"list"
|
||||
],
|
||||
"file": "[\n {\n \"id\": \"L1\",\n \"message\": \"this seed message is definitely longer than fifty characters in total\",\n \"timestamp\": \"2026-10-11T11:59:00.000001+00:00\",\n \"channel\": \"chat\",\n \"status\": \"pending\",\n \"task_id\": null,\n \"confidence\": 0.5,\n \"signals\": [\n \"s1\"\n ]\n }\n]"
|
||||
},
|
||||
"want": {
|
||||
"stdout": "Pending messages: 1\n L1: this seed message is definitely longer than fifty ... (confidence: 0.50)\n",
|
||||
"exit": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate plus per-function batteries replaying the
|
||||
// frozen oracle truth (truth.json, generated against the LIVE Python
|
||||
// oracle with the clock frozen and RUNE_MESSAGE_QUEUE_CORE=python pinned)
|
||||
// VERBATIM. Any divergence fails; the truth file rides the repo so the
|
||||
// gate holds in CI without /tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) *TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth.json: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth.json contained no rows")
|
||||
}
|
||||
return &doc
|
||||
}
|
||||
|
||||
func findRow(t *testing.T, name string) TruthRow {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
for _, row := range doc.Rows {
|
||||
if row.Name == name {
|
||||
return row
|
||||
}
|
||||
}
|
||||
t.Fatalf("frozen row %q missing from truth.json — battery/truth drift", name)
|
||||
return TruthRow{}
|
||||
}
|
||||
|
||||
func mustReplay(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func replayOp(t *testing.T, op string) int {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
n := 0
|
||||
for _, row := range doc.Rows {
|
||||
if row.Op != op {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("no %s rows in truth.json", op)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// rowWant decodes a row's want JSON into a generic map.
|
||||
func rowWant(t *testing.T, name string) map[string]any {
|
||||
t.Helper()
|
||||
row := findRow(t, name)
|
||||
var want map[string]any
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
t.Fatalf("decode want of %s: %v", name, err)
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// TestTruthParity — the whole frozen battery, one shot.
|
||||
func TestTruthParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
t.Fatalf("parity replay diverged after %d clean rows:\n%s",
|
||||
len(doc.Rows)-len(fails), strings.Join(fails, "\n"))
|
||||
}
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", len(doc.Rows))
|
||||
}
|
||||
|
||||
// TestBatteryDetectTaskAddition — every signal family, the >0.5 boundary,
|
||||
// the 1.0 cap, and the raw \bdon\'t forget\b pattern artifact.
|
||||
func TestBatteryDetectTaskAddition(t *testing.T) {
|
||||
n := replayOp(t, "detect")
|
||||
if n != 17 {
|
||||
t.Fatalf("expected 18 frozen detect rows, got %d — battery/truth drift", n)
|
||||
}
|
||||
// boundary: exactly 0.5 is NOT a task addition (strict > 0.5)
|
||||
want := rowWant(t, "detect:include-fileref-at-boundary")
|
||||
if want["detected"] != false || want["confidence"] != 0.5 {
|
||||
t.Fatalf("frozen boundary row changed: %v", want)
|
||||
}
|
||||
// cap: 1.0 ceiling
|
||||
want = rowWant(t, "detect:also-full-combo")
|
||||
if want["detected"] != true || want["confidence"] != 1.0 {
|
||||
t.Fatalf("frozen cap row changed: %v", want)
|
||||
}
|
||||
// sub-boundary: imperative verb alone (0.4)
|
||||
want = rowWant(t, "detect:imperative-only-below-boundary")
|
||||
if want["detected"] != false || want["confidence"] != 0.4 {
|
||||
t.Fatalf("frozen sub-boundary row changed: %v", want)
|
||||
}
|
||||
mustReplay(t, "detect:dont-forget")
|
||||
}
|
||||
|
||||
// TestBatteryDetectDontForgetArtifact — the oracle stores the pattern as
|
||||
// the RAW string \bdon\'t forget\b; the backslash must survive into the
|
||||
// signal text AND keep matching plain apostrophes.
|
||||
func TestBatteryDetectDontForgetArtifact(t *testing.T) {
|
||||
q := NewMessageQueue(t.TempDir())
|
||||
_, _, signals := q.detectTaskAddition("don't forget to log out")
|
||||
found := false
|
||||
for _, s := range signals {
|
||||
if s == `Pattern: \bdon\'t forget\b` {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("raw don\\'t pattern artifact lost; signals: %v", signals)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryAddMessage — msg_id from the FIRST now() call, timestamp from
|
||||
// the SECOND, task_id null passthrough, undetected adds still queue.
|
||||
func TestBatteryAddMessage(t *testing.T) {
|
||||
if n := replayOp(t, "add"); n != 3 {
|
||||
t.Fatalf("expected 3 frozen add rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "add:with-task-id")
|
||||
if want["id"] != "20261011-120000-000001" || want["timestamp"] != "2026-10-11T12:00:00.000002+00:00" {
|
||||
t.Fatalf("frozen clock pairing changed: id=%v ts=%v", want["id"], want["timestamp"])
|
||||
}
|
||||
if want["task_id"] != "T42" || want["status"] != "pending" {
|
||||
t.Fatalf("frozen add row changed: %v", want)
|
||||
}
|
||||
want = rowWant(t, "add:undetected")
|
||||
if want["confidence"] != 0.0 {
|
||||
t.Fatalf("undetected add should carry 0.0 confidence: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryQueueManagement — pending/approve/reject/approve_all/clear/
|
||||
// get_approved/mark_executed/status incl. hit+miss and file-bytes pins.
|
||||
func TestBatteryQueueManagement(t *testing.T) {
|
||||
for _, op := range []string{"pending", "approve", "reject", "approve_all",
|
||||
"clear", "get_approved", "mark_executed", "status"} {
|
||||
replayOp(t, op)
|
||||
}
|
||||
// miss rows must NOT rewrite the store (file bytes unchanged)
|
||||
want := rowWant(t, "approve:miss")
|
||||
if want["ok"] != false {
|
||||
t.Fatalf("approve miss should return false: %v", want)
|
||||
}
|
||||
missFile, _ := want["file"].(string)
|
||||
want = rowWant(t, "approve:hit")
|
||||
hitFile, _ := want["file"].(string)
|
||||
if missFile == hitFile {
|
||||
t.Fatal("miss row reuses hit row bytes — save-on-miss divergence")
|
||||
}
|
||||
// approve_all on an empty store saves "[]" and returns 0
|
||||
want = rowWant(t, "approve_all:empty")
|
||||
if want["count"] != float64(0) || want["file"] != "[]" {
|
||||
t.Fatalf("frozen empty approve_all changed: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryLoadSave — _load_queue branches (missing file / "[]" file /
|
||||
// populated) and _save_queue byte-parity (ensure_ascii, null task_id,
|
||||
// empty signals, indent=2 layout).
|
||||
func TestBatteryLoadSave(t *testing.T) {
|
||||
if n := replayOp(t, "load"); n != 3 {
|
||||
t.Fatalf("expected 3 frozen load rows, got %d", n)
|
||||
}
|
||||
replayOp(t, "save")
|
||||
mustReplay(t, "save:unicode-null-emptysignals")
|
||||
mustReplay(t, "save:empty")
|
||||
}
|
||||
|
||||
// TestBatteryCLI — message_queue.py main() byte-parity stdout + exit codes.
|
||||
func TestBatteryCLI(t *testing.T) {
|
||||
n := replayOp(t, "cli")
|
||||
if n != 15 {
|
||||
t.Fatalf("expected 15 frozen cli rows, got %d", n)
|
||||
}
|
||||
for name, wantExit := range map[string]float64{
|
||||
"cli:no-args": 1,
|
||||
"cli:add": 0,
|
||||
"cli:add-no-message": 1,
|
||||
"cli:list-empty": 0,
|
||||
"cli:list-some": 0,
|
||||
"cli:clear": 0,
|
||||
"cli:approve-all": 0,
|
||||
"cli:approve-id": 0,
|
||||
"cli:approve-miss": 0,
|
||||
"cli:reject-no-id": 1,
|
||||
"cli:reject-id": 0,
|
||||
"cli:reject-miss": 0,
|
||||
"cli:status": 0,
|
||||
"cli:unknown-cmd": 1,
|
||||
"cli:list-long-truncation": 0,
|
||||
} {
|
||||
want := rowWant(t, name)
|
||||
if want["exit"] != wantExit {
|
||||
t.Fatalf("%s: frozen exit code %v != %v", name, want["exit"], wantExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.206.0"
|
||||
__version__ = "0.210.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust build_basis core (dichotomy wave #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* bb_research_depth({route, mode, justification}) -> depth decision dict
|
||||
* bb_new_basis({question, depth_decision, issue_ref}) -> basis dict
|
||||
* bb_set_dispositions({basis, dispositions}) -> {"basis", "errors"}
|
||||
* bb_validate_basis({basis}) -> [errors]
|
||||
* bb_is_qualified({basis}) -> bool
|
||||
* bb_attach_evidence_ref({basis}) -> str
|
||||
* bb_summary({basis}) -> summary dict
|
||||
* bb_revise_depth({basis, new_depth, reason}) -> {"basis", "revision"}
|
||||
* bb_to_json({basis}) -> str (json.dumps indent=1 sort_keys semantics)
|
||||
* bb_from_json({text}) -> parsed value
|
||||
* bb_generate_research_plan({...}) -> plan dict
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with BB_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" / "build_basis"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("BB_RUST_LIB", _CRATE_DIR / "target" / "release" / "libbuild_basis.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"bb_research_depth", "bb_new_basis", "bb_set_dispositions",
|
||||
"bb_validate_basis", "bb_is_qualified", "bb_attach_evidence_ref",
|
||||
"bb_summary", "bb_revise_depth", "bb_to_json", "bb_from_json",
|
||||
"bb_generate_research_plan",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class BasisUnavailable(FileNotFoundError):
|
||||
"""The Rust build_basis 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 BasisUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
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"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def research_depth(route: str, mode: str = "", justification: str = "") -> dict:
|
||||
return _call("bb_research_depth", {
|
||||
"route": route, "mode": mode, "justification": justification})
|
||||
|
||||
|
||||
def new_basis(question: str, depth_decision: dict, issue_ref: str = "") -> dict:
|
||||
return _call("bb_new_basis", {
|
||||
"question": question, "depth_decision": depth_decision,
|
||||
"issue_ref": issue_ref})
|
||||
|
||||
|
||||
def set_dispositions(basis: dict, dispositions: list) -> dict:
|
||||
"""Returns {"basis": <mutated basis>, "errors": [...]}."""
|
||||
return _call("bb_set_dispositions", {
|
||||
"basis": basis, "dispositions": dispositions})
|
||||
|
||||
|
||||
def validate_basis(basis: dict) -> list:
|
||||
return _call("bb_validate_basis", {"basis": basis})
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
return _call("bb_is_qualified", {"basis": basis})
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
return _call("bb_attach_evidence_ref", {"basis": basis})
|
||||
|
||||
|
||||
def summary(basis: dict) -> dict:
|
||||
return _call("bb_summary", {"basis": basis})
|
||||
|
||||
|
||||
def revise_depth(basis: dict, new_depth: str, reason: str) -> dict:
|
||||
"""Returns {"basis": <mutated basis>, "revision": {...}}; raises
|
||||
RuntimeError carrying the exact oracle ValueError message on refusal."""
|
||||
return _call("bb_revise_depth", {
|
||||
"basis": basis, "new_depth": new_depth, "reason": reason})
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
return _call("bb_to_json", {"basis": basis})
|
||||
|
||||
|
||||
def from_json(text: str):
|
||||
return _call("bb_from_json", {"text": text})
|
||||
|
||||
|
||||
def generate_research_plan(intent: str, target: str = "",
|
||||
current_reality: str = "",
|
||||
unknowns: list | None = None,
|
||||
decisions: list | None = None,
|
||||
risks: list | None = None,
|
||||
depth: str | None = None) -> dict:
|
||||
return _call("bb_generate_research_plan", {
|
||||
"intent": intent, "target": target,
|
||||
"current_reality": current_reality,
|
||||
"unknowns": list(unknowns or []), "decisions": list(decisions or []),
|
||||
"risks": list(risks or []), "depth": depth})
|
||||
@@ -19,11 +19,34 @@ structures the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def _rust(fn: str, *args, **kwargs):
|
||||
"""Delegate to the Rust kernel (dichotomy wave #2479); returns
|
||||
_MISSING when the core is unavailable so the Python oracle body runs.
|
||||
RUNE_BB_CORE=python pins the oracle unconditionally."""
|
||||
if os.environ.get("RUNE_BB_CORE", "auto") == "python":
|
||||
return _MISSING
|
||||
try:
|
||||
from rune import bb_loader
|
||||
if not bb_loader.available():
|
||||
return _MISSING
|
||||
return getattr(bb_loader, fn)(*args, **kwargs)
|
||||
except Exception:
|
||||
return _MISSING
|
||||
|
||||
|
||||
class _Missing:
|
||||
pass
|
||||
|
||||
|
||||
_MISSING = _Missing()
|
||||
|
||||
# Research depth tiers — derived from adaptive_router ROUTES, never a
|
||||
# second router (rule C/A). Each tier records WHY it was chosen.
|
||||
DEPTH_NONE = "NONE" # tiny obvious local change; local evidence only
|
||||
@@ -48,6 +71,9 @@ def research_depth(route: str, *, mode: str = "",
|
||||
the canonical route authority (adaptive_router); `justification` is
|
||||
REQUIRED when deviating from the route default — recorded, never
|
||||
silent (rule D: record why research depth was chosen)."""
|
||||
out = _rust("research_depth", route, mode, justification)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
default = _ROUTE_DEFAULT_DEPTH.get(route, DEPTH_INWARD)
|
||||
depth = default
|
||||
why = f"route {route} default"
|
||||
@@ -89,6 +115,9 @@ def new_basis(*, question: str, depth_decision: dict,
|
||||
"""Create an empty-but-valid Build Basis. References only — full
|
||||
research dumps live in the Research Core's artifact dirs (rule 18C:
|
||||
do not store unnecessary dumps)."""
|
||||
out = _rust("new_basis", question, depth_decision, issue_ref)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
@@ -112,6 +141,11 @@ def new_basis(*, question: str, depth_decision: dict,
|
||||
def set_dispositions(basis: dict, dispositions: list[dict]) -> list[str]:
|
||||
"""Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
Returns validation errors; the basis stays unqualified until valid."""
|
||||
out = _rust("set_dispositions", basis, dispositions)
|
||||
if out is not _MISSING:
|
||||
basis.clear()
|
||||
basis.update(out["basis"])
|
||||
return out["errors"]
|
||||
basis["dispositions"] = [
|
||||
{"item": d.get("item", ""), "disposition": d.get("disposition", ""),
|
||||
"why": d.get("why", ""), "ref": d.get("ref", "")}
|
||||
@@ -124,6 +158,9 @@ def validate_basis(basis: dict) -> list[str]:
|
||||
"""Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
decision with why, at least one outward prior-art answer when depth
|
||||
is OUTWARD/FULL, and every disposition uses a legal disposition."""
|
||||
out = _rust("validate_basis", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
errors: list[str] = []
|
||||
if basis.get("schema_version") != SCHEMA_VERSION:
|
||||
errors.append(f"schema_version {basis.get('schema_version')!r}")
|
||||
@@ -152,12 +189,18 @@ def validate_basis(basis: dict) -> list[str]:
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
out = _rust("is_qualified", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return not validate_basis(basis)
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
"""The evidence_ref string a contract carries to point at this basis
|
||||
(durable pointer, not a copy — stores stay independent)."""
|
||||
out = _rust("attach_evidence_ref", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return (f"build_basis:{basis.get('issue_ref') or 'adhoc'}:"
|
||||
f"{basis.get('created_at')}")
|
||||
|
||||
@@ -165,6 +208,9 @@ def attach_evidence_ref(basis: dict) -> str:
|
||||
def summary(basis: dict) -> dict:
|
||||
"""Compact projection for prompts/previews (no private reasoning —
|
||||
structured conclusions only, rule 18G)."""
|
||||
out = _rust("summary", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return {
|
||||
"schema_version": basis.get("schema_version"),
|
||||
"question": basis.get("question"),
|
||||
@@ -193,6 +239,11 @@ def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
|
||||
explicit resolution evidence ('discovery resolved', 'prior-art
|
||||
resolved', 'unnecessary') — never a bare skip."""
|
||||
out = _rust("revise_depth", basis, new_depth=new_depth, reason=reason)
|
||||
if out is not _MISSING:
|
||||
basis.clear()
|
||||
basis.update(out["basis"])
|
||||
return out["revision"]
|
||||
if new_depth not in DEPTH_TIERS:
|
||||
raise ValueError(f"unknown depth {new_depth!r}")
|
||||
original = basis["depth"]["depth"]
|
||||
@@ -216,11 +267,17 @@ def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
out = _rust("to_json", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
import json
|
||||
return json.dumps(basis, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
def from_json(text: str) -> dict:
|
||||
out = _rust("from_json", text)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
import json
|
||||
return json.loads(text)
|
||||
|
||||
@@ -248,6 +305,10 @@ def generate_research_plan(*, intent: str, target: str = "",
|
||||
risks of THIS intent — the function has no topic vocabulary and no
|
||||
permanent category list. Depth, when not supplied, is chosen by
|
||||
consequence heuristics recorded in the plan (why is always present)."""
|
||||
out = _rust("generate_research_plan", intent, target, current_reality,
|
||||
unknowns, decisions, risks, depth)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
unknowns = [u for u in (unknowns or []) if u.strip()]
|
||||
decisions = [d for d in (decisions or []) if d.strip()]
|
||||
risks = [r for r in (risks or []) if r.strip()]
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.206.0"' in rune_version,
|
||||
'__version__ = "0.210.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -34,8 +34,32 @@ revision consumed so "which builds used the old rule" stays answerable.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from rune import structural_store as ss
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust pattern core, or None when pinned to the oracle / not built.
|
||||
RUNE_CP_CORE=python pins the oracle; the kernel is a drop-in and every
|
||||
caller falls back to the oracle path on any kernel error."""
|
||||
if os.environ.get("RUNE_CP_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cp_loader
|
||||
|
||||
return cp_loader if cp_loader.available() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _need_dict(need) -> dict:
|
||||
return {
|
||||
"objective": need.objective,
|
||||
"affected_surface": list(need.affected_surface),
|
||||
"constraints": dict(need.constraints),
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"ConstructionPattern",
|
||||
"evaluate_applicability",
|
||||
@@ -168,6 +192,12 @@ def record_pattern_proposal(
|
||||
Recorded with verification state PENDING until deterministic project
|
||||
evidence, research evidence, proven construction evidence, or an
|
||||
explicit user decision accepts it."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.propose(pattern_id, provider, model, list(evidence), confidence)
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
if not provider or not model:
|
||||
raise ValueError("pattern proposal requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
@@ -256,6 +286,13 @@ def evaluate_applicability(
|
||||
need explicitly declares the pattern non-applicable, DOES_NOT_APPLY
|
||||
with the exclusion recorded.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.evaluate(pattern.to_dict(), _need_dict(need),
|
||||
dict(surface_result), dict(authorities))
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
# §3/§18: PROJECT evidence overrides the pattern. Exclusions may be
|
||||
# declared by the project/need (pattern_exclusions constraint) or by the
|
||||
# pattern itself (non-applicability domains).
|
||||
@@ -427,6 +464,14 @@ def derive_pattern_obligations(
|
||||
verdict, reason/evidence, and provenance kind PATTERN (distinct from
|
||||
Tranche 5's STRUCTURAL derivation — §8 provenance separation).
|
||||
Project exceptions (§18) suppress with provenance retained."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.derive(pattern.to_dict(), _need_dict(need), dict(surface_result),
|
||||
dict(authorities),
|
||||
project_exceptions=list(project_exceptions or []))
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
project_exceptions = list(project_exceptions or [])
|
||||
out = {
|
||||
"pattern": pattern.pattern_id,
|
||||
@@ -474,6 +519,12 @@ def combine_pattern_obligations(derivations: list[dict]) -> dict:
|
||||
same obligation id collapse (provenance lists merge); conflicting
|
||||
obligations (same id, contradictory verdicts across patterns) surface
|
||||
as conflicts requiring evidence/user decision — never silently chosen."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.combine(list(derivations))
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
merged: dict[str, dict] = {}
|
||||
conflicts: list[dict] = []
|
||||
for d in derivations:
|
||||
|
||||
+86
-41
@@ -1,11 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_projection core (facade burn-down).
|
||||
"""ctypes loader for the Rust construction_pattern core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cp_project({contract_entity, project_id, ...}) -> projection dict
|
||||
* cp_free_string(ptr)
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cp_construct({pattern dict}) -> normalized pattern dict
|
||||
* cp_revise({pattern, reason, evidence, changes?}) -> new pattern dict
|
||||
* cp_propose({pattern_id, provider, model, evidence, confidence}) -> dict
|
||||
* cp_evaluate({pattern, need, surface_result, authorities}) -> verdict dict
|
||||
* cp_derive({pattern, need, surface_result, authorities,
|
||||
project_exceptions?}) -> derivation dict
|
||||
* cp_combine({derivations}) -> {"obligations", "conflicts"}
|
||||
|
||||
The `need` is a plain dict {objective, affected_surface, constraints} —
|
||||
the kernel never sees the live Need object.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with CP_RUST_LIB (a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
@@ -14,64 +25,98 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "construction_projection"
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "construction_pattern"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"CP_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_projection.so"
|
||||
)
|
||||
os.environ.get("CP_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_pattern.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ConstructionPatternCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust pattern core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
_SYMS = ("cp_construct", "cp_revise", "cp_propose", "cp_evaluate", "cp_derive", "cp_combine")
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise FileNotFoundError(f"construction_projection not built: {_LIB_PATH}")
|
||||
if not _LIB_PATH.is_file():
|
||||
raise ConstructionPatternCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cp_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.cp_free_string.restype = None
|
||||
fn = lib.cp_project
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
for sym in _SYMS:
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cp_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()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.cp_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
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"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def project(contract_entity: dict, contract_id: str = "", project_id: str = "", originating_goal_id: str = "",
|
||||
operation_id: str = "", waiting_state: str = "", live_correlation: str = "",
|
||||
stage: str = "", builder_state: str = "", verification_state: str = "",
|
||||
checkpoint: str = "", failure_class: str = "", completion_state: str = "") -> dict:
|
||||
return _call("cp_project", {"contract_entity": contract_entity,
|
||||
"contract_id": contract_id,
|
||||
"project_id": project_id,
|
||||
"originating_goal_id": originating_goal_id,
|
||||
"operation_id": operation_id,
|
||||
"waiting_state": waiting_state,
|
||||
"live_correlation": live_correlation,
|
||||
"stage": stage, "builder_state": builder_state,
|
||||
"verification_state": verification_state,
|
||||
"checkpoint": checkpoint,
|
||||
"failure_class": failure_class,
|
||||
"completion_state": completion_state})
|
||||
def construct(pattern: dict) -> dict:
|
||||
return _call("cp_construct", dict(pattern))
|
||||
|
||||
|
||||
def revise(pattern: dict, reason: str, evidence: list, changes: dict | None = None) -> dict:
|
||||
payload = {"pattern": pattern, "reason": reason, "evidence": list(evidence)}
|
||||
if changes is not None:
|
||||
payload["changes"] = dict(changes)
|
||||
return _call("cp_revise", payload)
|
||||
|
||||
|
||||
def propose(pattern_id: str, provider: str, model: str, evidence: list, confidence: float) -> dict:
|
||||
return _call("cp_propose", {
|
||||
"pattern_id": pattern_id, "provider": provider, "model": model,
|
||||
"evidence": list(evidence), "confidence": confidence,
|
||||
})
|
||||
|
||||
|
||||
def evaluate(pattern: dict, need: dict, surface_result: dict, authorities: dict) -> dict:
|
||||
return _call("cp_evaluate", {
|
||||
"pattern": pattern, "need": need,
|
||||
"surface_result": surface_result, "authorities": authorities,
|
||||
})
|
||||
|
||||
|
||||
def derive(pattern: dict, need: dict, surface_result: dict, authorities: dict,
|
||||
project_exceptions: list | None = None) -> dict:
|
||||
payload = {"pattern": pattern, "need": need,
|
||||
"surface_result": surface_result, "authorities": authorities}
|
||||
if project_exceptions is not None:
|
||||
payload["project_exceptions"] = list(project_exceptions)
|
||||
return _call("cp_derive", payload)
|
||||
|
||||
|
||||
def combine(derivations: list) -> dict:
|
||||
return _call("cp_combine", {"derivations": list(derivations)})
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust drift_observability core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* dob_add_alert({alerts, alert_type, message, severity, now}) -> {alerts}
|
||||
* dob_check_alerts({alerts, velocity, direction, last_washed, now})
|
||||
-> {new_alerts, alerts}
|
||||
* dob_run_retrospective({new_entries, washed_entries, promoted_entries,
|
||||
decayed_entries, velocity, direction, days, now, now_local})
|
||||
-> {report, filename}
|
||||
* dob_get_unacknowledged_alerts({alerts}) -> {unacked}
|
||||
* dob_acknowledge_alert({alerts, index}) -> {alerts, changed}
|
||||
|
||||
No-clock law: `now` (aware UTC ISO-8601) and `now_local` (naive local
|
||||
ISO-8601) are always arguments — the kernel never reads the wall clock.
|
||||
|
||||
Boundary: the sqlite reads (drift_core's compute_drift_velocity /
|
||||
compute_drift_direction, the MAX(last_washed) probe, the four
|
||||
retrospective counts) and the alerts / retrospectives FILE IO stay in the
|
||||
Python facade (drift_observability.py); the kernel is a pure function of
|
||||
their results.
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char* — the default c_int truncates 64-bit pointers).
|
||||
Strings are freed via capability_scoring's cs_free_string — the kernel
|
||||
exports NO free symbol of its own (duplicate exports break the cdylib
|
||||
link when sibling crates share one process).
|
||||
|
||||
Override the .so with DO_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" / "drift_observability"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"DO_RUST_LIB", _CRATE_DIR / "target" / "release" / "libdrift_observability.so"
|
||||
)
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"dob_add_alert",
|
||||
"dob_check_alerts",
|
||||
"dob_run_retrospective",
|
||||
"dob_get_unacknowledged_alerts",
|
||||
"dob_acknowledge_alert",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class DriftObservabilityCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust drift 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 DriftObservabilityCoreUnavailable(
|
||||
f"Rust drift_observability core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_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 via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def add_alert(alerts, alert_type, message, severity="info", now="") -> list:
|
||||
return _call("dob_add_alert", {
|
||||
"alerts": alerts, "alert_type": alert_type, "message": message,
|
||||
"severity": severity, "now": now,
|
||||
})["alerts"]
|
||||
|
||||
|
||||
def check_alerts(alerts, velocity, direction, last_washed, now) -> dict:
|
||||
return _call("dob_check_alerts", {
|
||||
"alerts": alerts, "velocity": velocity, "direction": direction,
|
||||
"last_washed": last_washed, "now": now,
|
||||
})
|
||||
|
||||
|
||||
def run_retrospective(new_entries, washed_entries, promoted_entries,
|
||||
decayed_entries, velocity, direction, days,
|
||||
now, now_local) -> dict:
|
||||
return _call("dob_run_retrospective", {
|
||||
"new_entries": new_entries, "washed_entries": washed_entries,
|
||||
"promoted_entries": promoted_entries, "decayed_entries": decayed_entries,
|
||||
"velocity": velocity, "direction": direction, "days": days,
|
||||
"now": now, "now_local": now_local,
|
||||
})
|
||||
|
||||
|
||||
def get_unacknowledged_alerts(alerts) -> list:
|
||||
return _call("dob_get_unacknowledged_alerts", {"alerts": alerts})["unacked"]
|
||||
|
||||
|
||||
def acknowledge_alert(alerts, index) -> dict:
|
||||
return _call("dob_acknowledge_alert", {"alerts": alerts, "index": index})
|
||||
@@ -35,6 +35,23 @@ NEGATIVE_DRIFT_THRESHOLD = 0.1 # stability ratio below this = alert
|
||||
PROMOTION_STALL_DAYS = 14 # no promotions in this many days = alert
|
||||
|
||||
|
||||
def _do_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_DO_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the alert state
|
||||
machine (stamp/append, threshold checks, assessment lattice,
|
||||
acknowledge bounds); the sqlite reads and FILE IO stay here."""
|
||||
if os.environ.get("RUNE_DO_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import do_loader
|
||||
|
||||
if do_loader.available():
|
||||
return do_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class DriftObservability:
|
||||
"""Drift monitoring and alerting system."""
|
||||
|
||||
@@ -60,6 +77,14 @@ class DriftObservability:
|
||||
|
||||
def add_alert(self, alert_type: str, message: str, severity: str = "info"):
|
||||
"""Add a drift alert."""
|
||||
core = _do_core()
|
||||
if core is not None:
|
||||
self.alerts = core.add_alert(
|
||||
self.alerts, alert_type, message, severity,
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
)
|
||||
self._save_alerts()
|
||||
return
|
||||
alert = {
|
||||
"type": alert_type,
|
||||
"message": message,
|
||||
@@ -74,6 +99,23 @@ class DriftObservability:
|
||||
"""Check for conditions that warrant alerts."""
|
||||
import drift_core
|
||||
|
||||
core = _do_core()
|
||||
if core is not None:
|
||||
velocity = drift_core.compute_drift_velocity(db)
|
||||
direction = drift_core.compute_drift_direction(db)
|
||||
last_promo = db.execute(
|
||||
"SELECT MAX(last_washed) FROM entries WHERE rock_status != 'washable' AND status = 'active'"
|
||||
).fetchone()
|
||||
out = core.check_alerts(
|
||||
self.alerts, velocity, direction,
|
||||
last_promo[0] if last_promo else None,
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
)
|
||||
self.alerts = out["alerts"]
|
||||
if out["new_alerts"]:
|
||||
self._save_alerts()
|
||||
return out["new_alerts"]
|
||||
|
||||
new_alerts = []
|
||||
|
||||
# Check velocity
|
||||
@@ -130,6 +172,38 @@ class DriftObservability:
|
||||
"""
|
||||
import drift_core
|
||||
|
||||
core = _do_core()
|
||||
if core is not None:
|
||||
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
||||
counts = {
|
||||
"new_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE created_at > ? AND status = 'active'", (cutoff,)
|
||||
).fetchone()[0],
|
||||
"washed_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'", (cutoff,)
|
||||
).fetchone()[0],
|
||||
"promoted_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE rock_status != 'washable' AND last_washed > ? AND status = 'active'",
|
||||
(cutoff,),
|
||||
).fetchone()[0],
|
||||
"decayed_entries": db.execute(
|
||||
"SELECT COUNT(*) FROM entries WHERE status = 'deleted' AND last_accessed > ?", (cutoff,)
|
||||
).fetchone()[0],
|
||||
}
|
||||
out = core.run_retrospective(
|
||||
counts["new_entries"], counts["washed_entries"],
|
||||
counts["promoted_entries"], counts["decayed_entries"],
|
||||
drift_core.compute_drift_velocity(db, days),
|
||||
drift_core.compute_drift_direction(db),
|
||||
days,
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
datetime.now().isoformat(),
|
||||
)
|
||||
filepath = os.path.join(RETROSPECTIVES_DIR, out["filename"])
|
||||
with open(filepath, "w") as f:
|
||||
json.dump(out["report"], f, indent=2)
|
||||
return out["report"]
|
||||
|
||||
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
||||
|
||||
# Count changes by type
|
||||
@@ -188,10 +262,20 @@ class DriftObservability:
|
||||
|
||||
def get_unacknowledged_alerts(self) -> List[dict]:
|
||||
"""Get alerts that haven't been acknowledged."""
|
||||
core = _do_core()
|
||||
if core is not None:
|
||||
return core.get_unacknowledged_alerts(self.alerts)
|
||||
return [a for a in self.alerts if not a.get("acknowledged")]
|
||||
|
||||
def acknowledge_alert(self, index: int):
|
||||
"""Acknowledge an alert."""
|
||||
core = _do_core()
|
||||
if core is not None:
|
||||
out = core.acknowledge_alert(self.alerts, index)
|
||||
if out["changed"]:
|
||||
self.alerts = out["alerts"]
|
||||
self._save_alerts()
|
||||
return
|
||||
if 0 <= index < len(self.alerts):
|
||||
self.alerts[index]["acknowledged"] = True
|
||||
self._save_alerts()
|
||||
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.206.0
|
||||
Version: v0.210.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -2037,7 +2037,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.206.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.210.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! build_basis branch battery — parity vs the frozen Python-oracle truth
|
||||
//! (frozen_bb_truth.json, 68 cases across every public function and
|
||||
//! branch, frozen 2026-10-11). Embedded VERBATIM via include_str!.
|
||||
//!
|
||||
//! Timestamp fields the kernel generates (decided_at, created_at,
|
||||
//! revised_at, plan_generated_at) are masked on both sides — wall clock,
|
||||
//! not logic. created_at values inside basis INPUTS are fixed
|
||||
//! pass-through and compare exactly.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::*;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_bb_truth.json");
|
||||
|
||||
/// Remove kernel-generated wall-clock fields wherever they appear.
|
||||
fn mask(v: Value) -> Value {
|
||||
match v {
|
||||
Value::Object(map) => Value::Object(
|
||||
map.into_iter()
|
||||
.filter(|(k, _)| {
|
||||
!matches!(
|
||||
k.as_str(),
|
||||
"decided_at" | "created_at" | "revised_at" | "plan_generated_at"
|
||||
)
|
||||
})
|
||||
.map(|(k, v)| (k, mask(v)))
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(items) => Value::Array(items.into_iter().map(mask).collect()),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn cases(fn_name: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][fn_name]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn input(case: &Value) -> Value {
|
||||
case["input"].clone()
|
||||
}
|
||||
|
||||
fn expected(case: &Value) -> &Value {
|
||||
case.get("expected").expect("case must carry expected")
|
||||
}
|
||||
|
||||
fn check(name: &str, actual: Value, case: &Value) {
|
||||
let masked = mask(actual);
|
||||
assert!(
|
||||
&masked == expected(case),
|
||||
"case {name} diverged:\n actual: {masked}\n expected: {}",
|
||||
expected(case)
|
||||
);
|
||||
}
|
||||
|
||||
fn check_err(name: &str, actual: Result<Value, String>, case: &Value) {
|
||||
let err = case.get("error").expect("case must carry error");
|
||||
match actual {
|
||||
Ok(_) => panic!("case {name} should have raised: {err}"),
|
||||
Err(e) => assert_eq!(&e, err, "case {name} error message diverged"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn research_depth_parity_battery() {
|
||||
for case in cases("research_depth") {
|
||||
let a = input(&case);
|
||||
let out = research_depth(
|
||||
a["route"].as_str().unwrap(),
|
||||
a.get("mode").and_then(Value::as_str).unwrap_or(""),
|
||||
a.get("justification").and_then(Value::as_str).unwrap_or(""),
|
||||
);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_basis_parity_battery() {
|
||||
for case in cases("new_basis") {
|
||||
let a = input(&case);
|
||||
let out = new_basis(
|
||||
a["question"].as_str().unwrap(),
|
||||
&a["depth_decision"],
|
||||
a.get("issue_ref").and_then(Value::as_str).unwrap_or(""),
|
||||
);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_dispositions_parity_battery() {
|
||||
for case in cases("set_dispositions") {
|
||||
let a = input(&case);
|
||||
let mut basis = a["basis"].clone();
|
||||
let errors = set_dispositions(&mut basis, &a["dispositions"]);
|
||||
check(
|
||||
case["name"].as_str().unwrap(),
|
||||
serde_json::json!({"basis": basis, "errors": errors}),
|
||||
&case,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_basis_parity_battery() {
|
||||
for case in cases("validate_basis") {
|
||||
let a = input(&case);
|
||||
let errors = validate_basis(&a["basis"]);
|
||||
check(
|
||||
case["name"].as_str().unwrap(),
|
||||
serde_json::to_value(errors).unwrap(),
|
||||
&case,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_qualified_parity_battery() {
|
||||
for case in cases("is_qualified") {
|
||||
let a = input(&case);
|
||||
let out = is_qualified(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), Value::Bool(out), &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_evidence_ref_parity_battery() {
|
||||
for case in cases("attach_evidence_ref") {
|
||||
let a = input(&case);
|
||||
let out = attach_evidence_ref(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), Value::String(out), &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_parity_battery() {
|
||||
for case in cases("summary") {
|
||||
let a = input(&case);
|
||||
let out = summary(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revise_depth_parity_battery() {
|
||||
for case in cases("revise_depth") {
|
||||
let a = input(&case);
|
||||
let mut basis = a["basis"].clone();
|
||||
let r = revise_depth(&mut basis, a["new_depth"].as_str().unwrap(), a["reason"].as_str().unwrap());
|
||||
match r {
|
||||
Ok(revision) => check(
|
||||
case["name"].as_str().unwrap(),
|
||||
serde_json::json!({"basis": basis, "revision": revision}),
|
||||
&case,
|
||||
),
|
||||
Err(e) => check_err(case["name"].as_str().unwrap(), Err(e), &case),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_json_parity_battery() {
|
||||
for case in cases("to_json") {
|
||||
let a = input(&case);
|
||||
let out = to_json(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), Value::String(out), &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_json_parity_battery() {
|
||||
for case in cases("from_json") {
|
||||
let a = input(&case);
|
||||
match from_json(a["text"].as_str().unwrap()) {
|
||||
Ok(v) => check(case["name"].as_str().unwrap(), v, &case),
|
||||
Err(_) => assert!(
|
||||
case.get("error_kind").is_some(),
|
||||
"case {} unexpected parse failure",
|
||||
case["name"].as_str().unwrap()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_research_plan_parity_battery() {
|
||||
for case in cases("generate_research_plan") {
|
||||
let a = input(&case);
|
||||
let strs = |k: &str| -> Vec<String> {
|
||||
a.get(k)
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let out = generate_research_plan(
|
||||
a["intent"].as_str().unwrap(),
|
||||
a.get("target").and_then(Value::as_str).unwrap_or(""),
|
||||
a.get("current_reality").and_then(Value::as_str).unwrap_or(""),
|
||||
&strs("unknowns"),
|
||||
&strs("decisions"),
|
||||
&strs("risks"),
|
||||
a.get("depth").and_then(Value::as_str),
|
||||
);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+876
-38
@@ -1,50 +1,888 @@
|
||||
//! build_basis — build basis summary and evidence (W6).
|
||||
//! PARITY port of src/rune/build_basis.py.
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! build_basis — T18 §18C: durable versioned RESEARCH BASIS for
|
||||
//! construction (V1 rule F: RESEARCH INFORMS STRUCTURE).
|
||||
//!
|
||||
//! FULL parity port of src/rune/build_basis.py (dichotomy wave, #2479
|
||||
//! tranche, RUST side verdict). A Build Basis is a durable, schema-
|
||||
//! versioned document capturing WHAT research decided and WHY, with
|
||||
//! references — never research dumps. This module NEVER researches
|
||||
//! (rule C): it records the depth decision derived from the canonical
|
||||
//! route authority and structures adopt/adapt/combine/reject/innovate
|
||||
//! decisions (rule E).
|
||||
//!
|
||||
//! FFI (doc-envelope law): every `bb_*` symbol takes one JSON arg and
|
||||
//! returns {"ok": true, "value": …} or {"ok": false, "err": msg}.
|
||||
//! NOTE: no bb_free_string export — sibling crates sharing the Python
|
||||
//! process already export identical free symbols; a duplicate breaks the
|
||||
//! cdylib link. Callers free through capability_scoring's cs_free_string.
|
||||
//!
|
||||
//! to_json reproduces the oracle byte-for-byte:
|
||||
//! json.dumps(basis, indent=1, sort_keys=True) — including ensure_ascii
|
||||
//! \uXXXX escaping (lowercase hex, surrogate pairs for astral chars),
|
||||
//! code-point key sorting, and Python repr() strings inside error
|
||||
//! messages.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BuildBasis {
|
||||
#[serde(default)]
|
||||
pub depth: String,
|
||||
#[serde(default)]
|
||||
pub implications: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub risks: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub evidence: Vec<String>,
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub const SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
pub const OUTWARD_PRIOR_ART_QUESTIONS: &[&str] = &[
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required",
|
||||
];
|
||||
|
||||
// ── decision lattices (enums with exhaustive matches at the JSON
|
||||
// boundary — the wire always carries the oracle's exact strings) ─────
|
||||
|
||||
/// Research-depth lattice (rule C: derived from adaptive_router ROUTES,
|
||||
/// never a second router).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum DepthTier {
|
||||
None,
|
||||
Inward,
|
||||
Outward,
|
||||
Full,
|
||||
}
|
||||
|
||||
pub fn summarize(depth: &str, risks: Vec<String>, implications: serde_json::Value) -> BuildBasis {
|
||||
BuildBasis {
|
||||
depth: depth.to_string(),
|
||||
implications,
|
||||
risks,
|
||||
evidence: vec![],
|
||||
impl DepthTier {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
DepthTier::None => "NONE",
|
||||
DepthTier::Inward => "INWARD",
|
||||
DepthTier::Outward => "OUTWARD",
|
||||
DepthTier::Full => "FULL",
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's DEPTH_TIERS tuple, rendered as Python repr.
|
||||
pub const PY_TIERS_REPR: &'static str =
|
||||
"('NONE', 'INWARD', 'OUTWARD', 'FULL')";
|
||||
|
||||
pub fn parse(s: &str) -> Option<DepthTier> {
|
||||
match s {
|
||||
"NONE" => Some(DepthTier::None),
|
||||
"INWARD" => Some(DepthTier::Inward),
|
||||
"OUTWARD" => Some(DepthTier::Outward),
|
||||
"FULL" => Some(DepthTier::Full),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// Disposition lattice (rule E): adopt/adapt/combine/reject/innovate.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Disposition {
|
||||
Adopt,
|
||||
Adapt,
|
||||
Combine,
|
||||
Reject,
|
||||
Innovate,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summarize() {
|
||||
let b = summarize(
|
||||
"STANDARD",
|
||||
vec!["risk1".into()],
|
||||
serde_json::json!({"migration": []}),
|
||||
impl Disposition {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Disposition::Adopt => "ADOPT",
|
||||
Disposition::Adapt => "ADAPT",
|
||||
Disposition::Combine => "COMBINE",
|
||||
Disposition::Reject => "REJECT",
|
||||
Disposition::Innovate => "INNOVATE",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Disposition> {
|
||||
match s {
|
||||
"ADOPT" => Some(Disposition::Adopt),
|
||||
"ADAPT" => Some(Disposition::Adapt),
|
||||
"COMBINE" => Some(Disposition::Combine),
|
||||
"REJECT" => Some(Disposition::Reject),
|
||||
"INNOVATE" => Some(Disposition::Innovate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── time (matches time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())) ──
|
||||
|
||||
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
let z = z + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z.rem_euclid(146_097);
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
|
||||
(if m <= 2 { y + 1 } else { y }, m, d)
|
||||
}
|
||||
|
||||
pub fn now() -> String {
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
let days = secs.div_euclid(86400);
|
||||
let rem = secs.rem_euclid(86400);
|
||||
let (y, m, d) = civil_from_days(days);
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
rem / 3600,
|
||||
(rem % 3600) / 60,
|
||||
rem % 60
|
||||
)
|
||||
}
|
||||
|
||||
// ── Python semantics helpers ────────────────────────────────────────────
|
||||
|
||||
/// Python str repr: single quotes unless the string contains ' and not ".
|
||||
fn py_repr_str(s: &str) -> String {
|
||||
let quote = if s.contains('\'') && !s.contains('"') {
|
||||
'"'
|
||||
} else {
|
||||
'\''
|
||||
};
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push(quote);
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if c == quote => {
|
||||
out.push('\\');
|
||||
out.push(c);
|
||||
}
|
||||
c if (c as u32) < 0x20 || (c as u32) == 0x7f => {
|
||||
out.push_str(&format!("\\x{:02x}", c as u32));
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push(quote);
|
||||
out
|
||||
}
|
||||
|
||||
/// Python repr() of a JSON value (as f"{x!r}" renders it).
|
||||
fn py_repr(v: Option<&Value>) -> String {
|
||||
match v {
|
||||
None | Some(Value::Null) => "None".into(),
|
||||
Some(Value::Bool(b)) => if *b { "True" } else { "False" }.into(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
Some(Value::String(s)) => py_repr_str(s),
|
||||
Some(other) => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness for JSON values (0, "", [], {}, null, False are falsy).
|
||||
fn is_truthy(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
|
||||
Some(Value::String(s)) => !s.is_empty(),
|
||||
Some(Value::Array(a)) => !a.is_empty(),
|
||||
Some(Value::Object(o)) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python str[:n] — code points, not bytes.
|
||||
fn truncate_chars(s: &str, n: usize) -> String {
|
||||
s.chars().take(n).collect()
|
||||
}
|
||||
|
||||
/// schema_version equality with Python semantics: True == 1.
|
||||
fn schema_version_ok(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_i64() == Some(SCHEMA_VERSION),
|
||||
Some(Value::Bool(b)) => *b, // Python True == 1
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn route_default_depth(route: &str) -> Option<DepthTier> {
|
||||
match route {
|
||||
"LOCAL_FIX" => Some(DepthTier::None),
|
||||
"STANDARD_CHANGE" => Some(DepthTier::Inward),
|
||||
"RESEARCHED_CHANGE" => Some(DepthTier::Full),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── research_depth ──────────────────────────────────────────────────────
|
||||
|
||||
/// The research-depth DECISION for a construction need. Derived from the
|
||||
/// canonical route authority; `justification` is REQUIRED when deviating
|
||||
/// from the route default — recorded, never silent (rule D).
|
||||
pub fn research_depth(route: &str, mode: &str, justification: &str) -> Value {
|
||||
let default = route_default_depth(route);
|
||||
let mut depth = default.unwrap_or(DepthTier::Inward);
|
||||
let mut why = format!("route {route} default");
|
||||
if !justification.trim().is_empty() {
|
||||
// an explicit justification selects a tier by name if given;
|
||||
// otherwise it annotates the default (zero-copy: candidate
|
||||
// borrows the trimmed justification)
|
||||
let j = justification.trim();
|
||||
let candidate = j.split(':').next().unwrap_or("").trim();
|
||||
if let Some(tier) = DepthTier::parse(candidate) {
|
||||
depth = tier;
|
||||
why = j.to_string();
|
||||
} else {
|
||||
why = format!("route {route} default; note: {j}");
|
||||
}
|
||||
} else if default.is_none() {
|
||||
why = format!(
|
||||
"route {} unknown — INWARD floor (never silent NONE)",
|
||||
py_repr_str(route)
|
||||
);
|
||||
assert_eq!(b.depth, "STANDARD");
|
||||
assert_eq!(b.risks.len(), 1);
|
||||
depth = DepthTier::Inward;
|
||||
}
|
||||
json!({"depth": depth.as_str(), "why": why, "route": route, "mode": mode,
|
||||
"decided_at": now()})
|
||||
}
|
||||
|
||||
// ── new_basis ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Create an empty-but-valid Build Basis. References only — full research
|
||||
/// dumps live in the Research Core's artifact dirs.
|
||||
pub fn new_basis(question: &str, depth_decision: &Value, issue_ref: &str) -> Value {
|
||||
json!({
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
"created_at": now(),
|
||||
"question": question,
|
||||
"issue_ref": issue_ref,
|
||||
"depth": depth_decision,
|
||||
"inward": {"findings": [], "refs": []},
|
||||
"outward": {"prior_art_questions_answered": [],
|
||||
"findings": [], "refs": []},
|
||||
"deeper": {"used": false, "findings": [], "refs": []},
|
||||
"dispositions": [],
|
||||
"alternatives_considered": [],
|
||||
"uncertainty": [],
|
||||
"risks": [],
|
||||
"implications": {"architecture": [], "structural": [],
|
||||
"migration": [], "verification": []},
|
||||
})
|
||||
}
|
||||
|
||||
// ── set_dispositions / validate_basis / is_qualified ────────────────────
|
||||
|
||||
/// Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
/// Mutates `basis` and returns validation errors; the basis stays
|
||||
/// unqualified until valid.
|
||||
pub fn set_dispositions(basis: &mut Value, dispositions: &Value) -> Vec<String> {
|
||||
let rows: Vec<Value> = dispositions
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|d| {
|
||||
let get = |k: &str| d.get(k).cloned().unwrap_or(json!(""));
|
||||
json!({"item": get("item"), "disposition": get("disposition"),
|
||||
"why": get("why"), "ref": get("ref")})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
basis["dispositions"] = Value::Array(rows);
|
||||
validate_basis(basis)
|
||||
}
|
||||
|
||||
/// Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
/// decision with why, at least one outward prior-art answer when depth is
|
||||
/// OUTWARD/FULL, and every disposition uses a legal disposition.
|
||||
pub fn validate_basis(basis: &Value) -> Vec<String> {
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
if !schema_version_ok(basis.get("schema_version")) {
|
||||
errors.push(format!(
|
||||
"schema_version {}",
|
||||
py_repr(basis.get("schema_version"))
|
||||
));
|
||||
}
|
||||
// Python: (basis.get("depth") or {}) — falsy depth slots fall back to {}
|
||||
let depth_obj = basis
|
||||
.get("depth")
|
||||
.filter(|d| is_truthy(Some(d)))
|
||||
.and_then(|d| d.as_object());
|
||||
let empty = Map::new();
|
||||
let depth_obj = depth_obj.unwrap_or(&empty);
|
||||
let depth = depth_obj.get("depth");
|
||||
let depth_str = depth.and_then(Value::as_str);
|
||||
let depth_tier = depth_str.and_then(DepthTier::parse);
|
||||
if depth_tier.is_none() {
|
||||
errors.push(format!(
|
||||
"depth {} not in {}",
|
||||
py_repr(depth),
|
||||
DepthTier::PY_TIERS_REPR
|
||||
));
|
||||
}
|
||||
if !is_truthy(depth_obj.get("why")) {
|
||||
errors.push("depth decision lacks WHY (rule D)".into());
|
||||
}
|
||||
if matches!(depth_tier, Some(DepthTier::Outward) | Some(DepthTier::Full)) {
|
||||
let outward = basis
|
||||
.get("outward")
|
||||
.filter(|o| is_truthy(Some(o)))
|
||||
.and_then(Value::as_object);
|
||||
let has_evidence = outward
|
||||
.map(|o| {
|
||||
is_truthy(o.get("prior_art_questions_answered"))
|
||||
|| is_truthy(o.get("findings"))
|
||||
|| is_truthy(o.get("refs"))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !has_evidence {
|
||||
errors.push("OUTWARD/FULL depth requires prior-art evidence".into());
|
||||
}
|
||||
}
|
||||
if depth_tier == Some(DepthTier::Inward) {
|
||||
let inward = basis
|
||||
.get("inward")
|
||||
.filter(|o| is_truthy(Some(o)))
|
||||
.and_then(Value::as_object);
|
||||
let has_evidence = inward
|
||||
.map(|o| is_truthy(o.get("findings")) || is_truthy(o.get("refs")))
|
||||
.unwrap_or(false)
|
||||
|| is_truthy(basis.get("dispositions"));
|
||||
if !has_evidence {
|
||||
errors.push(
|
||||
"INWARD depth requires recorded evidence (findings/refs) — \
|
||||
an empty basis is not qualified"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for d in basis.get("dispositions").and_then(Value::as_array).unwrap_or(&vec![]) {
|
||||
let disp = d.get("disposition");
|
||||
if !disp
|
||||
.and_then(Value::as_str)
|
||||
.and_then(Disposition::parse)
|
||||
.is_some()
|
||||
{
|
||||
let item = d.get("item").and_then(Value::as_str).unwrap_or("");
|
||||
errors.push(format!(
|
||||
"illegal disposition {} for {}",
|
||||
py_repr(disp),
|
||||
py_repr_str(&truncate_chars(item, 40))
|
||||
));
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
pub fn is_qualified(basis: &Value) -> bool {
|
||||
validate_basis(basis).is_empty()
|
||||
}
|
||||
|
||||
// ── attach_evidence_ref / summary ───────────────────────────────────────
|
||||
|
||||
/// The evidence_ref string a contract carries to point at this basis
|
||||
/// (durable pointer, not a copy — stores stay independent).
|
||||
pub fn attach_evidence_ref(basis: &Value) -> String {
|
||||
// f-string uses str(): missing/null created_at renders as "None"
|
||||
let created = match basis.get("created_at") {
|
||||
None | Some(Value::Null) => "None".to_string(),
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(other) => other.to_string(),
|
||||
};
|
||||
let issue_ref = match basis.get("issue_ref") {
|
||||
Some(Value::String(s)) if !s.is_empty() => s.clone(),
|
||||
_ => "adhoc".to_string(),
|
||||
};
|
||||
format!("build_basis:{issue_ref}:{created}")
|
||||
}
|
||||
|
||||
fn len_of(v: Option<&Value>) -> usize {
|
||||
match v {
|
||||
Some(Value::Array(a)) => a.len(),
|
||||
Some(Value::String(s)) => s.chars().count(),
|
||||
Some(Value::Object(o)) => o.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact projection for prompts/previews (no private reasoning —
|
||||
/// structured conclusions only, rule 18G).
|
||||
pub fn summary(basis: &Value) -> Value {
|
||||
let empty_vec: Vec<Value> = Vec::new();
|
||||
json!({
|
||||
"schema_version": basis.get("schema_version").cloned().unwrap_or(Value::Null),
|
||||
"question": basis.get("question").cloned().unwrap_or(Value::Null),
|
||||
"issue_ref": basis.get("issue_ref").cloned().unwrap_or(Value::Null),
|
||||
"depth": basis.get("depth").cloned().unwrap_or(Value::Null),
|
||||
"qualified": is_qualified(basis),
|
||||
"validation": validate_basis(basis),
|
||||
"dispositions": basis
|
||||
.get("dispositions")
|
||||
.and_then(Value::as_array)
|
||||
.unwrap_or(&empty_vec)
|
||||
.iter()
|
||||
.map(|d| {
|
||||
json!({
|
||||
"item": truncate_chars(d.get("item").and_then(Value::as_str).unwrap_or(""), 80),
|
||||
"disposition": d.get("disposition").cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<Value>>(),
|
||||
"uncertainty_count": len_of(basis.get("uncertainty")),
|
||||
"risks_count": len_of(basis.get("risks")),
|
||||
"implications": match basis.get("implications").and_then(Value::as_object) {
|
||||
Some(o) => o
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), json!(len_of(Some(v)))))
|
||||
.collect::<Map<String, Value>>(),
|
||||
None => Map::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── revise_depth ────────────────────────────────────────────────────────
|
||||
|
||||
/// §21.1G: record a justified depth revision. Original + revised depths
|
||||
/// both persist. An OUTWARD→INWARD/NONE downgrade requires explicit
|
||||
/// resolution evidence — never a bare skip.
|
||||
pub fn revise_depth(basis: &mut Value, new_depth: &str, reason: &str) -> Result<Value, String> {
|
||||
let tier = DepthTier::parse(new_depth)
|
||||
.ok_or_else(|| format!("unknown depth {}", py_repr_str(new_depth)))?;
|
||||
let original = basis
|
||||
.get("depth")
|
||||
.and_then(|d| d.get("depth"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
if original == new_depth {
|
||||
return Err("no-op revision: depth unchanged".into());
|
||||
}
|
||||
// §21.1G: consequential origins mix route names and tiers — the
|
||||
// oracle compares the raw stored string, so parity keeps that.
|
||||
let consequential = matches!(original.as_str(), "RESEARCHED_CHANGE" | "FULL" | "OUTWARD");
|
||||
if consequential && matches!(tier, DepthTier::None | DepthTier::Inward) {
|
||||
let low = reason.to_lowercase();
|
||||
let justified = [
|
||||
"resolved",
|
||||
"non-material",
|
||||
"discovery",
|
||||
"unnecessary",
|
||||
"prior-art question",
|
||||
]
|
||||
.iter()
|
||||
.any(|k| low.contains(k));
|
||||
if !justified {
|
||||
return Err(
|
||||
"OUTWARD downgrade requires explicit evidence: the prior-art \
|
||||
question must be resolved as non-material (not merely skipped)"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let revision = json!({
|
||||
"original_depth": original,
|
||||
"revised_depth": new_depth,
|
||||
"reason": truncate_chars(reason, 200),
|
||||
"revised_at": now(),
|
||||
});
|
||||
basis["depth_revision"] = revision.clone();
|
||||
basis["depth"]["depth"] = json!(new_depth);
|
||||
basis["depth"]["why"] = json!(format!("revised from {original}: {reason}"));
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
// ── to_json / from_json (Python json.dumps/loads semantics) ─────────────
|
||||
|
||||
fn escape_py_json_string(s: &str, out: &mut String) {
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{08}' => out.push_str("\\b"),
|
||||
'\u{0c}' => out.push_str("\\f"),
|
||||
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c if (c as u32) > 0x7e => {
|
||||
let cp = c as u32;
|
||||
if cp <= 0xFFFF {
|
||||
out.push_str(&format!("\\u{:04x}", cp));
|
||||
} else {
|
||||
let v = cp - 0x10000;
|
||||
out.push_str(&format!(
|
||||
"\\u{:04x}\\u{:04x}",
|
||||
0xD800 + (v >> 10),
|
||||
0xDC00 + (v & 0x3FF)
|
||||
));
|
||||
}
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
/// json.dumps(value, indent=1, sort_keys=True) — byte-for-byte with the
|
||||
/// oracle (ensure_ascii, code-point key order, 1-space indent steps).
|
||||
pub fn to_json(basis: &Value) -> String {
|
||||
let mut out = String::new();
|
||||
write_py_json(basis, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_py_json(v: &Value, indent: usize, out: &mut String) {
|
||||
match v {
|
||||
Value::Object(map) => {
|
||||
if map.is_empty() {
|
||||
out.push_str("{}");
|
||||
return;
|
||||
}
|
||||
let mut keys: Vec<&String> = map.keys().collect();
|
||||
keys.sort(); // UTF-8 byte order == code-point order
|
||||
out.push_str("{\n");
|
||||
for (i, k) in keys.iter().enumerate() {
|
||||
for _ in 0..indent + 1 {
|
||||
out.push(' ');
|
||||
}
|
||||
escape_py_json_string(k, out);
|
||||
out.push_str(": ");
|
||||
write_py_json(&map[k.as_str()], indent + 1, out);
|
||||
if i + 1 < keys.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
for _ in 0..indent {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
Value::Array(items) => {
|
||||
if items.is_empty() {
|
||||
out.push_str("[]");
|
||||
return;
|
||||
}
|
||||
out.push_str("[\n");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
for _ in 0..indent + 1 {
|
||||
out.push(' ');
|
||||
}
|
||||
write_py_json(item, indent + 1, out);
|
||||
if i + 1 < items.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
for _ in 0..indent {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Value::String(s) => escape_py_json_string(s, out),
|
||||
Value::Null => out.push_str("null"),
|
||||
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
||||
Value::Number(n) => out.push_str(&n.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_json(text: &str) -> Result<Value, String> {
|
||||
serde_json::from_str(text).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ── generate_research_plan ──────────────────────────────────────────────
|
||||
|
||||
const OUTWARD_KEYWORDS: &[&str] = &[
|
||||
"exist",
|
||||
"existing",
|
||||
"prior art",
|
||||
"precedent",
|
||||
"standard",
|
||||
"convention",
|
||||
"library",
|
||||
"tool",
|
||||
"framework",
|
||||
"approach",
|
||||
];
|
||||
|
||||
const CONSEQUENTIAL_KEYWORDS: &[&str] = &[
|
||||
"architecture",
|
||||
"migration",
|
||||
"replace",
|
||||
"rewrite",
|
||||
"security",
|
||||
"schema",
|
||||
"delete",
|
||||
"deploy",
|
||||
"breaking",
|
||||
];
|
||||
|
||||
/// Generate a CONTEXT-SPECIFIC research plan for one construction intent.
|
||||
/// Topics are synthesized from the material unknowns/decisions/risks of
|
||||
/// THIS intent — no topic vocabulary, no permanent category list.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn generate_research_plan(
|
||||
intent: &str,
|
||||
target: &str,
|
||||
current_reality: &str,
|
||||
unknowns: &[String],
|
||||
decisions: &[String],
|
||||
risks: &[String],
|
||||
depth: Option<&str>,
|
||||
) -> Value {
|
||||
let unknowns: Vec<&str> = unknowns.iter().map(String::as_str).filter(|u| !u.trim().is_empty()).collect();
|
||||
let decisions: Vec<&str> = decisions.iter().map(String::as_str).filter(|d| !d.trim().is_empty()).collect();
|
||||
let risks: Vec<&str> = risks.iter().map(String::as_str).filter(|r| !r.trim().is_empty()).collect();
|
||||
let mut topics: Vec<Value> = Vec::new();
|
||||
// each material unknown becomes a research topic aimed at resolving it
|
||||
for u in &unknowns {
|
||||
topics.push(json!({
|
||||
"topic": format!("resolve unknown: {u}"),
|
||||
"kind": "unknown-resolution",
|
||||
"why": format!("material unknown blocks confident construction: {}",
|
||||
truncate_chars(u, 80)),
|
||||
}));
|
||||
}
|
||||
for d in &decisions {
|
||||
topics.push(json!({
|
||||
"topic": format!("inform decision: {d}"),
|
||||
"kind": "decision-support",
|
||||
"why": format!("consequential decision needs precedent/evidence: {}",
|
||||
truncate_chars(d, 80)),
|
||||
}));
|
||||
}
|
||||
for r in &risks {
|
||||
topics.push(json!({
|
||||
"topic": format!("de-risk: {r}"),
|
||||
"kind": "risk-investigation",
|
||||
"why": format!("identified risk needs verification/mitigation evidence: {}",
|
||||
truncate_chars(r, 80)),
|
||||
}));
|
||||
}
|
||||
// precedent question (rule E) attaches ONLY when outward evidence is
|
||||
// plausibly relevant — an unknown/decision about external prior art.
|
||||
let joined: String = unknowns
|
||||
.iter()
|
||||
.chain(decisions.iter())
|
||||
.copied()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ")
|
||||
.to_lowercase();
|
||||
let needs_outward = OUTWARD_KEYWORDS.iter().any(|w| joined.contains(w));
|
||||
if needs_outward {
|
||||
topics.push(json!({
|
||||
"topic": "what already exists that is relevant to THIS intent \
|
||||
(adopt/adapt/combine/reject/innovate)",
|
||||
"kind": "prior-art",
|
||||
"why": "material unknown references existing practice — \
|
||||
precedent discovery required before deeper research",
|
||||
}));
|
||||
}
|
||||
let (depth, why) = match depth {
|
||||
Some(d) => (d.to_string(), "depth supplied by caller (recorded, caller accountable)".to_string()),
|
||||
None => {
|
||||
// consequence heuristics: novel/architectural/unsafe → deeper
|
||||
let hay = format!("{intent} {}", risks.join(" ")).to_lowercase();
|
||||
let consequential = CONSEQUENTIAL_KEYWORDS.iter().any(|w| hay.contains(w));
|
||||
let d = if consequential && !topics.is_empty() {
|
||||
"FULL"
|
||||
} else if needs_outward && !topics.is_empty() {
|
||||
"OUTWARD"
|
||||
} else if !topics.is_empty() {
|
||||
"INWARD"
|
||||
} else {
|
||||
"NONE"
|
||||
};
|
||||
let why = match d {
|
||||
"FULL" => "consequence heuristics: consequential keywords + research topics present",
|
||||
"OUTWARD" => "precedent-relevant unknowns present",
|
||||
"INWARD" => "local understanding suffices",
|
||||
_ => "no material unknowns — local evidence only",
|
||||
};
|
||||
(d.to_string(), why.to_string())
|
||||
}
|
||||
};
|
||||
json!({
|
||||
"intent": intent,
|
||||
"target": target,
|
||||
"current_reality_note": truncate_chars(current_reality, 200),
|
||||
"depth": {"depth": depth, "why": why},
|
||||
"topics": topics,
|
||||
"plan_generated_at": now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn arg_str<'a>(a: &'a Value, k: &str) -> &'a str {
|
||||
a.get(k).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
fn arg_str_list(a: &Value, k: &str) -> Vec<String> {
|
||||
a.get(k)
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// bb_research_depth({"route", "mode"?, "justification"?}) → depth decision dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_research_depth(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
research_depth(
|
||||
arg_str(&a, "route"),
|
||||
arg_str(&a, "mode"),
|
||||
arg_str(&a, "justification"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_new_basis({"question", "depth_decision", "issue_ref"?}) → basis dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_new_basis(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let dd = a
|
||||
.get("depth_decision")
|
||||
.cloned()
|
||||
.ok_or_else(|| "missing depth_decision".to_string())?;
|
||||
Ok(new_basis(arg_str(&a, "question"), &dd, arg_str(&a, "issue_ref")))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_set_dispositions({"basis", "dispositions"}) → {"basis", "errors"}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_set_dispositions(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut basis = a
|
||||
.get("basis")
|
||||
.cloned()
|
||||
.ok_or_else(|| "missing basis".to_string())?;
|
||||
let dispositions = a
|
||||
.get("dispositions")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(vec![]));
|
||||
let errors = set_dispositions(&mut basis, &dispositions);
|
||||
Ok(json!({"basis": basis, "errors": errors}))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_validate_basis({"basis"}) → [errors].
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_validate_basis(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(serde_json::to_value(validate_basis(basis)).unwrap_or_default())
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_is_qualified({"basis"}) → bool.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_is_qualified(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(Value::Bool(is_qualified(basis)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_attach_evidence_ref({"basis"}) → evidence_ref string.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_attach_evidence_ref(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(Value::String(attach_evidence_ref(basis)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_summary({"basis"}) → summary dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_summary(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(summary(basis))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_revise_depth({"basis", "new_depth", "reason"}) → {"basis", "revision"}
|
||||
/// or {"ok": false, "err": <exact Python ValueError message>}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_revise_depth(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut basis = a
|
||||
.get("basis")
|
||||
.cloned()
|
||||
.ok_or_else(|| "missing basis".to_string())?;
|
||||
let revision = revise_depth(&mut basis, arg_str(&a, "new_depth"), arg_str(&a, "reason"))?;
|
||||
Ok(json!({"basis": basis, "revision": revision}))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_to_json({"basis"}) → JSON string (json.dumps indent=1 sort_keys).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_to_json(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(Value::String(to_json(basis)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_from_json({"text"}) → parsed value.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_from_json(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| from_json(arg_str(&a, "text"))))
|
||||
}
|
||||
|
||||
/// bb_generate_research_plan({"intent", "target"?, "current_reality"?,
|
||||
/// "unknowns"?, "decisions"?, "risks"?, "depth"?}) → plan dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_generate_research_plan(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
generate_research_plan(
|
||||
arg_str(&a, "intent"),
|
||||
arg_str(&a, "target"),
|
||||
arg_str(&a, "current_reality"),
|
||||
&arg_str_list(&a, "unknowns"),
|
||||
&arg_str_list(&a, "decisions"),
|
||||
&arg_str_list(&a, "risks"),
|
||||
a.get("depth").and_then(Value::as_str),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn crate_exists_and_compiles() {
|
||||
assert!(true);
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
Generated
+34
-6
@@ -6,10 +6,37 @@ version = 4
|
||||
name = "construction_pattern"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[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"
|
||||
@@ -47,7 +74,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -72,10 +98,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -85,9 +113,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -7,5 +7,4 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
@@ -1,36 +1,173 @@
|
||||
//! branch_battery.rs — #2479 RUST batch 34: the pattern-matching kernels.
|
||||
//! ORACLE-VERIFIED: the token-matching discipline is correct — short
|
||||
//! signals (<4 chars) must match WHOLE tokens, not substrings (the
|
||||
//! 'ui'-in-'builtin' false-positive class is blocked by design).
|
||||
//! branch_battery.rs — frozen-oracle battery for the construction_pattern
|
||||
//! port (dichotomy wave #2479 tranche).
|
||||
//!
|
||||
//! Truth embedded VERBATIM from src/frozen_cp_truth.json (frozen by
|
||||
//! scripts/gen_construction_pattern_truth.py against the live Python
|
||||
//! oracle, RUNE_CP_CORE=python pinned): every case replays its recorded
|
||||
//! INPUT through the kernel and asserts byte-identical EXPECTED output
|
||||
//! (errors compare on the exact ValueError text).
|
||||
|
||||
#[cfg(test)]
|
||||
mod battery {
|
||||
// batch 155: the kernels are now SHIPPED pub fns (promoted from here).
|
||||
use crate::{entity_matches, path_matches};
|
||||
use crate::{
|
||||
combine_pattern_obligations, construct_pattern, derive_pattern_obligations,
|
||||
entity_matches, evaluate_applicability, path_matches, record_pattern_proposal,
|
||||
revise_pattern,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(include_str!("frozen_cp_truth.json")).expect("frozen truth parses")
|
||||
}
|
||||
|
||||
fn cases(op: &str) -> Vec<Value> {
|
||||
truth()["cases"]
|
||||
.as_array()
|
||||
.expect("cases array")
|
||||
.iter()
|
||||
.filter(|c| c["op"] == op)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn obj(v: &Value) -> serde_json::Map<String, Value> {
|
||||
v.as_object().expect("object").clone()
|
||||
}
|
||||
|
||||
fn run_construct(c: &Value) -> Result<Value, String> {
|
||||
construct_pattern(&obj(&c["input"]["kwargs"])).map(Value::Object)
|
||||
}
|
||||
|
||||
fn run_revise(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
let evidence: Vec<Value> = i["evidence"].as_array().cloned().unwrap_or_default();
|
||||
let changes = if i["changes"].is_null() { None } else { Some(obj(&i["changes"])) };
|
||||
revise_pattern(&obj(&i["pattern"]), i["reason"].as_str().unwrap_or(""), &evidence, changes.as_ref())
|
||||
.map(Value::Object)
|
||||
}
|
||||
|
||||
fn run_propose(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
if let Some(bounds) = i["confidence"].as_array() {
|
||||
let mut out = Vec::new();
|
||||
for b in bounds {
|
||||
out.push(record_pattern_proposal(
|
||||
i["pattern_id"].as_str().unwrap_or(""),
|
||||
i["provider"].as_str().unwrap_or(""),
|
||||
i["model"].as_str().unwrap_or(""),
|
||||
vec![],
|
||||
b.as_f64().unwrap(),
|
||||
)?);
|
||||
}
|
||||
return Ok(Value::Array(out));
|
||||
}
|
||||
let evidence: Vec<&str> =
|
||||
i["evidence"].as_array().map(|a| a.iter().filter_map(|e| e.as_str()).collect()).unwrap_or_default();
|
||||
record_pattern_proposal(
|
||||
i["pattern_id"].as_str().unwrap_or(""),
|
||||
i["provider"].as_str().unwrap_or(""),
|
||||
i["model"].as_str().unwrap_or(""),
|
||||
evidence,
|
||||
i["confidence"].as_f64().unwrap_or(f64::NAN),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_evaluate(c: &Value) -> Value {
|
||||
let i = &c["input"];
|
||||
evaluate_applicability(&obj(&i["pattern"]), &obj(&i["need"]), &obj(&i["surface_result"]), &obj(&i["authorities"]))
|
||||
}
|
||||
|
||||
fn run_derive(c: &Value) -> Value {
|
||||
let i = &c["input"];
|
||||
let exc: Vec<Value> =
|
||||
if i["project_exceptions"].is_null() { vec![] } else { i["project_exceptions"].as_array().cloned().unwrap_or_default() };
|
||||
derive_pattern_obligations(&obj(&i["pattern"]), &obj(&i["need"]), &obj(&i["surface_result"]), &obj(&i["authorities"]), &exc)
|
||||
}
|
||||
|
||||
fn run_combine(c: &Value) -> Value {
|
||||
let derivs: Vec<Value> = c["input"]["derivations"].as_array().cloned().unwrap_or_default();
|
||||
combine_pattern_obligations(&derivs)
|
||||
}
|
||||
|
||||
fn check(name: &str, got: Result<Value, String>, expected: &Value) {
|
||||
match expected.get("error") {
|
||||
Some(Value::String(msg)) => match got {
|
||||
Err(e) => assert_eq!(&e, msg, "{name}: error text"),
|
||||
Ok(v) => panic!("{name}: kernel succeeded ({v}) where oracle raised"),
|
||||
},
|
||||
_ => match got {
|
||||
Ok(v) => assert_eq!(&v, expected, "{name}: value mismatch"),
|
||||
Err(e) => panic!("{name}: unexpected kernel error {e}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_construct() {
|
||||
let cs = cases("construct");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_construct(c), &c["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_revise() {
|
||||
let cs = cases("revise");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_revise(c), &c["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_propose() {
|
||||
let cs = cases("propose");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_propose(c), &c["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_evaluate() {
|
||||
let cs = cases("evaluate");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
let got = run_evaluate(c);
|
||||
assert_eq!(&got, &c["expected"], "{}: value mismatch", c["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_derive() {
|
||||
let cs = cases("derive");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
let got = run_derive(c);
|
||||
assert_eq!(&got, &c["expected"], "{}: value mismatch", c["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_combine() {
|
||||
let cs = cases("combine");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
let got = run_combine(c);
|
||||
assert_eq!(&got, &c["expected"], "{}: value mismatch", c["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── oracle-verified matcher discipline (batch 155, preserved) ──
|
||||
|
||||
#[test]
|
||||
fn path_token_discipline() {
|
||||
// ORACLE-VERIFIED: short signals match WHOLE tokens only
|
||||
assert!(
|
||||
!path_matches("ui", "src/rune/rune_tui.py"),
|
||||
"'ui' is not a token in rune_tui.py"
|
||||
);
|
||||
assert!(
|
||||
!path_matches("ui", "src/rune/builtin.py"),
|
||||
"the 'ui'-in-'builtin' FP class"
|
||||
);
|
||||
assert!(
|
||||
path_matches("spine", "src/rune/spine.py"),
|
||||
"long signal: containment"
|
||||
);
|
||||
assert!(
|
||||
path_matches("tui", "src/rune/rune_tui.py"),
|
||||
"'tui' IS a token"
|
||||
);
|
||||
assert!(
|
||||
!path_matches("cfg", "config/settings.py"),
|
||||
"'cfg' not a token of config"
|
||||
);
|
||||
assert!(!path_matches("ui", "src/rune/rune_tui.py"), "'ui' is not a token in rune_tui.py");
|
||||
assert!(!path_matches("ui", "src/rune/builtin.py"), "the 'ui'-in-'builtin' FP class");
|
||||
assert!(path_matches("spine", "src/rune/spine.py"), "long signal: containment");
|
||||
assert!(path_matches("tui", "src/rune/rune_tui.py"), "'tui' IS a token");
|
||||
assert!(!path_matches("cfg", "config/settings.py"), "'cfg' not a token of config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -39,21 +176,13 @@ mod battery {
|
||||
assert!(entity_matches("module:", "module:spine"), "prefix match");
|
||||
assert!(!entity_matches("ui:", "region:left"), "prefix mismatch");
|
||||
assert!(entity_matches("spine", "module:spine"), "long containment");
|
||||
// ORACLE-VERIFIED: 'ui' (no colon, <4 chars) does NOT match 'ui:chat'
|
||||
// because the entity token-split keeps ':' as part of the token
|
||||
assert!(
|
||||
!entity_matches("ui", "ui:chat"),
|
||||
"oracle truth: token discipline"
|
||||
);
|
||||
assert!(!entity_matches("ui", "ui:chat"), "oracle truth: token discipline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_signal_containment() {
|
||||
assert!(path_matches("structural", "src/rune/structural_store.py"));
|
||||
assert!(entity_matches(
|
||||
"construction",
|
||||
"module:construction_pattern"
|
||||
));
|
||||
assert!(entity_matches("construction", "module:construction_pattern"));
|
||||
assert!(!path_matches("structural", "src/rune/other.py"));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,74 +1,855 @@
|
||||
//! construction_pattern — reusable construction knowledge (W6).
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! construction_pattern — reusable evidence about HOW a class of thing
|
||||
//! is normally built (SCAFFOLD-TRANCHE-006).
|
||||
//!
|
||||
//! FULL parity port of src/rune/construction_pattern.py (dichotomy wave,
|
||||
//! #2479 tranche). A pattern is GUIDANCE, not authority (§3): project
|
||||
//! evidence always wins. Applicability is evidence-backed and deterministic;
|
||||
//! obligation derivation is filtered by the project; provenance answers
|
||||
//! "why is this here"; revisions keep old-rule traceability (§17).
|
||||
//!
|
||||
//! FFI (doc-envelope law), one entry point per public oracle function:
|
||||
//! cp_construct({pattern dict}) → normalized pattern dict
|
||||
//! cp_revise({pattern, reason, evidence, changes?}) → new pattern dict
|
||||
//! cp_propose({pattern_id, provider, model, evidence, confidence}) → dict
|
||||
//! cp_evaluate({pattern, need, surface_result, authorities}) → verdict dict
|
||||
//! cp_derive({pattern, need, surface_result, authorities,
|
||||
//! project_exceptions?}) → derivation dict
|
||||
//! cp_combine({derivations}) → {"obligations", "conflicts"}
|
||||
//! The `need` arrives as a plain dict {objective, affected_surface,
|
||||
//! constraints} — the kernel never sees the live Need object.
|
||||
//!
|
||||
//! The boundary-aware matchers are the oracle-verified kernels promoted by
|
||||
//! batch 155 (the 'ui'-in-'builtin' false-positive class is blocked by
|
||||
//! design): short (<4 char, char-counted) signals match whole tokens only.
|
||||
//!
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the verdict lattices
|
||||
//! are enums with exhaustive `as_str` matches at the JSON boundary; the
|
||||
//! obligation `when`-condition is classified into a `WhenShape` enum and
|
||||
//! dispatched exhaustively (no string-soup if-chains); signal/path/entity
|
||||
//! lists borrow slices of the input JSON instead of cloning; tokenization
|
||||
//! is zero-copy over the lowered input.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConstructionPattern {
|
||||
pub pattern_id: String,
|
||||
pub description: String,
|
||||
pub obligations: Vec<serde_json::Value>,
|
||||
pub revision: String,
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
// ── verdict lattices (enums with exhaustive matches at the JSON boundary) ──
|
||||
|
||||
/// Applicability verdict lattice (§4-5): APPLIES / PARTIAL /
|
||||
/// DOES_NOT_APPLY / UNKNOWN. UNKNOWN never becomes APPLIES because a name
|
||||
/// sounds similar.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum ApplicabilityVerdict {
|
||||
Applies,
|
||||
Partial,
|
||||
DoesNotApply,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PatternApplicability {
|
||||
pub verdict: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
pub fn check_applicability(
|
||||
_pattern: &ConstructionPattern,
|
||||
project_evidence: &serde_json::Value,
|
||||
) -> PatternApplicability {
|
||||
// Patterns are GUIDANCE, not authority. Project evidence wins.
|
||||
if project_evidence
|
||||
.get("overrides_pattern")
|
||||
.and_then(|o| o.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return PatternApplicability {
|
||||
verdict: "DOES_NOT_APPLY".to_string(),
|
||||
reason: "project evidence overrides pattern".to_string(),
|
||||
};
|
||||
}
|
||||
PatternApplicability {
|
||||
verdict: "APPLIES".to_string(),
|
||||
reason: "no override".to_string(),
|
||||
impl ApplicabilityVerdict {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ApplicabilityVerdict::Applies => "APPLIES",
|
||||
ApplicabilityVerdict::Partial => "PARTIAL",
|
||||
ApplicabilityVerdict::DoesNotApply => "DOES_NOT_APPLY",
|
||||
ApplicabilityVerdict::Unknown => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// pathMatches — boundary-aware path-signal matching (batch 155: promoted
|
||||
/// from the test module to the shipped lib — the kernels ARE the product).
|
||||
/// Short (<4 char) signals match path TOKENS only (the 'ui'-in-'builtin'
|
||||
/// false-positive class); longer signals may use containment.
|
||||
/// Obligation `when`-condition verdict lattice (§6-7): REQUIRED /
|
||||
/// APPLICABLE / NOT_APPLICABLE / UNKNOWN — an obligation with no
|
||||
/// recognized `when` is UNKNOWN (never silently required).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum ObligationVerdict {
|
||||
Required,
|
||||
Applicable,
|
||||
NotApplicable,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ObligationVerdict {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ObligationVerdict::Required => "REQUIRED",
|
||||
ObligationVerdict::Applicable => "APPLICABLE",
|
||||
ObligationVerdict::NotApplicable => "NOT_APPLICABLE",
|
||||
ObligationVerdict::Unknown => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `when`-condition shapes (all deterministic; §6-7). Classified once,
|
||||
/// dispatched exhaustively — a shape outside the lattice is Unrecognized
|
||||
/// and surfaces as UNKNOWN, never REQUIRED.
|
||||
enum WhenShape<'a> {
|
||||
Unconditioned,
|
||||
Always,
|
||||
PathContains(&'a Value),
|
||||
EntitiesMatch(&'a Value),
|
||||
ContractsTouched(&'a Value),
|
||||
Unrecognized(Vec<String>),
|
||||
}
|
||||
|
||||
fn when_shape(obligation: &Map<String, Value>) -> WhenShape<'_> {
|
||||
match obligation.get("when") {
|
||||
None | Some(Value::Null) => WhenShape::Unconditioned,
|
||||
Some(w) => {
|
||||
if w.get("always").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return WhenShape::Always;
|
||||
}
|
||||
// Python checks KEY PRESENCE (`"path_contains" in when`), so a
|
||||
// null value still enters the branch — `get` mirrors that.
|
||||
if let Some(subs) = w.get("path_contains") {
|
||||
return WhenShape::PathContains(subs);
|
||||
}
|
||||
if let Some(subs) = w.get("entities_match") {
|
||||
return WhenShape::EntitiesMatch(subs);
|
||||
}
|
||||
if let Some(wanted) = w.get("contracts_touched") {
|
||||
return WhenShape::ContractsTouched(wanted);
|
||||
}
|
||||
match w {
|
||||
Value::Object(m) => WhenShape::Unrecognized(m.keys().cloned().collect()),
|
||||
// Python crashes on `when.get` before reaching the
|
||||
// sorted-shape message for non-dict `when` — unreachable.
|
||||
_ => WhenShape::Unrecognized(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KNOWN_KEYS: &[&str] = &[
|
||||
"pattern_id",
|
||||
"purpose",
|
||||
"revision",
|
||||
"maturity",
|
||||
"signals",
|
||||
"obligations",
|
||||
"provenance",
|
||||
"exclusions",
|
||||
"toolchain_notes",
|
||||
"runtime_effects",
|
||||
];
|
||||
|
||||
const REL_TYPES: &[&str] = &["implements", "exposes", "provides", "serves"];
|
||||
|
||||
// ── Python-repr helpers (reason/error strings embed Python list/dict reprs) ──
|
||||
|
||||
/// Single-quoted Python string repr (escapes \\, \', \n, \r, \t).
|
||||
fn py_quote(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push('\'');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\'' => out.push_str("\\'"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('\'');
|
||||
out
|
||||
}
|
||||
|
||||
/// Python `repr` of a JSON value: True/False/None, single-quoted strings,
|
||||
/// `[...]` lists, `{k: v}` dicts in insertion order.
|
||||
fn py_repr(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Null => "None".to_string(),
|
||||
Value::Bool(true) => "True".to_string(),
|
||||
Value::Bool(false) => "False".to_string(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::String(s) => py_quote(s),
|
||||
Value::Array(items) => {
|
||||
let inner: Vec<String> = items.iter().map(py_repr).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let inner: Vec<String> = map.iter().map(|(k, v)| format!("{}: {}", py_quote(k), py_repr(v))).collect();
|
||||
format!("{{{}}}", inner.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn py_str_list<T: AsRef<str>>(items: &[T]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| py_quote(s.as_ref())).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
|
||||
// ── shared boundary-aware matchers (Tranche 7 §0) ──────────────────────────
|
||||
|
||||
/// Oracle-faithful token split: runs of non-token chars are ONE separator,
|
||||
/// with leading/trailing empties kept (`re.split` semantics — only the
|
||||
/// empties are observable, via the empty-signal quirk). Zero-copy: tokens
|
||||
/// borrow slices of the lowered input.
|
||||
fn split_tokens<'a>(lowered: &'a str, keep_colon: bool) -> Vec<&'a str> {
|
||||
let is_tok = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || (keep_colon && c == ':');
|
||||
if lowered.is_empty() {
|
||||
return vec![""];
|
||||
}
|
||||
let mut toks: Vec<&'a str> = Vec::new();
|
||||
let mut start: Option<usize> = None;
|
||||
let mut ended_with_sep = false;
|
||||
for (i, c) in lowered.char_indices() {
|
||||
if is_tok(c) {
|
||||
if start.is_none() {
|
||||
start = Some(i);
|
||||
}
|
||||
ended_with_sep = false;
|
||||
} else {
|
||||
if let Some(s) = start.take() {
|
||||
toks.push(&lowered[s..i]);
|
||||
}
|
||||
ended_with_sep = true;
|
||||
}
|
||||
}
|
||||
if let Some(s) = start {
|
||||
toks.push(&lowered[s..]);
|
||||
}
|
||||
if lowered.starts_with(|c: char| !is_tok(c)) {
|
||||
toks.insert(0, "");
|
||||
}
|
||||
if ended_with_sep {
|
||||
toks.push("");
|
||||
}
|
||||
toks
|
||||
}
|
||||
|
||||
/// Boundary-aware path-signal matching: short (<4 char) signals match path
|
||||
/// TOKENS only (the 'ui'-in-'builtin' false-positive class); longer signals
|
||||
/// may use containment.
|
||||
pub fn path_matches(signal: &str, path: &str) -> bool {
|
||||
let s = signal.to_lowercase();
|
||||
if s.len() >= 4 {
|
||||
if s.chars().count() >= 4 {
|
||||
return path.to_lowercase().contains(&s);
|
||||
}
|
||||
let lower = path.to_lowercase();
|
||||
let tokens: std::collections::HashSet<&str> = lower
|
||||
.split(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit())
|
||||
.collect();
|
||||
tokens.contains(s.as_str())
|
||||
split_tokens(&path.to_lowercase(), false).iter().any(|t| *t == s)
|
||||
}
|
||||
|
||||
/// entityMatches — same discipline for entity ids; ':'-suffixed signals
|
||||
/// match the namespace prefix (structural kinds carry meaning there).
|
||||
/// Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
/// 'ui:', 'region:') match the id's namespace prefix — structural kinds
|
||||
/// carry meaning in their prefix, and prefix equality is exact, not a
|
||||
/// substring accident.
|
||||
pub fn entity_matches(signal: &str, entity_id: &str) -> bool {
|
||||
let s = signal.to_lowercase();
|
||||
if s.ends_with(':') {
|
||||
return entity_id.to_lowercase().starts_with(&s);
|
||||
return entity_id.to_lowercase().starts_with(s.as_str());
|
||||
}
|
||||
if s.len() >= 4 {
|
||||
if s.chars().count() >= 4 {
|
||||
return entity_id.to_lowercase().contains(&s);
|
||||
}
|
||||
let lower = entity_id.to_lowercase();
|
||||
let tokens: std::collections::HashSet<&str> = lower
|
||||
.split(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != ':')
|
||||
.collect();
|
||||
tokens.contains(s.as_str())
|
||||
split_tokens(&entity_id.to_lowercase(), true).iter().any(|t| *t == s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// ── small JSON accessors (borrowed; owned only where the output needs it) ──
|
||||
|
||||
fn str_list(v: &Value) -> Vec<&str> {
|
||||
v.as_array()
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn str_list_owned(v: &Value) -> Vec<String> {
|
||||
str_list(v).into_iter().map(String::from).collect()
|
||||
}
|
||||
|
||||
/// `ss.relationships(doc, from_id=e)` — including the falsy-filter quirk:
|
||||
/// an empty from_id applies NO filter (Python `if from_id and ...`).
|
||||
fn relationships<'a>(doc: &'a Value, from_id: &str) -> Vec<&'a Map<String, Value>> {
|
||||
doc.get("relationships")
|
||||
.and_then(|r| r.as_array())
|
||||
.map(|rels| {
|
||||
rels.iter()
|
||||
.filter_map(|r| r.as_object())
|
||||
.filter(|r| from_id.is_empty() || r.get("from").and_then(|f| f.as_str()).unwrap_or("") == from_id)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ── pattern object (open-world) ───────────────────────────────────────────
|
||||
|
||||
/// `ConstructionPattern.from_dict` / constructor / `to_dict`: validate the
|
||||
/// required fields, then normalize (sorted provenance-family lists, ext
|
||||
/// passthrough). The input and output share one shape — `to_dict` output
|
||||
/// round-trips through here unchanged.
|
||||
pub fn construct_pattern(input: &Map<String, Value>) -> Result<Map<String, Value>, String> {
|
||||
let pattern_id = input.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if pattern_id.is_empty() {
|
||||
return Err("pattern_id required".to_string());
|
||||
}
|
||||
let purpose = input.get("purpose").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if purpose.is_empty() {
|
||||
return Err("purpose required".to_string());
|
||||
}
|
||||
if let Some(obs) = input.get("obligations").and_then(|v| v.as_array()) {
|
||||
for o in obs {
|
||||
for key in ["id", "obligation", "when"] {
|
||||
if o.get(key).is_none() {
|
||||
return Err(format!("obligation missing {}: {}", py_quote(key), py_repr(o)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Map::new();
|
||||
out.insert("pattern_id".into(), Value::String(pattern_id.to_string()));
|
||||
out.insert("purpose".into(), Value::String(purpose.to_string()));
|
||||
out.insert(
|
||||
"revision".into(),
|
||||
input.get("revision").cloned().unwrap_or(json!(1)),
|
||||
);
|
||||
out.insert(
|
||||
"maturity".into(),
|
||||
input.get("maturity").cloned().unwrap_or(json!("CANDIDATE")),
|
||||
);
|
||||
out.insert(
|
||||
"signals".into(),
|
||||
input.get("signals").cloned().unwrap_or_else(|| Value::Object(Map::new())),
|
||||
);
|
||||
out.insert(
|
||||
"obligations".into(),
|
||||
input.get("obligations").cloned().unwrap_or_else(|| Value::Array(vec![])),
|
||||
);
|
||||
for key in ["provenance", "exclusions", "toolchain_notes", "runtime_effects"] {
|
||||
let mut vals = input.get(key).map(str_list_owned).unwrap_or_default();
|
||||
vals.sort_unstable();
|
||||
out.insert(key.into(), json!(vals));
|
||||
}
|
||||
for (k, v) in input.iter() {
|
||||
if !KNOWN_KEYS.contains(&k.as_str()) {
|
||||
out.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `ConstructionPattern.revise` (§17): returns a NEW pattern at revision+1
|
||||
/// recording the change — prior meaning is never invisibly mutated.
|
||||
pub fn revise_pattern(
|
||||
pattern: &Map<String, Value>,
|
||||
reason: &str,
|
||||
evidence: &[Value],
|
||||
changes: Option<&Map<String, Value>>,
|
||||
) -> Result<Map<String, Value>, String> {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let revision = pattern.get("revision").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
let mut merged = pattern.clone();
|
||||
merged.remove("pattern_id");
|
||||
if let Some(ch) = changes {
|
||||
for (k, v) in ch.iter() {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
merged.insert("revision".into(), json!(revision + 1));
|
||||
let mut ev: Vec<String> = evidence.iter().filter_map(|e| e.as_str().map(String::from)).collect();
|
||||
ev.sort_unstable();
|
||||
let mut changed: Vec<String> = changes.map(|ch| ch.keys().cloned().collect()).unwrap_or_default();
|
||||
changed.sort_unstable();
|
||||
let entry = json!({
|
||||
"prior_revision": revision,
|
||||
"reason": reason.chars().take(200).collect::<String>(),
|
||||
"evidence": ev,
|
||||
"changed": changed,
|
||||
});
|
||||
match merged.get_mut("revision_history") {
|
||||
Some(Value::Array(hist)) => hist.push(entry),
|
||||
_ => {
|
||||
merged.insert("revision_history".into(), Value::Array(vec![entry]));
|
||||
}
|
||||
}
|
||||
merged.insert("pattern_id".into(), Value::String(pattern_id));
|
||||
construct_pattern(&merged)
|
||||
}
|
||||
|
||||
// ── model proposal seam (§5 — recorded, never authoritative) ────────────────
|
||||
|
||||
/// A model may PROPOSE a pattern; it may not certify applicability.
|
||||
pub fn record_pattern_proposal(
|
||||
pattern_id: &str,
|
||||
provider: &str,
|
||||
model: &str,
|
||||
evidence: Vec<&str>,
|
||||
confidence: f64,
|
||||
) -> Result<Value, String> {
|
||||
if provider.is_empty() || model.is_empty() {
|
||||
return Err("pattern proposal requires provider+model".to_string());
|
||||
}
|
||||
if !(0.0..=1.0).contains(&confidence) {
|
||||
return Err("confidence must be in [0,1]".to_string());
|
||||
}
|
||||
let mut ev = evidence;
|
||||
ev.sort_unstable();
|
||||
Ok(json!({
|
||||
"kind": "pattern_proposal",
|
||||
"pattern_id": pattern_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": ev,
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}))
|
||||
}
|
||||
|
||||
// ── applicability (§4 — deterministic signals only) ─────────────────────────
|
||||
|
||||
fn surface_kind_present(kind: &str, surface: &Map<String, Value>) -> bool {
|
||||
let mapped = match kind {
|
||||
"source_path" => "affected_paths",
|
||||
"workflow_config" => "workflow_config_surfaces",
|
||||
"ui_component" => "affected_ui_surfaces",
|
||||
"layout_region" => "affected_regions",
|
||||
_ => "",
|
||||
};
|
||||
if surface.get(mapped).and_then(|v| v.as_array()).map(|a| !a.is_empty()).unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
surface
|
||||
.get("resolutions")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|rs| rs.iter().any(|r| r.get("kind").and_then(|k| k.as_str()).unwrap_or("") == kind))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Deterministic applicability verdict from the pattern's declared signals.
|
||||
pub fn evaluate_applicability(
|
||||
pattern: &Map<String, Value>,
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let objective = need.get("objective").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let exclusions: Vec<&str> = pattern.get("exclusions").map(str_list).unwrap_or_default();
|
||||
let need_exclusions: Vec<&str> = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("pattern_exclusions"))
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
|
||||
// §3/§18: PROJECT evidence overrides the pattern.
|
||||
let objective_low = objective.to_lowercase();
|
||||
for ex in exclusions.iter().copied().chain(need_exclusions.iter().copied()) {
|
||||
if objective_low.contains(&ex.to_lowercase()) || need_exclusions.contains(&ex) {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": ApplicabilityVerdict::DoesNotApply.as_str(),
|
||||
"reason": format!("declared exclusion matched: {ex}"),
|
||||
"evidence": {"exclusion": ex, "source": "project/need declaration"},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let signals = match pattern.get("signals").and_then(|v| v.as_object()) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": ApplicabilityVerdict::Unknown.as_str(),
|
||||
"reason": "pattern declares no applicability signals — nothing deterministic to check",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut groups_hit: Vec<&'static str> = Vec::new();
|
||||
let mut groups_declared: usize = 0;
|
||||
let mut evidence = Map::new();
|
||||
|
||||
let paths: Vec<&str> = surface
|
||||
.get("affected_paths")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let entities: Vec<&str> = surface.get("affected_existing_entities").map(str_list).unwrap_or_default();
|
||||
|
||||
let mut check = |group: &'static str, candidates: &[&str], present: &dyn Fn(&str) -> bool| {
|
||||
if candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
groups_declared += 1;
|
||||
let hits: Vec<&str> = candidates.iter().filter(|c| present(c)).copied().collect();
|
||||
if !hits.is_empty() {
|
||||
groups_hit.push(group);
|
||||
evidence.insert(group.to_string(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
||||
}
|
||||
};
|
||||
|
||||
let surface_kinds = signals.get("surface_kinds").map(str_list).unwrap_or_default();
|
||||
check("surface_kinds", &surface_kinds, &|c| surface_kind_present(c, surface));
|
||||
let path_signals = signals.get("path_contains").map(str_list).unwrap_or_default();
|
||||
check("path_contains", &path_signals, &|c| paths.iter().any(|p| path_matches(c, p)));
|
||||
let entity_signals = signals.get("entities_match").map(str_list).unwrap_or_default();
|
||||
check("entities_match", &entity_signals, &|c| entities.iter().any(|e| entity_matches(c, e)));
|
||||
let objective_words: Vec<String> =
|
||||
objective.replace('/', " ").split_whitespace().map(|w| w.to_lowercase()).collect();
|
||||
let objective_signals_owned: Vec<String> =
|
||||
signals.get("objective_any").map(str_list).unwrap_or_default().into_iter().map(str::to_lowercase).collect();
|
||||
let objective_signals: Vec<&str> = objective_signals_owned.iter().map(String::as_str).collect();
|
||||
check("objective_any", &objective_signals, &|c| objective_words.iter().any(|w| w == c));
|
||||
|
||||
if let Some(contracts) = signals.get("contracts_match").and_then(|v| v.as_array()) {
|
||||
if !contracts.is_empty() {
|
||||
groups_declared += 1;
|
||||
let wanted: Vec<&str> = contracts.iter().filter_map(|c| c.as_str()).collect();
|
||||
let hits: Vec<String> = authorities
|
||||
.values()
|
||||
.flat_map(|doc| entities.iter().map(move |&e| (doc, e)))
|
||||
.flat_map(|(doc, e)| relationships(doc, e).into_iter().map(move |r| (e, r)))
|
||||
.filter(|(_, r)| REL_TYPES.contains(&r.get("type").and_then(|t| t.as_str()).unwrap_or("")))
|
||||
.map(|(e, r)| (e, r.get("to").and_then(|t| t.as_str()).unwrap_or("")))
|
||||
.filter(|(_, to)| wanted.contains(to))
|
||||
.map(|(e, to)| format!("{e}->{to}"))
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
groups_hit.push("contracts_match");
|
||||
evidence.insert("contracts_match".into(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if groups_declared == 0 {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": ApplicabilityVerdict::Unknown.as_str(),
|
||||
"reason": "no applicable signal groups declared",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
let verdict = if groups_hit.len() == groups_declared {
|
||||
ApplicabilityVerdict::Applies
|
||||
} else if !groups_hit.is_empty() {
|
||||
ApplicabilityVerdict::Partial
|
||||
} else {
|
||||
ApplicabilityVerdict::DoesNotApply
|
||||
};
|
||||
let mut hit_sorted = groups_hit.clone();
|
||||
hit_sorted.sort_unstable();
|
||||
let detail = if hit_sorted.is_empty() { "none".to_string() } else { py_str_list(&hit_sorted) };
|
||||
json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": verdict.as_str(),
|
||||
"reason": format!("{}/{} signal groups produced evidence: {}", groups_hit.len(), groups_declared, detail),
|
||||
"evidence": evidence,
|
||||
})
|
||||
}
|
||||
|
||||
// ── obligation derivation (§6-7 — filtered by project evidence) ─────────────
|
||||
|
||||
/// Evaluate one obligation's `when` condition against project evidence.
|
||||
pub fn obligation_when_hit(
|
||||
obligation: &Map<String, Value>,
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> (ObligationVerdict, String) {
|
||||
let paths: Vec<&str> = surface
|
||||
.get("affected_paths")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let entities: Vec<&str> = surface
|
||||
.get("affected_existing_entities")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("affected_ui_surfaces").map(str_list).unwrap_or_default())
|
||||
.chain(surface.get("affected_regions").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
|
||||
match when_shape(obligation) {
|
||||
WhenShape::Unconditioned => (
|
||||
ObligationVerdict::Unknown,
|
||||
"obligation declares no applicability condition".to_string(),
|
||||
),
|
||||
WhenShape::Always => (
|
||||
ObligationVerdict::Required,
|
||||
"pattern-level obligation (always applies when the pattern applies)".to_string(),
|
||||
),
|
||||
WhenShape::PathContains(subs) => {
|
||||
let hits: Vec<&str> = str_list(subs)
|
||||
.iter()
|
||||
.flat_map(|sub| paths.iter().filter(|p| path_matches(sub, p)).copied())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (ObligationVerdict::Applicable, format!("affected path(s): {}", py_str_list(&hits[..3.min(hits.len())])));
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("no affected path matches {}", py_repr(subs)))
|
||||
}
|
||||
WhenShape::EntitiesMatch(subs) => {
|
||||
let hits: Vec<&str> = str_list(subs)
|
||||
.iter()
|
||||
.flat_map(|sub| entities.iter().filter(|e| entity_matches(sub, e)).copied())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (
|
||||
ObligationVerdict::Applicable,
|
||||
format!("affected entity/entities: {}", py_str_list(&hits[..3.min(hits.len())])),
|
||||
);
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("no affected entity matches {}", py_repr(subs)))
|
||||
}
|
||||
WhenShape::ContractsTouched(wanted) => {
|
||||
let mut touched: Vec<String> = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("contracts"))
|
||||
.map(str_list_owned)
|
||||
.unwrap_or_default();
|
||||
for doc in authorities.values() {
|
||||
for e in &entities {
|
||||
for r in relationships(doc, e) {
|
||||
let rtype = r.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let to = r.get("to").and_then(|t| t.as_str()).unwrap_or("");
|
||||
if REL_TYPES.contains(&rtype) && !touched.iter().any(|t| t == to) {
|
||||
touched.push(to.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hits: Vec<&str> =
|
||||
str_list(wanted).into_iter().filter(|c| touched.iter().any(|t| t == c)).collect();
|
||||
if !hits.is_empty() {
|
||||
return (ObligationVerdict::Applicable, format!("contract(s) touched: {}", py_str_list(&hits)));
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("touched contracts do not include {}", py_repr(wanted)))
|
||||
}
|
||||
WhenShape::Unrecognized(keys) => {
|
||||
let mut keys = keys;
|
||||
keys.sort_unstable();
|
||||
(ObligationVerdict::Unknown, format!("unrecognized when-condition shape: {}", py_str_list(&keys)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive this pattern's obligations FILTERED by project evidence (§7).
|
||||
pub fn derive_pattern_obligations(
|
||||
pattern: &Map<String, Value>,
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
project_exceptions: &[Value],
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let revision = pattern.get("revision").cloned().unwrap_or(json!(1));
|
||||
let provenance = pattern.get("provenance").cloned().unwrap_or_else(|| Value::Array(vec![]));
|
||||
let mut accepted = Vec::new();
|
||||
let mut rejected = Vec::new();
|
||||
let mut unresolved = Vec::new();
|
||||
if let Some(obs) = pattern.get("obligations").and_then(|v| v.as_array()) {
|
||||
for o in obs {
|
||||
let empty = Map::new();
|
||||
let o_obj: &Map<String, Value> = o.as_object().unwrap_or(&empty);
|
||||
let mut entry = Map::new();
|
||||
entry.insert("id".into(), o.get("id").cloned().unwrap_or(Value::Null));
|
||||
entry.insert("obligation".into(), o.get("obligation").cloned().unwrap_or(Value::Null));
|
||||
entry.insert("pattern".into(), Value::String(pattern_id.clone()));
|
||||
entry.insert("revision".into(), revision.clone());
|
||||
entry.insert("provenance".into(), Value::String("PATTERN".to_string()));
|
||||
entry.insert("pattern_provenance".into(), provenance.clone());
|
||||
let oid = o.get("id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if let Some(exc) = project_exceptions
|
||||
.iter()
|
||||
.find(|x| x.get("obligation").and_then(|v| v.as_str()).unwrap_or("") == oid)
|
||||
{
|
||||
entry.insert("verdict".into(), Value::String("EXCEPTED".to_string()));
|
||||
entry.insert(
|
||||
"reason".into(),
|
||||
exc.get("reason").cloned().unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
// Oracle field names on the exception are `evidence` /
|
||||
// `authority`; they land on the entry as exception_*.
|
||||
entry.insert(
|
||||
"exception_evidence".into(),
|
||||
exc.get("evidence").cloned().unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
entry.insert(
|
||||
"exception_authority".into(),
|
||||
exc.get("authority").cloned().unwrap_or_else(|| Value::String("user".to_string())),
|
||||
);
|
||||
rejected.push(Value::Object(entry));
|
||||
continue;
|
||||
}
|
||||
let (verdict, reason) = obligation_when_hit(o_obj, need, surface, authorities);
|
||||
entry.insert("verdict".into(), Value::String(verdict.as_str().to_string()));
|
||||
entry.insert("reason".into(), Value::String(reason));
|
||||
match verdict {
|
||||
ObligationVerdict::Required | ObligationVerdict::Applicable => accepted.push(Value::Object(entry)),
|
||||
ObligationVerdict::NotApplicable => rejected.push(Value::Object(entry)),
|
||||
ObligationVerdict::Unknown => unresolved.push(Value::Object(entry)),
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"pattern": pattern_id,
|
||||
"revision": revision,
|
||||
"accepted": accepted,
|
||||
"rejected": rejected,
|
||||
"unresolved": unresolved,
|
||||
})
|
||||
}
|
||||
|
||||
/// Combine multiple patterns' derivations (§26-27): duplicates of the same
|
||||
/// obligation id collapse (provenance lists merge); conflicting obligations
|
||||
/// surface as conflicts — never silently chosen.
|
||||
pub fn combine_pattern_obligations(derivations: &[Value]) -> Value {
|
||||
let mut merged: Map<String, Value> = Map::new();
|
||||
let mut order: Vec<String> = Vec::new();
|
||||
let mut conflicts = Vec::new();
|
||||
for d in derivations {
|
||||
if let Some(acc) = d.get("accepted").and_then(|v| v.as_array()) {
|
||||
for entry in acc {
|
||||
let oid = entry.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
if let Some(prev) = merged.get_mut(&oid) {
|
||||
let prev_text = prev.get("obligation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let new_text = entry.get("obligation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if prev_text != new_text {
|
||||
conflicts.push(json!({
|
||||
"obligation_id": oid,
|
||||
"conflict": "same id, different text",
|
||||
"sources": [
|
||||
prev.get("pattern").cloned().unwrap_or(Value::Null),
|
||||
entry.get("pattern").cloned().unwrap_or(Value::Null),
|
||||
],
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
let prev_obj = prev.as_object_mut().unwrap();
|
||||
match prev_obj.get_mut("also_from") {
|
||||
Some(Value::Array(arr)) => {
|
||||
arr.push(entry.get("pattern").cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
_ => {
|
||||
prev_obj.insert(
|
||||
"also_from".into(),
|
||||
json!([entry.get("pattern").cloned().unwrap_or(Value::Null)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
order.push(oid.clone());
|
||||
merged.insert(oid, entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
order.sort_unstable();
|
||||
let obligations: Vec<Value> = order.into_iter().filter_map(|k| merged.remove(&k)).collect();
|
||||
json!({"obligations": obligations, "conflicts": conflicts})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn obj_arg(a: &Value) -> Result<Map<String, Value>, String> {
|
||||
a.as_object().cloned().ok_or_else(|| "argument must be a JSON object".to_string())
|
||||
}
|
||||
|
||||
/// cp_construct({pattern dict}) → normalized pattern dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_construct(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| construct_pattern(&a).map(Value::Object));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_revise({pattern, reason, evidence, changes?}) → new pattern dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_revise(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let reason = a.get("reason").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let evidence: Vec<Value> =
|
||||
a.get("evidence").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
let changes = a.get("changes").and_then(|v| v.as_object()).cloned();
|
||||
revise_pattern(&pattern, reason, &evidence, changes.as_ref()).map(Value::Object)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_propose({pattern_id, provider, model, evidence, confidence}) → dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_propose(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| {
|
||||
let evidence = a.get("evidence").map(str_list).unwrap_or_default();
|
||||
let confidence = a.get("confidence").and_then(|v| v.as_f64()).unwrap_or(f64::NAN);
|
||||
record_pattern_proposal(
|
||||
a.get("pattern_id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("provider").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("model").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
evidence,
|
||||
confidence,
|
||||
)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_evaluate({pattern, need, surface_result, authorities}) → verdict dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_evaluate(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let need = a.get("need").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let surface = a.get("surface_result").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let authorities = a.get("authorities").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
evaluate_applicability(&pattern, &need, &surface, &authorities)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_derive({pattern, need, surface_result, authorities,
|
||||
/// project_exceptions?}) → derivation dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_derive(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let need = a.get("need").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let surface = a.get("surface_result").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let authorities = a.get("authorities").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let exceptions: Vec<Value> =
|
||||
a.get("project_exceptions").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
derive_pattern_obligations(&pattern, &need, &surface, &authorities, &exceptions)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_combine({derivations}) → {"obligations", "conflicts"}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_combine(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let derivs: Vec<Value> =
|
||||
a.get("derivations").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
combine_pattern_obligations(&derivs)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no cp_free_string export — sibling crates sharing the Python
|
||||
// process already export identical free symbols; a duplicate breaks the
|
||||
// cdylib link. Callers free through capability_scoring's cs_free_string.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1,101 @@
|
||||
//! drift_observability branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_do_truth.json: 33 cases across all five public functions
|
||||
//! and every branch — severity tiers, the three alert thresholds with
|
||||
//! 14-day stall boundary, malformed/naive/NULL/empty last_washed skips,
|
||||
//! all-three combined, the strict >0.3/>0.1 assessment lattice with the
|
||||
//! velocity==10 boundary, empty db). Frozen 2026-10-11 via
|
||||
//! scripts/gen_drift_observability_truth.py with RUNE_DO_CORE=python
|
||||
//! pinned (poisoned-truth law). Embedded VERBATIM — never transcribed.
|
||||
|
||||
use crate::{
|
||||
add_alert, acknowledge_alert, check_alerts, get_unacknowledged_alerts,
|
||||
run_retrospective,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_do_truth.json");
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][group]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
for case in cases(group) {
|
||||
let got = f(&case["input"]);
|
||||
assert_eq!(
|
||||
&got,
|
||||
&case["expected"],
|
||||
"[{}] case '{}' diverged",
|
||||
group,
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_alert_parity_battery() {
|
||||
replay("add_alert", |i| {
|
||||
json!({"alerts": add_alert(
|
||||
i["alerts"].as_array().unwrap(),
|
||||
i["alert_type"].as_str().unwrap(),
|
||||
i["message"].as_str().unwrap(),
|
||||
i["severity"].as_str().unwrap(),
|
||||
i["now"].as_str().unwrap(),
|
||||
)})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_alerts_parity_battery() {
|
||||
replay("check_alerts", |i| {
|
||||
let (all, new_alerts) = check_alerts(
|
||||
i["alerts"].as_array().unwrap(),
|
||||
&i["velocity"],
|
||||
&i["direction"],
|
||||
i["last_washed"].as_str(),
|
||||
i["now"].as_str().unwrap(),
|
||||
);
|
||||
json!({"new_alerts": new_alerts, "alerts": all})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_retrospective_parity_battery() {
|
||||
replay("run_retrospective", |i| {
|
||||
let n = |k: &str| i[k].as_i64().unwrap_or(0);
|
||||
run_retrospective(
|
||||
n("new_entries"),
|
||||
n("washed_entries"),
|
||||
n("promoted_entries"),
|
||||
n("decayed_entries"),
|
||||
&i["velocity"],
|
||||
&i["direction"],
|
||||
n("days"),
|
||||
i["now"].as_str().unwrap(),
|
||||
i["now_local"].as_str().unwrap(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_unacknowledged_alerts_parity_battery() {
|
||||
replay("get_unacknowledged_alerts", |i| {
|
||||
json!({"unacked": get_unacknowledged_alerts(i["alerts"].as_array().unwrap())})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acknowledge_alert_parity_battery() {
|
||||
replay("acknowledge_alert", |i| {
|
||||
let index = match &i["index"] {
|
||||
Value::Bool(b) => *b as i64,
|
||||
v => v.as_i64().unwrap_or(-1),
|
||||
};
|
||||
let (alerts, changed) = acknowledge_alert(i["alerts"].as_array().unwrap(), index);
|
||||
json!({"alerts": alerts, "changed": changed})
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,25 +1,594 @@
|
||||
//! drift_observability — drift dashboard (W6).
|
||||
pub fn dashboard(health: &serde_json::Value) -> serde_json::Value {
|
||||
serde_json::json!({"dashboard": health})
|
||||
//! drift_observability — drift monitoring, alerts, and retrospectives (W6).
|
||||
//!
|
||||
//! FULL parity port of src/rune/drift_observability.py (dichotomy wave,
|
||||
//! #2479 tranche). Makes drift visible — observable drift is governable
|
||||
//! drift (Layered Mutability, arXiv:2604.14717; VaG, arXiv:2608.05810).
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
|
||||
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
|
||||
//! * dob_add_alert({alerts, alert_type, message, severity, now})
|
||||
//! → {"alerts": [...]}
|
||||
//! * dob_check_alerts({alerts, velocity, direction, last_washed, now})
|
||||
//! → {"new_alerts": [...], "alerts": [...]}
|
||||
//! * dob_run_retrospective({new_entries, washed_entries, promoted_entries,
|
||||
//! decayed_entries, velocity, direction, days, now, now_local})
|
||||
//! → {"report": {...}, "filename": "YYYYMMDD_HHMMSS"}
|
||||
//! * dob_get_unacknowledged_alerts({alerts}) → {"unacked": [...]}
|
||||
//! * dob_acknowledge_alert({alerts, index}) → {"alerts": [...], "changed": bool}
|
||||
//!
|
||||
//! No-clock law: `now` (aware UTC ISO-8601, as the oracle's
|
||||
//! datetime.now(timezone.utc).isoformat()) and `now_local` (naive local
|
||||
//! ISO, as datetime.now().isoformat()) are always arguments.
|
||||
//!
|
||||
//! Boundary: the sqlite reads (drift_core's velocity/direction, the
|
||||
//! MAX(last_washed) probe, the four retrospective counts) and the
|
||||
//! alerts/retrospectives FILE IO stay in the Python facade — the kernel
|
||||
//! is a pure function of their results (memory_store precedent).
|
||||
//!
|
||||
//! No dob_free_string export — sibling crates sharing the Python process
|
||||
//! already export identical free symbols; callers free through
|
||||
//! capability_scoring's cs_free_string.
|
||||
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the emitted-alert
|
||||
//! vocabulary (kind + severity) and the retrospective assessment lattice
|
||||
//! are enums with exhaustive matches at the JSON boundary; assessment
|
||||
//! summaries come back as canonical `&'static str`; check_alerts composes
|
||||
//! the appended list with a borrow→clone chain instead of an intermediate
|
||||
//! owned Vec clone; FFI errors are a typed `FfiError` enum behind the
|
||||
//! Result envelope. The add_alert INPUT severity stays a pass-through
|
||||
//! &str deliberately — the oracle forwards any caller string verbatim,
|
||||
//! so a closed input lattice would be a parity break, not an advantage.
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::fmt;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
pub const VELOCITY_THRESHOLD: i64 = 10;
|
||||
pub const NEGATIVE_DRIFT_THRESHOLD: f64 = 0.1;
|
||||
pub const PROMOTION_STALL_DAYS: i64 = 14;
|
||||
|
||||
// ── decision lattices (enums, not string soup) ──────────────────────────
|
||||
|
||||
/// Severity of an alert EMITTED by `check_alerts` — the oracle's checks
|
||||
/// only ever produce "warning" (velocity, negative drift) or "info"
|
||||
/// (promotion stall). Caller-supplied severities on `add_alert` are NOT
|
||||
/// this lattice (pass-through &str, see module doc).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum EmittedSeverity {
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
impl EmittedSeverity {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
EmittedSeverity::Warning => "warning",
|
||||
EmittedSeverity::Info => "info",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The three alert kinds `check_alerts` can raise (§ the oracle's three
|
||||
/// threshold checks). Message formatting is centralized per kind.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum AlertKind {
|
||||
HighVelocity,
|
||||
NegativeDrift,
|
||||
PromotionStall,
|
||||
}
|
||||
|
||||
impl AlertKind {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
AlertKind::HighVelocity => "high_velocity",
|
||||
AlertKind::NegativeDrift => "negative_drift",
|
||||
AlertKind::PromotionStall => "promotion_stall",
|
||||
}
|
||||
}
|
||||
|
||||
fn severity(self) -> EmittedSeverity {
|
||||
match self {
|
||||
AlertKind::HighVelocity | AlertKind::NegativeDrift => EmittedSeverity::Warning,
|
||||
AlertKind::PromotionStall => EmittedSeverity::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An alert raised by `check_alerts` — held typed until the JSON
|
||||
/// boundary so the stamped and unstamped projections share one source
|
||||
/// (the oracle returns unstamped copies from check_alerts and appends
|
||||
/// stamped ones to the persisted list).
|
||||
struct NewAlert {
|
||||
kind: AlertKind,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl NewAlert {
|
||||
fn unstamped(&self) -> Value {
|
||||
json!({
|
||||
"type": self.kind.as_str(),
|
||||
"message": self.message,
|
||||
"severity": self.kind.severity().as_str(),
|
||||
})
|
||||
}
|
||||
|
||||
fn stamped(&self, now: &str) -> Value {
|
||||
json!({
|
||||
"type": self.kind.as_str(),
|
||||
"message": self.message,
|
||||
"severity": self.kind.severity().as_str(),
|
||||
"timestamp": now,
|
||||
"acknowledged": false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrospective assessment lattice — strict >0.3-and-calm > >0.1 >
|
||||
/// everything else; the tier order is centralized in `classify` and the
|
||||
/// summary vocabulary is canonical `&'static str` (no allocation).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Assessment {
|
||||
Positive,
|
||||
Neutral,
|
||||
Concerning,
|
||||
}
|
||||
|
||||
impl Assessment {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Assessment::Positive => "positive",
|
||||
Assessment::Neutral => "neutral",
|
||||
Assessment::Concerning => "concerning",
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(self) -> &'static str {
|
||||
match self {
|
||||
Assessment::Positive => {
|
||||
"Drift is flowing naturally. Stability is increasing. No intervention needed."
|
||||
}
|
||||
Assessment::Neutral => "Drift is active but not concerning. Monitor for changes.",
|
||||
Assessment::Concerning => {
|
||||
"Drift is moving away from stability. Consider reviewing recent changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's assessment branch — `stability_ratio > 0.3 AND
|
||||
/// changes_per_day < 10` first, then `> 0.1`, else concerning.
|
||||
fn classify(stability_ratio: f64, changes_per_day: f64) -> Assessment {
|
||||
if stability_ratio > 0.3 && changes_per_day < VELOCITY_THRESHOLD as f64 {
|
||||
Assessment::Positive
|
||||
} else if stability_ratio > 0.1 {
|
||||
Assessment::Neutral
|
||||
} else {
|
||||
Assessment::Concerning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness over a JSON value (`not a.get("acknowledged")`).
|
||||
fn truthy(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
|
||||
Some(Value::String(s)) => !s.is_empty(),
|
||||
Some(Value::Array(a)) => !a.is_empty(),
|
||||
Some(Value::Object(o)) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// str(float) parity: serde_json's shortest round-trip float repr matches
|
||||
/// Python's (both print 12.0 as "12.0", 14.43 as "14.43").
|
||||
fn py_num(v: &Value) -> String {
|
||||
v.to_string()
|
||||
}
|
||||
|
||||
// ── alert actions ───────────────────────────────────────────────────────────
|
||||
|
||||
/// `add_alert` — stamp and append; persistence stays in the facade.
|
||||
pub fn add_alert(
|
||||
alerts: &[Value],
|
||||
alert_type: &str,
|
||||
message: &str,
|
||||
severity: &str,
|
||||
now: &str,
|
||||
) -> Vec<Value> {
|
||||
let mut out = alerts.to_vec();
|
||||
out.push(json!({
|
||||
"type": alert_type,
|
||||
"message": message,
|
||||
"severity": severity, // info, warning, critical
|
||||
"timestamp": now,
|
||||
"acknowledged": false,
|
||||
}));
|
||||
out
|
||||
}
|
||||
|
||||
/// `check_alerts` — the three threshold checks against velocity/direction
|
||||
/// dicts (computed by the facade via drift_core) and the MAX(last_washed)
|
||||
/// probe. Returns (alerts-with-appended, new_alerts-unstamped); the oracle
|
||||
/// stamps a COPY via add_alert, so the returned new_alerts carry no
|
||||
/// timestamp — the facade saves the appended list once.
|
||||
pub fn check_alerts(
|
||||
alerts: &[Value],
|
||||
velocity: &Value,
|
||||
direction: &Value,
|
||||
last_washed: Option<&str>,
|
||||
now: &str,
|
||||
) -> (Vec<Value>, Vec<Value>) {
|
||||
let mut raised: Vec<NewAlert> = Vec::new();
|
||||
|
||||
// Velocity
|
||||
if velocity.get("alert").and_then(Value::as_bool).unwrap_or(false) {
|
||||
raised.push(NewAlert {
|
||||
kind: AlertKind::HighVelocity,
|
||||
message: format!(
|
||||
"Drift velocity is {} changes/day (threshold: {})",
|
||||
py_num(velocity.get("changes_per_day").unwrap_or(&Value::Null)),
|
||||
VELOCITY_THRESHOLD
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Direction
|
||||
let ratio = direction
|
||||
.get("stability_ratio")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0);
|
||||
if ratio < NEGATIVE_DRIFT_THRESHOLD {
|
||||
raised.push(NewAlert {
|
||||
kind: AlertKind::NegativeDrift,
|
||||
message: format!(
|
||||
"Stability ratio is {:.2}% (threshold: {:.0}%)",
|
||||
ratio * 100.0,
|
||||
NEGATIVE_DRIFT_THRESHOLD * 100.0
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Promotion stall — the oracle's try/except skips unparseable or
|
||||
// timezone-naive MAX(last_washed) values (ValueError/TypeError paths).
|
||||
if let Some(last) = last_washed {
|
||||
if !last.is_empty() {
|
||||
if let Some(days_since) = days_since_now(last, now) {
|
||||
if days_since > PROMOTION_STALL_DAYS {
|
||||
raised.push(NewAlert {
|
||||
kind: AlertKind::PromotionStall,
|
||||
message: format!(
|
||||
"No promotions in {} days (threshold: {})",
|
||||
days_since, PROMOTION_STALL_DAYS
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let all = alerts
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(raised.iter().map(|a| a.stamped(now)))
|
||||
.collect();
|
||||
let new_alerts = raised.iter().map(NewAlert::unstamped).collect();
|
||||
(all, new_alerts)
|
||||
}
|
||||
|
||||
/// `run_retrospective` — assessment branch over the velocity/direction
|
||||
/// dicts plus the four facade-computed counts. Returns the report dict and
|
||||
/// the retrospective filename (the facade owns the file write). The wide
|
||||
/// parameter list mirrors the oracle's assembled inputs one-to-one.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_retrospective(
|
||||
new_entries: i64,
|
||||
washed_entries: i64,
|
||||
promoted_entries: i64,
|
||||
decayed_entries: i64,
|
||||
velocity: &Value,
|
||||
direction: &Value,
|
||||
days: i64,
|
||||
now: &str,
|
||||
now_local: &str,
|
||||
) -> Value {
|
||||
let ratio = direction
|
||||
.get("stability_ratio")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0);
|
||||
let changes_per_day = velocity
|
||||
.get("changes_per_day")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let assessment = Assessment::classify(ratio, changes_per_day);
|
||||
|
||||
let report = json!({
|
||||
"period_days": days,
|
||||
"new_entries": new_entries,
|
||||
"washed_entries": washed_entries,
|
||||
"promoted_entries": promoted_entries,
|
||||
"decayed_entries": decayed_entries,
|
||||
"velocity": velocity,
|
||||
"direction": direction,
|
||||
"assessment": assessment.as_str(),
|
||||
"summary": assessment.summary(),
|
||||
"timestamp": now,
|
||||
});
|
||||
json!({"report": report, "filename": local_stamp(now_local)})
|
||||
}
|
||||
|
||||
/// `get_unacknowledged_alerts` — keep alerts whose `acknowledged` is
|
||||
/// Python-falsy (missing key stays unacknowledged).
|
||||
pub fn get_unacknowledged_alerts(alerts: &[Value]) -> Vec<Value> {
|
||||
alerts
|
||||
.iter()
|
||||
.filter(|a| !truthy(a.get("acknowledged")))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `acknowledge_alert` — out-of-range index is a silent no-op (the oracle
|
||||
/// neither mutates nor saves); `changed` mirrors that bound.
|
||||
pub fn acknowledge_alert(alerts: &[Value], index: i64) -> (Vec<Value>, bool) {
|
||||
let mut out = alerts.to_vec();
|
||||
if index < 0 || index as usize >= out.len() {
|
||||
return (out, false);
|
||||
}
|
||||
out[index as usize]["acknowledged"] = Value::Bool(true);
|
||||
(out, true)
|
||||
}
|
||||
|
||||
// ── ISO-8601 arithmetic (Python datetime.timedelta.days semantics) ─────────
|
||||
|
||||
struct IsoInstant {
|
||||
/// Epoch microseconds of the wall-clock fields MINUS the UTC offset.
|
||||
micros: i64,
|
||||
/// True when the string carries an explicit UTC offset (oracle:
|
||||
/// aware datetime); naive values make the subtraction a TypeError.
|
||||
aware: bool,
|
||||
}
|
||||
|
||||
/// Strict ISO-8601 parse of the shapes `datetime.isoformat()` emits:
|
||||
/// YYYY-MM-DDTHH:MM:SS[.ffffff][Z|±HH:MM]. Anything else (including a
|
||||
/// missing offset) yields None — mirroring the oracle's try/except skip.
|
||||
fn parse_iso(s: &str) -> Option<IsoInstant> {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() < 19 {
|
||||
return None;
|
||||
}
|
||||
let num = |from: usize, to: usize| -> Option<i64> {
|
||||
s.get(from..to)?.parse::<i64>().ok()
|
||||
};
|
||||
let (y, mo, d) = (num(0, 4)?, num(5, 7)?, num(8, 10)?);
|
||||
let sep = bytes[10];
|
||||
if sep != b'T' && sep != b't' && sep != b' ' {
|
||||
return None;
|
||||
}
|
||||
let (h, mi, sec) = (num(11, 13)?, num(14, 16)?, num(17, 19)?);
|
||||
let mut rest = &s[19..];
|
||||
let mut micros = 0i64;
|
||||
if let Some(stripped) = rest.strip_prefix('.') {
|
||||
let digits: String = stripped.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut frac = digits.clone();
|
||||
while frac.len() < 6 {
|
||||
frac.push('0');
|
||||
}
|
||||
micros = frac[..6].parse::<i64>().ok()?;
|
||||
rest = &stripped[digits.len()..];
|
||||
}
|
||||
let (offset_secs, aware) = match rest {
|
||||
"" => (0, false),
|
||||
"Z" | "z" => (0, true),
|
||||
_ => {
|
||||
let sign = match rest.as_bytes()[0] {
|
||||
b'+' => 1,
|
||||
b'-' => -1,
|
||||
_ => return None,
|
||||
};
|
||||
let body = &rest[1..];
|
||||
let body = body.trim_end_matches(|c: char| c.is_whitespace());
|
||||
let (oh, om) = if body.len() == 5 && body.as_bytes()[2] == b':' {
|
||||
(body[0..2].parse::<i64>().ok()?, body[3..5].parse::<i64>().ok()?)
|
||||
} else if body.len() == 2 {
|
||||
(body[0..2].parse::<i64>().ok()?, 0)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
(sign * (oh * 3600 + om * 60), true)
|
||||
}
|
||||
};
|
||||
let day_secs = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + sec - offset_secs;
|
||||
Some(IsoInstant { micros: day_secs * 1_000_000 + micros, aware })
|
||||
}
|
||||
|
||||
/// Howard Hinnant's days_from_civil — days since 1970-01-01.
|
||||
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
|
||||
let y = if m <= 2 { y - 1 } else { y };
|
||||
let era = if y >= 0 { y } else { y - 399 } / 400;
|
||||
let yoe = y - era * 400;
|
||||
let mp = (m + 9) % 12;
|
||||
let doy = (153 * mp + 2) / 5 + d - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
era * 146097 + doe - 719468
|
||||
}
|
||||
|
||||
/// (aware now − aware last).days — timedelta.days FLOORS toward negative
|
||||
/// infinity, so div_euclid (not truncating division).
|
||||
fn days_since_now(last: &str, now: &str) -> Option<i64> {
|
||||
let a = parse_iso(last)?;
|
||||
let b = parse_iso(now)?;
|
||||
if !a.aware || !b.aware {
|
||||
return None;
|
||||
}
|
||||
Some((b.micros - a.micros).div_euclid(86_400_000_000))
|
||||
}
|
||||
|
||||
/// datetime.now().strftime("%Y%m%d_%H%M%S") over the naive local ISO
|
||||
/// string the facade passes.
|
||||
fn local_stamp(now_local: &str) -> String {
|
||||
let s = parse_iso(now_local);
|
||||
match s {
|
||||
Some(t) => {
|
||||
let total_secs = t.micros.div_euclid(1_000_000);
|
||||
let secs_of_day = total_secs.rem_euclid(86400);
|
||||
let days = total_secs.div_euclid(86400);
|
||||
let (y, m, d) = civil_from_days(days);
|
||||
format!(
|
||||
"{:04}{:02}{:02}_{:02}{:02}{:02}",
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
secs_of_day / 3600,
|
||||
(secs_of_day % 3600) / 60,
|
||||
secs_of_day % 60
|
||||
)
|
||||
}
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn civil_from_days(z: i64) -> (i64, i64, i64) {
|
||||
let z = z + 719468;
|
||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||
let doe = z - era * 146097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
(if m <= 2 { y + 1 } else { y }, m, d)
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────────
|
||||
|
||||
/// Typed FFI failure lattice behind the doc-envelope Result — the
|
||||
/// caller-visible `err` string is unchanged (enum only shapes the Rust
|
||||
/// side; parity is pinned by the battery).
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for FfiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FfiError::NullPtr => write!(f, "null argument pointer"),
|
||||
FfiError::Utf8(e) => write!(f, "{e}"),
|
||||
FfiError::Json(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
|
||||
if ptr.is_null() {
|
||||
return Err(FfiError::NullPtr);
|
||||
}
|
||||
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(FfiError::Utf8)?;
|
||||
serde_json::from_str(x).map_err(FfiError::Json)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e.to_string()}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn alerts_of(a: &Value) -> &[Value] {
|
||||
a.get("alerts")
|
||||
.and_then(Value::as_array)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// dob_add_alert({alerts, alert_type, message, severity, now}) → {alerts}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dob_add_alert(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
json!({"alerts": add_alert(
|
||||
alerts_of(&a),
|
||||
s("alert_type"),
|
||||
s("message"),
|
||||
s("severity"),
|
||||
s("now"),
|
||||
)})
|
||||
}))
|
||||
}
|
||||
|
||||
/// dob_check_alerts({alerts, velocity, direction, last_washed, now})
|
||||
/// → {new_alerts, alerts}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dob_check_alerts(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let empty = Value::Object(Map::new());
|
||||
let velocity = a.get("velocity").unwrap_or(&empty);
|
||||
let direction = a.get("direction").unwrap_or(&empty);
|
||||
let (all, new_alerts) = check_alerts(
|
||||
alerts_of(&a),
|
||||
velocity,
|
||||
direction,
|
||||
a.get("last_washed").and_then(Value::as_str),
|
||||
a.get("now").and_then(Value::as_str).unwrap_or(""),
|
||||
);
|
||||
json!({"new_alerts": new_alerts, "alerts": all})
|
||||
}))
|
||||
}
|
||||
|
||||
/// dob_run_retrospective({new_entries, washed_entries, promoted_entries,
|
||||
/// decayed_entries, velocity, direction, days, now, now_local})
|
||||
/// → {report, filename}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dob_run_retrospective(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let i = |k: &str| a.get(k).and_then(Value::as_i64).unwrap_or(0);
|
||||
let empty = Value::Object(Map::new());
|
||||
run_retrospective(
|
||||
i("new_entries"),
|
||||
i("washed_entries"),
|
||||
i("promoted_entries"),
|
||||
i("decayed_entries"),
|
||||
a.get("velocity").unwrap_or(&empty),
|
||||
a.get("direction").unwrap_or(&empty),
|
||||
i("days"),
|
||||
a.get("now").and_then(Value::as_str).unwrap_or(""),
|
||||
a.get("now_local").and_then(Value::as_str).unwrap_or(""),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// dob_get_unacknowledged_alerts({alerts}) → {unacked}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dob_get_unacknowledged_alerts(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({"unacked": get_unacknowledged_alerts(alerts_of(&a))})
|
||||
}))
|
||||
}
|
||||
|
||||
/// dob_acknowledge_alert({alerts, index}) → {alerts, changed}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dob_acknowledge_alert(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
// Python accepts bools as ints (True == 1) — mirror the coercion.
|
||||
let index = match a.get("index") {
|
||||
Some(Value::Bool(b)) => *b as i64,
|
||||
Some(v) => v.as_i64().unwrap_or(-1),
|
||||
None => -1,
|
||||
};
|
||||
let (alerts, changed) = acknowledge_alert(alerts_of(&a), index);
|
||||
json!({"alerts": alerts, "changed": changed})
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dashboard_shape() {
|
||||
let health = serde_json::json!({
|
||||
"authorities": {"spine": {"status": "CURRENT", "generation": 5}},
|
||||
"relationships": [{"authority": "spine", "status": "STALE"}]
|
||||
});
|
||||
let d = dashboard(&health);
|
||||
assert!(d.is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashboard_empty() {
|
||||
let d = dashboard(&serde_json::json!({}));
|
||||
assert!(d.is_object());
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,25 @@
|
||||
# Contract BUILDER-2026-10-11T16:08:13Z-2317ae-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port drift_observability.py to FULL Rust (dichotomy wave, #2479 tranche): pure parity kernel over the alert/retrospective state machine (add/check/unack/acknowledge/retrospective report), doc-envelope FFI + ctypes loader + facade delegation with RUNE_DO_CORE=python pin, frozen-truth battery, live parity 100%.
|
||||
|
||||
## What it connects to
|
||||
|
||||
- `src/rune/drift_observability.py`
|
||||
- `src/rust/drift_observability`
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [{"entity": "commit:819525c6", "note": "genuinely unexpected new surface \u2014 evidence required before store update", "status": "CANDIDATE"}, {"entity": "commit:efee6c57", "note": "genuinely unexpected new surface \u2014 evidence required before store update", "status": "CANDIDATE"}, {"entity": "truth:src/rust/drift_observability/src/frozen_do_truth.json", "note": "genuinely unexpected new surface \u2014 evidence required before store update", "status": "CANDIDATE"}], "captured_at": "2026-10-11T20:41:55Z", "contract": "BUILDER-2026-10-11T16:08:13Z-2317ae-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": ["commit:819525c6", "commit:efee6c57", "truth:src/rust/drift_observability/src/frozen_do_truth.json"], "missed": ["commit:819525c6", "commit:efee6c57", "truth:src/rust/drift_observability/src/frozen_do_truth.json"], "needless": ["module:deploy_qualify"], "observed": ["commit:819525c6", "commit:efee6c57", "truth:src/rust/drift_observability/src/frozen_do_truth.json"], "observed_existing": [], "observed_new": ["commit:819525c6", "commit:efee6c57", "truth:src/rust/drift_observability/src/frozen_do_truth.json"], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": ["src/rune/drift_observability.py", "src/rust/drift_observability/src/branch_battery.rs", "src/rust/drift_observability/src/frozen_do_truth.json", "src/rust/drift_observability/src/lib.rs"], "out_of_scope": ["scripts/gen_drift_observability_truth.py", "src/rune/do_loader.py"], "verdict": "red"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,25 @@
|
||||
# Contract BUILDER-2026-10-11T16:17:08Z-35a41f-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/attention_capsule.py to FULL Rust (dichotomy wave #2479 tranche): kernel parity port with doc-envelope FFI, ctypes loader mirroring broker_loader.py, facade delegation with RUNE_AC_CORE pin, frozen-truth branch battery.
|
||||
|
||||
## What it connects to
|
||||
|
||||
- `src/rune/attention_capsule.py`
|
||||
- `src/rust/attention_capsule`
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-11T18:08:32Z", "contract": "BUILDER-2026-10-11T16:17:08Z-35a41f-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": ["src/rune/attention_capsule.py", "src/rust/attention_capsule/src/branch_battery.rs", "src/rust/attention_capsule/src/frozen_ac_truth.json", "src/rust/attention_capsule/src/lib.rs"], "out_of_scope": ["scripts/gen_attention_capsule_truth.py", "src/rune/ac_loader.py"], "verdict": "red"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
Reference in new issue
Block a user