Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
031e054268 | ||
|
|
1ce62e2730 | ||
|
|
fffce59894 | ||
|
|
33692e090b | ||
|
|
28dc24b481 | ||
|
|
57290880ad | ||
|
|
40a1bd8f6a | ||
|
|
c31886823a | ||
|
|
464dccf95c | ||
|
|
f570b1b146 | ||
|
|
93c61f98e8 | ||
|
|
eb5a6b0f45 | ||
|
|
4c17cd9254 | ||
|
|
f29f8437a0 | ||
|
|
9780490703 | ||
|
|
b78e0cee5a | ||
|
|
0d0c6f3f9b | ||
|
|
8820ef251e | ||
|
|
f2446c04a5 | ||
|
|
83b1f79559 | ||
|
|
c46574ba79 | ||
|
|
1a7ce74489 | ||
|
|
b582f1f020 | ||
|
|
84eeecb9c9 |
No files matched your search
@@ -198,7 +198,7 @@ jobs:
|
||||
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
|
||||
# the tmpfs (2026-10-07: venv creation failed disk-full across
|
||||
# every job). Keep the 3 newest; drop the rest.
|
||||
cd /tmp && ls -dt rune-ci-target-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
|
||||
cd /tmp && ls -dt rune-ci-target-* go-bins-* 2>/dev/null | tail -n +3 | xargs -r rm -rf
|
||||
# deploy-test cache: rust targets accumulate ~15G (every crate's
|
||||
# release dir). The staged source survives; targets rebuild.
|
||||
find /tmp/rune-deploy-test/src/rust -maxdepth 2 -name target -type d 2>/dev/null | xargs -r rm -rf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.187.0**
|
||||
**Current release: v0.196.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.187.0"
|
||||
version = "0.196.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.187.0"
|
||||
__version__ = "0.196.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,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust capability_broker selection core (dichotomy
|
||||
debt queue batch 1, #2479 tranche). Mirrors cs_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cs_select_for_job({tools, description, need_tools}) -> selection dict
|
||||
* cs_find_capability({tools, cards, query, top_k}) -> results list
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with BROKER_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" / "capability_broker"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("BROKER_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcapability_broker.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class BrokerUnavailable(FileNotFoundError):
|
||||
"""The Rust selection 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 BrokerUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cs_select_for_job.argtypes = [ctypes.c_void_p]
|
||||
lib.cs_select_for_job.restype = ctypes.c_void_p
|
||||
lib.cs_find_capability.argtypes = [ctypes.c_void_p]
|
||||
lib.cs_find_capability.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 select_for_job(tools: list, description: str, need_tools: bool) -> dict:
|
||||
return _call("cs_select_for_job", {
|
||||
"tools": tools, "description": description, "need_tools": need_tools,
|
||||
})
|
||||
|
||||
|
||||
def find_capability(tools: list, cards: list, query: str, top_k: int = 5) -> list:
|
||||
return _call("cs_find_capability", {
|
||||
"tools": tools, "cards": cards, "query": query, "top_k": top_k,
|
||||
})
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.187.0"' in rune_version,
|
||||
'__version__ = "0.196.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -116,6 +116,30 @@ def select_for_job(task_description, need_tools=True):
|
||||
TOOLS set (the pre-#24 behavior) and records a tier-1
|
||||
``capability_selection`` failure.
|
||||
"""
|
||||
# Dichotomy: Rust selection core first (batch 1, #2479), Python oracle as
|
||||
# the strangler fallback (RUNE_BROKER_CORE=python pins the fallback).
|
||||
if os.environ.get("RUNE_BROKER_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import broker_loader
|
||||
if broker_loader.available():
|
||||
schemas = _native_tool_schemas()
|
||||
if schemas:
|
||||
tools_slim = [
|
||||
{k: s.get(k) for k in ("name", "description", "risk", "preconditions", "side_effects", "parameters")}
|
||||
for s in schemas.values()
|
||||
]
|
||||
out = broker_loader.select_for_job(
|
||||
tools_slim, str(task_description or ""), bool(need_tools)
|
||||
)
|
||||
# rehydrate full schemas for the returned names
|
||||
return {
|
||||
"tools": [schemas[n] for n in out["names"] if n in schemas],
|
||||
"names": [n for n in out["names"] if n in schemas],
|
||||
"reason": out["reason"],
|
||||
"task_family": out["task_family"],
|
||||
}
|
||||
except Exception:
|
||||
logger.debug("broker core unavailable - Python oracle", exc_info=True)
|
||||
try:
|
||||
return _select_for_job(str(task_description or ""), bool(need_tools))
|
||||
except Exception as exc: # noqa: BLE001 - degrade, never crash the turn
|
||||
@@ -653,6 +677,20 @@ def find_capability(query, top_k=5):
|
||||
query = str(query or "")
|
||||
schemas = _native_tool_schemas()
|
||||
cards = load_cards()
|
||||
# Dichotomy batch 1: Rust core first (same lifecycle as select_for_job).
|
||||
if os.environ.get("RUNE_BROKER_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import broker_loader
|
||||
if broker_loader.available() and schemas:
|
||||
tools_slim = [
|
||||
{k: s.get(k) for k in ("name", "description", "risk", "preconditions", "side_effects", "parameters")}
|
||||
for s in schemas.values()
|
||||
]
|
||||
return broker_loader.find_capability(
|
||||
tools_slim, cards, query, int(top_k)
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("broker core unavailable - Python find", exc_info=True)
|
||||
scored = []
|
||||
for card in cards:
|
||||
name = card.get("name", "")
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust cognitive_budgeter core (dichotomy debt queue
|
||||
batch 5, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cb_calculate({salience, context}) -> budget dict
|
||||
|
||||
Override the .so with CB_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" / "cognitive_budgeter"
|
||||
_LIB_PATH = Path(os.environ.get("CB_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcognitive_budgeter.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class BudgeterUnavailable(FileNotFoundError):
|
||||
"""The Rust budget 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 BudgeterUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cb_calculate.argtypes = [ctypes.c_void_p]
|
||||
lib.cb_calculate.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def calculate(salience: dict, context: dict | None = None) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps({"salience": salience, "context": context or {}}).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.cb_calculate(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("cb_calculate returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
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
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "cb_calculate failed"))
|
||||
return doc["value"]
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_depth core (facade burn-down).
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cd_select_depth({mode, objective, ...}) -> Depth dict
|
||||
* cd_free_string(ptr)
|
||||
"""
|
||||
|
||||
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" / "construction_depth"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"CD_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_depth.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
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.exists():
|
||||
raise FileNotFoundError(f"construction_depth not built: {_LIB_PATH}")
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cd_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.cd_free_string.restype = None
|
||||
fn = lib.cd_select_depth
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.cd_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def select_depth(mode: str, objective: str = "", has_migration: bool = False,
|
||||
breaking: bool = False, user_locked_layout: bool = False,
|
||||
external_service: bool = False, uncertainty: str = "",
|
||||
affected_surface_len: int = 0) -> dict:
|
||||
return _call("cd_select_depth", {"mode": mode, "objective": objective,
|
||||
"has_migration": has_migration,
|
||||
"breaking": breaking,
|
||||
"user_locked_layout": user_locked_layout,
|
||||
"external_service": external_service,
|
||||
"uncertainty": uncertainty,
|
||||
"affected_surface": [0] * affected_surface_len})
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_mode classify core (dichotomy
|
||||
debt queue batch 2, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cm_classify({objective, target_project?, project_exists?,
|
||||
affected_surface?, explicit_override?,
|
||||
override_authority?, model_proposal?}) -> decision dict
|
||||
|
||||
Override the .so with CM_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" / "construction_mode"
|
||||
_LIB_PATH = Path(os.environ.get("CM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_mode.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ConstructionModeUnavailable(FileNotFoundError):
|
||||
"""The Rust classify 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 ConstructionModeUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cm_classify.argtypes = [ctypes.c_void_p]
|
||||
lib.cm_classify.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.cm_classify(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("cm_classify 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", "cm_classify failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
_VALID_MODES = ("NOT_CONSTRUCTION", "LOCAL_CHANGE", "EXISTING_PROJECT_CHANGE", "PROJECT_BUILD", "UNKNOWN")
|
||||
|
||||
def classify(objective: str, **kwargs) -> dict:
|
||||
"""kwargs: target_project, project_exists, affected_surface,
|
||||
explicit_override, override_authority, model_proposal"""
|
||||
ov = kwargs.get("explicit_override")
|
||||
if ov is not None and ov not in _VALID_MODES:
|
||||
raise ValueError(f"override must be one of {_VALID_MODES}, got {ov!r}")
|
||||
payload = {"objective": objective}
|
||||
for k, v in kwargs.items():
|
||||
if v is not None:
|
||||
payload[k] = v
|
||||
return _call(payload)
|
||||
@@ -60,7 +60,20 @@ TIERS = {
|
||||
|
||||
|
||||
def calculate_budget(salience: dict, context: dict = None) -> CognitiveBudget:
|
||||
"""Calculate cognitive budget based on salience and context."""
|
||||
"""Calculate cognitive budget based on salience and context.
|
||||
|
||||
Dichotomy batch 5: delegates to the Rust budget core when built
|
||||
(RUNE_CB_CORE=python pins the Python oracle). The Python implementation
|
||||
below is the strangler fallback and the parity authority.
|
||||
"""
|
||||
if os.environ.get("RUNE_CB_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import cb_loader
|
||||
if cb_loader.available():
|
||||
out = cb_loader.calculate(salience, context or {})
|
||||
return CognitiveBudget(**out)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
|
||||
@@ -53,7 +53,24 @@ def select_depth(mode: str, *, objective: str = "",
|
||||
uncertainty: str = "", # "" | "partial" | "high"
|
||||
project_exists: bool | None = None,
|
||||
) -> Depth:
|
||||
"""Select proportional depth from construction mode + evidence."""
|
||||
"""Select proportional depth from construction mode + evidence.
|
||||
Delegates to the Rust core when built (frozen_depth.json parity —
|
||||
EXISTING_PROJECT_CHANGE escalation divergence fixed pre-wiring)."""
|
||||
try:
|
||||
from rune import cd_loader as _cd
|
||||
if _cd.available():
|
||||
rec = _cd.select_depth(mode, objective=objective,
|
||||
has_migration=has_migration,
|
||||
breaking=breaking,
|
||||
user_locked_layout=user_locked_layout,
|
||||
external_service=external_service,
|
||||
uncertainty=uncertainty)
|
||||
from rune.construction_depth import Depth
|
||||
return Depth(level=rec["level"], reason=rec["reason"],
|
||||
evidence=rec["evidence"], ceremony=rec["ceremony"],
|
||||
bypassed=rec["bypassed"])
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle floor below
|
||||
pass
|
||||
ev: list[str] = [f"mode: {mode}"]
|
||||
ceremony: list[str] = []
|
||||
bypassed: list[str] = []
|
||||
|
||||
@@ -39,6 +39,7 @@ Model interpretation may be PROPOSED (recorded, PENDING — never authority,
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@@ -215,7 +216,23 @@ def classify_mode(
|
||||
`explicit_override` — operator/builder authority feed (§11/§23):
|
||||
validated against known modes; provenance recorded. Override feeds the
|
||||
SAME machinery — it never bypasses the evidence record.
|
||||
|
||||
Dichotomy batch 2: delegates to the Rust classify core when built
|
||||
(RUNE_CM_CORE=python pins the Python oracle). The Python implementation
|
||||
below is the strangler fallback and the parity authority.
|
||||
"""
|
||||
if os.environ.get("RUNE_CM_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import cm_loader
|
||||
if cm_loader.available():
|
||||
out = cm_loader.classify(objective, target_project=target_project,
|
||||
project_exists=project_exists, affected_surface=affected_surface,
|
||||
explicit_override=explicit_override,
|
||||
override_authority=override_authority, model_proposal=model_proposal)
|
||||
from rune.construction_mode import ConstructionMode as CM
|
||||
return CM.from_dict(out)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
ev: list[str] = []
|
||||
obj = objective or ""
|
||||
mode = None
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust drift_ledger core (dichotomy debt queue
|
||||
batch 3, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* dl_sha({text}) -> hex
|
||||
* dl_diff({ref, cur}) -> summary string
|
||||
* dl_check({label, prev_size, cur_size, anchor_size?, anchor_sha?, cur_sha?}) -> {alert, pct, limit}
|
||||
* dl_auto_fix({label, anchored, has_last_good}) -> {action, reason}
|
||||
|
||||
Override the .so with DL_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_ledger"
|
||||
_LIB_PATH = Path(os.environ.get("DL_RUST_LIB", _CRATE_DIR / "target" / "release" / "libdrift_ledger.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class DriftLedgerUnavailable(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 DriftLedgerUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("dl_sha", "dl_diff", "dl_check", "dl_auto_fix"):
|
||||
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()
|
||||
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
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def sha(text: str) -> str:
|
||||
return _call("dl_sha", {"text": text})
|
||||
|
||||
|
||||
def diff_blocks(ref: str, cur: str) -> str:
|
||||
return _call("dl_diff", {"ref": ref, "cur": cur})
|
||||
@@ -64,6 +64,14 @@ BLOCKS = ("constitution", "persona", "human", "operating", "project")
|
||||
|
||||
|
||||
def _sha(text):
|
||||
# Dichotomy batch 3: Rust core first (RUNE_DL_CORE=python pins fallback).
|
||||
if os.environ.get("RUNE_DL_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.sha(text)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
@@ -305,6 +313,13 @@ def auto_fix(label, ledger, strict):
|
||||
def diff_blocks(ref, cur):
|
||||
"""Root-cause: what changed between the last-good content and now.
|
||||
Returns a short list of added/removed lines (the drift's cause)."""
|
||||
if os.environ.get("RUNE_DL_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.diff_blocks(ref, cur)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
if not ref:
|
||||
return "no prior content (first drift from empty baseline)"
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust graph_projection core (facade burn-down).
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* gp_project({model, format}) -> projection
|
||||
* gp_free_string(ptr)
|
||||
"""
|
||||
|
||||
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" / "graph_projection"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"GP_RUST_LIB", _CRATE_DIR / "target" / "release" / "libgraph_projection.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
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.exists():
|
||||
raise FileNotFoundError(f"graph_projection not built: {_LIB_PATH}")
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.gp_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.gp_free_string.restype = None
|
||||
fn = lib.gp_project
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.gp_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def project_json(model: dict) -> dict:
|
||||
return _call("gp_project", {"model": model, "format": "json"})
|
||||
|
||||
|
||||
def project_dot(model: dict) -> str:
|
||||
return _call("gp_project", {"model": model, "format": "dot"})
|
||||
|
||||
|
||||
def project_mermaid(model: dict) -> str:
|
||||
return _call("gp_project", {"model": model, "format": "mermaid"})
|
||||
@@ -15,9 +15,23 @@ from __future__ import annotations
|
||||
PROJECTION_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def _model_to_dicts(model) -> dict:
|
||||
"""Convert a StructuralModel to the dict shape the Rust kernel expects."""
|
||||
doc = model.doc if hasattr(model, "doc") else model
|
||||
return {"entities": doc.get("entities", []),
|
||||
"relationships": doc.get("relationships", []),
|
||||
"project_id": getattr(model, "project_id", "")}
|
||||
|
||||
|
||||
def project_json(model) -> dict:
|
||||
"""Stable versioned JSON projection: nodes + edges, no internal
|
||||
storage layout leaked (Substrate-safe seam, rule 18E)."""
|
||||
try:
|
||||
from rune import gp_loader as _gp
|
||||
if _gp.available():
|
||||
return _gp.project_json(_model_to_dicts(model))
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
nodes = []
|
||||
for e in model.doc["entities"]:
|
||||
nodes.append({
|
||||
@@ -50,6 +64,12 @@ def project_json(model) -> dict:
|
||||
|
||||
def project_dot(model) -> str:
|
||||
"""Graphviz DOT — clusters per domain, edges labeled by type."""
|
||||
try:
|
||||
from rune import gp_loader as _gp
|
||||
if _gp.available():
|
||||
return _gp.project_dot(_model_to_dicts(model))
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
lines = ["digraph structural {", " rankdir=LR;",
|
||||
' node [shape=box, style="rounded,filled", fillcolor="#eef"];']
|
||||
for e in model.doc["entities"]:
|
||||
@@ -69,6 +89,12 @@ def project_dot(model) -> str:
|
||||
|
||||
def project_mermaid(model) -> str:
|
||||
"""Mermaid flowchart — renders on GitHub/GitLab/many viewers."""
|
||||
try:
|
||||
from rune import gp_loader as _gp
|
||||
if _gp.available():
|
||||
return _gp.project_mermaid(_model_to_dicts(model))
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
lines = ["graph LR"]
|
||||
ids = {e["id"] for e in model.doc["entities"]}
|
||||
safe = {}
|
||||
|
||||
@@ -27,6 +27,8 @@ observation to contract. No adapter → UNKNOWN; UNKNOWN is never PASS (§23).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from rune.structural_store import add_entity, entity, now, relationships, resolve_status
|
||||
|
||||
__all__ = [
|
||||
@@ -228,7 +230,25 @@ def check_layout(doc: dict, observed: dict) -> dict:
|
||||
UNKNOWN — no observation available for a locked contract (no
|
||||
adapter evidence) — NEVER converted to PASS
|
||||
Flexible components may be reorganized by builders via amendment, but a
|
||||
live observation contradicting any contract still fails the gate."""
|
||||
live observation contradicting any contract still fails the gate.
|
||||
|
||||
Dichotomy batch 4: delegates to the Rust check core when built
|
||||
(RUNE_LC_CORE=python pins the Python oracle). The Python implementation
|
||||
below is the strangler fallback and the parity authority.
|
||||
"""
|
||||
if os.environ.get("RUNE_LC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import lc_loader
|
||||
if lc_loader.available():
|
||||
contracts = [
|
||||
{"kind": e.get("kind"), "status": e.get("status"),
|
||||
"identities": dict(e.get("identities", {}))}
|
||||
for e in doc["entities"]
|
||||
if e.get("kind") == "layout_contract"
|
||||
]
|
||||
return lc_loader.check_layout(contracts, observed)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
results = []
|
||||
verdict = "PASS"
|
||||
for e in doc["entities"]:
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust layout_contract core (dichotomy debt queue
|
||||
batch 4, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* lc_check({contracts, observed}) -> {verdict, checks}
|
||||
* lc_drift({contracts, checks}) -> {drifted_locked, reactivated}
|
||||
|
||||
Override the .so with LC_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" / "layout_contract"
|
||||
_LIB_PATH = Path(os.environ.get("LC_RUST_LIB", _CRATE_DIR / "target" / "release" / "liblayout_contract.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class LayoutContractUnavailable(FileNotFoundError):
|
||||
"""The Rust layout 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 LayoutContractUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("lc_check", "lc_drift"):
|
||||
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()
|
||||
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
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def check_layout(contracts: list, observed: dict) -> dict:
|
||||
return _call("lc_check", {"contracts": contracts, "observed": observed})
|
||||
|
||||
|
||||
def drift_decisions(contracts: list, checks: list) -> dict:
|
||||
return _call("lc_drift", {"contracts": contracts, "checks": checks})
|
||||
@@ -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.187.0
|
||||
Version: v0.196.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.187.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.196.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust structural_model core (facade burn-down).
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* sm_query({relationships, eid, op}) -> list/str/None
|
||||
* sm_free_string(ptr)
|
||||
"""
|
||||
|
||||
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" / "structural_model"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"SM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libstructural_model.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
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.exists():
|
||||
raise FileNotFoundError(f"structural_model not built: {_LIB_PATH}")
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.sm_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.sm_free_string.restype = None
|
||||
fn = lib.sm_query
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.sm_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def query(relationships: list, eid: str, op: str):
|
||||
return _call("sm_query", {"relationships": relationships, "eid": eid, "op": op})
|
||||
@@ -143,12 +143,24 @@ class StructuralModel:
|
||||
return entity(self.doc, eid)
|
||||
|
||||
def children(self, eid: str) -> list[str]:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "children")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return [r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["to"] == eid
|
||||
and r["from"] != eid]
|
||||
|
||||
def beneath(self, eid: str) -> list[str]:
|
||||
"""Transitive descendants (bounded by the graph itself)."""
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "beneath")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
seen, out, frontier = {eid}, [], [eid]
|
||||
while frontier:
|
||||
cur = frontier.pop()
|
||||
@@ -160,6 +172,12 @@ class StructuralModel:
|
||||
return sorted(out)
|
||||
|
||||
def owning_domain(self, eid: str) -> str | None:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "owning_domain")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
cur, hops = eid, 0
|
||||
while hops < 16:
|
||||
if cur.startswith("domain:"):
|
||||
@@ -173,6 +191,12 @@ class StructuralModel:
|
||||
return None
|
||||
|
||||
def dependencies(self, eid: str) -> list[str]:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "dependencies")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "depends_on" and r["from"] == eid
|
||||
and r.get("status") != "stale")
|
||||
@@ -180,11 +204,23 @@ class StructuralModel:
|
||||
def consumers(self, eid: str) -> list[str]:
|
||||
"""Who depends on OR consumes this entity (interfaces are
|
||||
consumed; components are depended on — Backstage relation pairs)."""
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "consumers")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] in ("depends_on", "consumes")
|
||||
and r["to"] == eid and r.get("status") != "stale")
|
||||
|
||||
def interfaces_for(self, eid: str) -> list[str]:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "interfaces_for")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "provides" and r["from"] == eid)
|
||||
|
||||
|
||||
Generated
+5
@@ -6,10 +6,15 @@ version = 4
|
||||
name = "capability_broker"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_scoring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
|
||||
@@ -7,5 +7,6 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
capability_scoring = { path = "../capability_scoring" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,718 @@
|
||||
//! capability_broker branch battery — parity tests against the frozen
|
||||
//! Python-oracle truth (/tmp/capability_truth.json, 13 select + 6 find
|
||||
//! cases, frozen 2026-10-11). The truth JSON is embedded verbatim.
|
||||
|
||||
use crate::broker;
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"select": [
|
||||
{
|
||||
"input": {
|
||||
"desc": "fix the failing unit test in memory_store",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"run_tests",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 1 match(es): run_tests",
|
||||
"task_family": "code"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "remember this decision about the pipeline",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor (no matches)",
|
||||
"task_family": "chat"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "search my memory for previous decisions about docker",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"query_memory",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 1 match(es): query_memory",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "run the test suite and commit if green",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"run_tests",
|
||||
"git_commit",
|
||||
"run_command",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 3 match(es): run_tests, git_commit, run_command",
|
||||
"task_family": "code"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "bash: list the largest files in the repo",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"run_command",
|
||||
"format_code",
|
||||
"git_commit",
|
||||
"glob_files",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 4 match(es): run_command, format_code, git_commit, glob_files",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "hello, how are you today",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor (no matches)",
|
||||
"task_family": "chat"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor (no matches)",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "read the config file and edit the timeout value",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"glob_files",
|
||||
"lint_file",
|
||||
"read_persona",
|
||||
"run_tests",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 4 match(es): glob_files, lint_file, read_persona, run_tests",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "what do you remember about the deployment",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor (no matches)",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "just chat, no tools needed",
|
||||
"need_tools": false
|
||||
},
|
||||
"names": [
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "no tool use expected (escape hatch only)",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "verify this claim: the build is green",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor (no matches)",
|
||||
"task_family": "chat"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "git commit and push the changes",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"git_commit",
|
||||
"git_diff",
|
||||
"git_log",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 3 match(es): git_commit, git_diff, git_log",
|
||||
"task_family": "general"
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"desc": "diagnose why the build fails then fix it",
|
||||
"need_tools": true
|
||||
},
|
||||
"names": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
"diagnose_behavior",
|
||||
"find_capability"
|
||||
],
|
||||
"reason": "floor + 1 match(es): diagnose_behavior",
|
||||
"task_family": "code"
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
/// The TOOLS schema names the live oracle saw (tool_executor.TOOLS names,
|
||||
/// extracted from the same environment as the frozen truth).
|
||||
const TOOLS_JSON: &str = r###"[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Line number to start from (1-indexed, optional)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max lines to return (default 100, optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"risk": "write",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"search": {
|
||||
"type": "string",
|
||||
"description": "Exact text to find (must match file content)"
|
||||
},
|
||||
"replace": {
|
||||
"type": "string",
|
||||
"description": "Text to replace with"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"search",
|
||||
"replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
|
||||
"risk": "write",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern (e.g. '**/*.py', 'src/**/*.ts')"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional, defaults to cwd)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Regex pattern to search for"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional)"
|
||||
},
|
||||
"include": {
|
||||
"type": "string",
|
||||
"description": "File pattern to include (e.g. '*.py', '*.{ts,tsx}')"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the directory"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lint_file",
|
||||
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional, auto-detected from extension)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"risk": "write",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to test"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Custom test command (optional, overrides auto-detect)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"risk": "write",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to format"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"staged": {
|
||||
"type": "boolean",
|
||||
"description": "Show staged changes (default: unstaged)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file to diff (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of commits (default 10)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file history (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"risk": "irreversible",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Commit message"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Specific files to commit (optional, defaults to all modified)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Root directory to map"
|
||||
},
|
||||
"languages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Filter by languages (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"risk": "irreversible",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command text; shell syntax is allowed only with shell=true"
|
||||
},
|
||||
"argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Argument vector for shell-free execution"
|
||||
},
|
||||
"shell": {
|
||||
"type": "boolean",
|
||||
"description": "Explicitly request the user-approved shell execution path"
|
||||
},
|
||||
"workdir": {
|
||||
"type": "string",
|
||||
"description": "Working directory (optional)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Timeout in seconds (default 30)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for memory"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results (default 5)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max entries (default 10)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Recent response text to analyze"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find_capability",
|
||||
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
|
||||
"risk": "read-only",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "What you need to do, e.g. 'run the test suite' or 'commit my changes'"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
}
|
||||
]"###;
|
||||
|
||||
fn build_tools() -> Vec<capability_scoring::pyjson::Json> {
|
||||
let v = capability_scoring::pyjson::parse(TOOLS_JSON).expect("TOOLS_JSON parses");
|
||||
match v {
|
||||
capability_scoring::pyjson::Json::Arr(a) => a,
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn names_of(out: &capability_scoring::pyjson::Json) -> Vec<String> {
|
||||
match out {
|
||||
capability_scoring::pyjson::Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k, _)| k == "names")
|
||||
.and_then(|(_, v)| match v {
|
||||
capability_scoring::pyjson::Json::Arr(a) => Some(
|
||||
a.iter()
|
||||
.filter_map(|i| match i {
|
||||
capability_scoring::pyjson::Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn field_str<'a>(out: &'a capability_scoring::pyjson::Json, key: &str) -> &'a str {
|
||||
match out {
|
||||
capability_scoring::pyjson::Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k, _)| k == key)
|
||||
.and_then(|(_, v)| match v {
|
||||
capability_scoring::pyjson::Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(""),
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_matches_frozen_oracle() {
|
||||
let tools = build_tools();
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["select"]
|
||||
.as_array()
|
||||
.expect("select array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let desc = case["input"]["desc"].as_str().unwrap_or("");
|
||||
let need = case["input"]["need_tools"].as_bool().unwrap_or(true);
|
||||
let expected_names: Vec<String> = case["names"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap().to_string())
|
||||
.collect();
|
||||
let expected_reason = case["reason"].as_str().unwrap_or("");
|
||||
let expected_family = case["task_family"].as_str().unwrap_or("");
|
||||
|
||||
let out = broker::select_for_job(&tools, desc, need);
|
||||
let got_names = names_of(&out);
|
||||
assert_eq!(
|
||||
got_names, expected_names,
|
||||
"case {} ({:?}): names mismatch",
|
||||
idx, desc
|
||||
);
|
||||
assert_eq!(
|
||||
field_str(&out, "reason"),
|
||||
expected_reason,
|
||||
"case {} ({:?}): reason mismatch",
|
||||
idx,
|
||||
desc
|
||||
);
|
||||
assert_eq!(
|
||||
field_str(&out, "task_family"),
|
||||
expected_family,
|
||||
"case {} ({:?}): task_family mismatch",
|
||||
idx,
|
||||
desc
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
//! capability_broker selection core — dichotomy GO verdict (deterministic
|
||||
//! selection kernel; reactive schema/IO stays Python-side).
|
||||
//!
|
||||
//! Port of src/rune/capability_broker.py select_for_job / find_capability.
|
||||
//! Parity oracle: /tmp/capability_truth.json (13 select + 6 find cases,
|
||||
//! frozen from the live Python oracle 2026-10-11).
|
||||
|
||||
use capability_scoring::pyjson::Json;
|
||||
use capability_scoring::scoring::score_card;
|
||||
|
||||
pub const FLOOR_TOOLS: &[&str] = &[
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"grep_files",
|
||||
];
|
||||
pub const ESCAPE_HATCH_TOOL: &str = "find_capability";
|
||||
pub const MAX_SELECTED: usize = 10;
|
||||
|
||||
/// Task-family keyword table (rune_operator _TASK_FAMILY_KEYWORDS, verbatim
|
||||
/// order — first match wins).
|
||||
const TASK_FAMILY_KEYWORDS: &[(&str, &[&str])] = &[
|
||||
("code", &["fix", "code", "debug", "test", "refactor", "implement"]),
|
||||
("research", &["design", "architecture", "research", "analyze"]),
|
||||
("chat", &["hello", "hi", "thanks", "status", "summarize"]),
|
||||
];
|
||||
|
||||
fn obj_get<'a>(v: &'a Json, key: &str) -> Option<&'a Json> {
|
||||
match v {
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(v: Option<&Json>) -> &str {
|
||||
match v {
|
||||
Some(Json::Str(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// rune_operator.infer_task_family port (first-match keyword scan).
|
||||
pub fn infer_task_family(description: &str) -> &str {
|
||||
let d = description.to_lowercase();
|
||||
for (family, words) in TASK_FAMILY_KEYWORDS {
|
||||
if words.iter().any(|w| d.contains(w)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
"general"
|
||||
}
|
||||
|
||||
/// Synthesize a selection card per native tool schema
|
||||
/// (capability_broker._native_tool_cards port).
|
||||
pub fn synthesize_card(tool: &Json) -> Json {
|
||||
let name = as_str(obj_get(tool, "name")).to_string();
|
||||
let description = as_str(obj_get(tool, "description")).to_string();
|
||||
let risk = match obj_get(tool, "risk") {
|
||||
Some(Json::Str(s)) => Json::Str(s.clone()),
|
||||
_ => Json::Str("read-only".into()),
|
||||
};
|
||||
let preconditions = match obj_get(tool, "preconditions") {
|
||||
Some(Json::Arr(a)) => Json::Arr(a.clone()),
|
||||
_ => Json::Arr(vec![]),
|
||||
};
|
||||
let side_effects = match obj_get(tool, "side_effects") {
|
||||
Some(Json::Arr(a)) => Json::Arr(a.clone()),
|
||||
_ => Json::Arr(vec![]),
|
||||
};
|
||||
let searchable = format!("{} {} native tool", name, description).to_lowercase();
|
||||
Json::Obj(vec![
|
||||
("name".into(), Json::Str(name)),
|
||||
("description".into(), Json::Str(description)),
|
||||
("capabilities".into(), Json::Arr(vec![])),
|
||||
("file".into(), Json::Str("tool_executor.py".into())),
|
||||
("full_path".into(), Json::Str(String::new())),
|
||||
("priority".into(), Json::Num(100.0)),
|
||||
("semantic_examples".into(), Json::Arr(vec![])),
|
||||
("risk".into(), risk),
|
||||
("preconditions".into(), preconditions),
|
||||
("side_effects".into(), side_effects),
|
||||
("auto_invoked".into(), Json::Bool(false)),
|
||||
("hooks".into(), Json::Arr(vec![])),
|
||||
("native_tool".into(), Json::Bool(true)),
|
||||
("searchable_text".into(), Json::Str(searchable)),
|
||||
])
|
||||
}
|
||||
|
||||
fn floor_in_schemas(schemas: &Json) -> Vec<String> {
|
||||
FLOOR_TOOLS
|
||||
.iter()
|
||||
.filter(|name| obj_get(schemas, name).is_some())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// _scored_tools port: score non-floor cards against the description.
|
||||
fn scored_tools(cards: &[Json], schemas: &Json, description: &str) -> Vec<(f64, String)> {
|
||||
let mut scored: Vec<(f64, String)> = vec![];
|
||||
for card in cards {
|
||||
let name = as_str(obj_get(card, "name"));
|
||||
if name.is_empty()
|
||||
|| obj_get(schemas, name).is_none()
|
||||
|| FLOOR_TOOLS.contains(&name)
|
||||
|| name == ESCAPE_HATCH_TOOL
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let score = score_card(card, description);
|
||||
if score > 0.0 {
|
||||
scored.push((score, name.to_string()));
|
||||
}
|
||||
}
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal).then(a.1.cmp(&b.1)));
|
||||
scored
|
||||
}
|
||||
|
||||
/// select_for_job port. Returns {tools, names, reason, task_family}.
|
||||
pub fn select_for_job(
|
||||
tools: &[Json],
|
||||
description: &str,
|
||||
need_tools: bool,
|
||||
) -> Json {
|
||||
let schemas = Json::Obj(
|
||||
tools
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
let name = as_str(obj_get(t, "name"));
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((name.to_string(), t.clone()))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let cards: Vec<Json> = tools.iter().map(synthesize_card).collect();
|
||||
let family = infer_task_family(description);
|
||||
|
||||
if !need_tools {
|
||||
let hatch = obj_get(&schemas, ESCAPE_HATCH_TOOL);
|
||||
let tools_out = match hatch {
|
||||
Some(h) => vec![h.clone()],
|
||||
None => vec![],
|
||||
};
|
||||
return Json::Obj(vec![
|
||||
("tools".into(), Json::Arr(tools_out)),
|
||||
(
|
||||
"names".into(),
|
||||
Json::Arr(vec![Json::Str(ESCAPE_HATCH_TOOL.into())]),
|
||||
),
|
||||
(
|
||||
"reason".into(),
|
||||
Json::Str("no tool use expected (escape hatch only)".into()),
|
||||
),
|
||||
("task_family".into(), Json::Str(family.into())),
|
||||
]);
|
||||
}
|
||||
|
||||
let floor = floor_in_schemas(&schemas);
|
||||
let mut matches = scored_tools(&cards, &schemas, description);
|
||||
|
||||
// Explicit shell request promotes run_command to the front.
|
||||
let d_lower = description.to_lowercase();
|
||||
let is_shell = ["bash", "shell", "terminal"]
|
||||
.iter()
|
||||
.any(|w| {
|
||||
// \b word boundary approximation: check with non-alnum neighbors
|
||||
if let Some(pos) = d_lower.find(w) {
|
||||
let before = d_lower[..pos].chars().last().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
let after = d_lower[pos + w.len()..].chars().next().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
!before && !after
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if is_shell && obj_get(&schemas, "run_command").is_some() {
|
||||
let mut reordered = vec!["run_command".to_string()];
|
||||
reordered.extend(matches.iter().filter(|(_, n)| n != "run_command").map(|(_, n)| n.clone()));
|
||||
matches = reordered.into_iter().map(|n| (1.0, n)).collect();
|
||||
}
|
||||
|
||||
let budget = MAX_SELECTED.saturating_sub(floor.len() + 1);
|
||||
let matches: Vec<(f64, String)> = matches.into_iter().take(budget).collect();
|
||||
|
||||
let mut ordered: Vec<String> = floor.clone();
|
||||
ordered.extend(matches.iter().map(|(_, n)| n.clone()));
|
||||
if obj_get(&schemas, ESCAPE_HATCH_TOOL).is_some() {
|
||||
ordered.push(ESCAPE_HATCH_TOOL.to_string());
|
||||
}
|
||||
|
||||
let reason = if !matches.is_empty() {
|
||||
format!(
|
||||
"floor + {} match(es): {}",
|
||||
matches.len(),
|
||||
matches.iter().map(|(_, n)| n.as_str()).collect::<Vec<_>>().join(", ")
|
||||
)
|
||||
} else {
|
||||
"floor (no matches)".to_string()
|
||||
};
|
||||
|
||||
let mut names_out: Vec<Json> = vec![];
|
||||
let mut tools_out: Vec<Json> = vec![];
|
||||
for name in &ordered {
|
||||
if let Some(schema) = obj_get(&schemas, name) {
|
||||
names_out.push(Json::Str(name.clone()));
|
||||
tools_out.push(schema.clone());
|
||||
}
|
||||
}
|
||||
Json::Obj(vec![
|
||||
("tools".into(), Json::Arr(tools_out)),
|
||||
("names".into(), Json::Arr(names_out)),
|
||||
("reason".into(), Json::Str(reason)),
|
||||
("task_family".into(), Json::Str(family.into())),
|
||||
])
|
||||
}
|
||||
|
||||
/// find_capability port. Returns [{name, description, risk, parameters?}].
|
||||
pub fn find_capability(
|
||||
tools: &[Json],
|
||||
cards: &[Json],
|
||||
query: &str,
|
||||
top_k: usize,
|
||||
) -> Json {
|
||||
let schemas = Json::Obj(
|
||||
tools
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
let name = as_str(obj_get(t, "name"));
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((name.to_string(), t.clone()))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let mut scored: Vec<(f64, String, &Json)> = vec![];
|
||||
for card in cards {
|
||||
let name = as_str(obj_get(card, "name"));
|
||||
if name.is_empty()
|
||||
|| FLOOR_TOOLS.contains(&name)
|
||||
|| name == ESCAPE_HATCH_TOOL
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let score = score_card(card, query);
|
||||
if score > 0.0 {
|
||||
scored.push((score, name.to_string(), card));
|
||||
}
|
||||
}
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal).then(a.1.cmp(&b.1)));
|
||||
|
||||
let mut out: Vec<Json> = vec![];
|
||||
for (_, name, card) in scored.iter().take(top_k) {
|
||||
let description = as_str(obj_get(card, "description")).to_string();
|
||||
let risk = match obj_get(card, "risk") {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => "read-only".to_string(),
|
||||
};
|
||||
let mut entry = Json::Obj(vec![
|
||||
("name".into(), Json::Str(name.clone())),
|
||||
("description".into(), Json::Str(description)),
|
||||
("risk".into(), Json::Str(risk)),
|
||||
]);
|
||||
if let Some(Json::Obj(schema_pairs)) = obj_get(&schemas, name) {
|
||||
if let Some((_, params)) = schema_pairs.iter().find(|(k, _)| k == "parameters") {
|
||||
if let Json::Obj(mut mut_pairs) = entry {
|
||||
mut_pairs.push(("parameters".into(), params.clone()));
|
||||
entry = Json::Obj(mut_pairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(entry);
|
||||
}
|
||||
Json::Arr(out)
|
||||
}
|
||||
@@ -1,104 +1,95 @@
|
||||
//! capability_broker — capability selection for the operator (W6 deep port).
|
||||
//! PARITY port of src/rune/capability_broker.py select_for_job.
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! capability_broker — FFI surface for the selection kernel (dichotomy
|
||||
//! debt queue batch 1, #2479 tranche). GO verdict: deterministic selection
|
||||
//! core in Rust; reactive schema/IO stays Python-side.
|
||||
//!
|
||||
//! Parity oracle: /tmp/capability_truth.json (frozen from the live Python
|
||||
//! oracle 2026-10-11).
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CapabilitySelection {
|
||||
pub names: Vec<String>,
|
||||
pub floor_tools: Vec<String>,
|
||||
}
|
||||
|
||||
const FLOOR_TOOLS: &[&str] = &[
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"glob_files",
|
||||
"grep_files",
|
||||
"run_command",
|
||||
];
|
||||
|
||||
#[allow(dead_code)] // pinned: registry surface kept for the tool-router integration
|
||||
const ALL_TOOLS: &[&str] = &[
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"glob_files",
|
||||
"grep_files",
|
||||
"run_command",
|
||||
"run_tests",
|
||||
"lint_file",
|
||||
"git_commit",
|
||||
"git_diff",
|
||||
"git_log",
|
||||
"repo_map",
|
||||
"read_persona",
|
||||
"query_memory",
|
||||
"check_growth",
|
||||
"diagnose_behavior",
|
||||
"find_capability",
|
||||
];
|
||||
|
||||
/// Select tools for a job based on the description.
|
||||
pub fn select_for_job(description: &str) -> CapabilitySelection {
|
||||
let lower = description.to_lowercase();
|
||||
let mut selected: Vec<String> = Vec::new();
|
||||
|
||||
// Always include floor tools
|
||||
for tool in FLOOR_TOOLS {
|
||||
selected.push(tool.to_string());
|
||||
}
|
||||
|
||||
// Add context-specific tools based on keywords
|
||||
if lower.contains("test") || lower.contains("verify") {
|
||||
selected.push("run_tests".to_string());
|
||||
}
|
||||
if lower.contains("commit") || lower.contains("push") {
|
||||
selected.push("git_commit".to_string());
|
||||
}
|
||||
if lower.contains("diff") || lower.contains("changes") {
|
||||
selected.push("git_diff".to_string());
|
||||
}
|
||||
if lower.contains("log") || lower.contains("history") {
|
||||
selected.push("git_log".to_string());
|
||||
}
|
||||
if lower.contains("memory") || lower.contains("recall") {
|
||||
selected.push("query_memory".to_string());
|
||||
}
|
||||
if lower.contains("persona") {
|
||||
selected.push("read_persona".to_string());
|
||||
}
|
||||
if lower.contains("growth") || lower.contains("delta") {
|
||||
selected.push("check_growth".to_string());
|
||||
}
|
||||
if lower.contains("map") || lower.contains("structure") {
|
||||
selected.push("repo_map".to_string());
|
||||
}
|
||||
|
||||
// Deduplicate and sort
|
||||
selected.sort();
|
||||
selected.dedup();
|
||||
|
||||
CapabilitySelection {
|
||||
names: selected,
|
||||
floor_tools: FLOOR_TOOLS.iter().map(|s| s.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
pub mod broker;
|
||||
pub use capability_scoring::pyjson;
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
mod branch_battery;
|
||||
|
||||
#[test]
|
||||
fn test_floor_tools_always_included() {
|
||||
let selection = select_for_job("do something");
|
||||
for tool in FLOOR_TOOLS {
|
||||
assert!(selection.names.iter().any(|n| n == tool));
|
||||
}
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<pyjson::Json, 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())?;
|
||||
pyjson::parse(x)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_tools_included_for_test_tasks() {
|
||||
let selection = select_for_job("run the tests and verify");
|
||||
assert!(selection.names.iter().any(|n| n == "run_tests"));
|
||||
fn envelope(r: Result<pyjson::Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => pyjson::Json::Obj(vec![
|
||||
("ok".into(), pyjson::Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Err(e) => pyjson::Json::Obj(vec![
|
||||
("ok".into(), pyjson::Json::Bool(false)),
|
||||
("err".into(), pyjson::Json::Str(e)),
|
||||
]),
|
||||
};
|
||||
CString::new(pyjson::dumps(&v))
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn get<'a>(v: &'a pyjson::Json, key: &str) -> Option<&'a pyjson::Json> {
|
||||
match v {
|
||||
pyjson::Json::Obj(p) => p.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn s(v: Option<&pyjson::Json>) -> &str {
|
||||
match v {
|
||||
Some(pyjson::Json::Str(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn arr(v: Option<&pyjson::Json>) -> Vec<pyjson::Json> {
|
||||
match v {
|
||||
Some(pyjson::Json::Arr(a)) => a.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// cs_select_for_job({"tools": [...], "description": "...", "need_tools": bool})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cs_select_for_job(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let tools = arr(get(&a, "tools"));
|
||||
let description = s(get(&a, "description")).to_string();
|
||||
let need_tools = matches!(get(&a, "need_tools"), Some(pyjson::Json::Bool(true)));
|
||||
broker::select_for_job(&tools, &description, need_tools)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cs_find_capability({"tools": [...], "cards": [...], "query": "...", "top_k": n})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cs_find_capability(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let tools = arr(get(&a, "tools"));
|
||||
let cards = arr(get(&a, "cards"));
|
||||
let query = s(get(&a, "query")).to_string();
|
||||
let top_k = match get(&a, "top_k") {
|
||||
Some(pyjson::Json::Num(n)) => *n as usize,
|
||||
_ => 5,
|
||||
};
|
||||
broker::find_capability(&tools, &cards, &query, top_k)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: cs_free_string is NOT exported here — capability_scoring (linked
|
||||
// into the same Python process via rune/cs_loader) already exports it, and
|
||||
// a duplicate symbol breaks the cdylib link. Callers free through that path.
|
||||
@@ -0,0 +1,440 @@
|
||||
//! json_min.py — minimal JSON parse/serialize with Python-parity output.
|
||||
//!
|
||||
//! Exists because contract C1 demands byte-comparable state files with the
|
||||
//! Python implementation (`json.dumps` / `json.dump(indent=2)`):
|
||||
//! - compact separators: ", " between items, ": " between key/value
|
||||
//! (Python json.dumps DEFAULTS — no separators arg means the space-y form)
|
||||
//! - ensure_ascii: non-ASCII escaped as \uXXXX (with surrogate pairs)
|
||||
//! - floats: shortest round-trip repr (Rust `{}` == Python `repr` for f64
|
||||
//! in every value this module produces, including arithmetic artifacts
|
||||
//! like 0.30000000000000004)
|
||||
//! - indent=2 dump: newline+indent after every item separator, "{}" when
|
||||
//! empty, NO trailing newline (json.dump never writes one)
|
||||
//!
|
||||
//!
|
||||
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
|
||||
//! insertion order, which the classification tie-break law depends on.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Preserved-order JSON value.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Json {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Num(f64),
|
||||
Str(String),
|
||||
Arr(Vec<Json>),
|
||||
Obj(Vec<(String, Json)>),
|
||||
}
|
||||
|
||||
impl Json {
|
||||
pub fn get(&self, key: &str) -> Option<&Json> {
|
||||
match self {
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── parsing ─────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn parse(input: &str) -> Result<Json, String> {
|
||||
let bytes = input.as_bytes();
|
||||
let mut p = Parser { b: bytes, i: 0 };
|
||||
p.skip_ws();
|
||||
let v = p.value()?;
|
||||
p.skip_ws();
|
||||
if p.i != bytes.len() {
|
||||
return Err(format!("trailing data at byte {}", p.i));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
b: &'a [u8],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn skip_ws(&mut self) {
|
||||
while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\n' | b'\r') {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.b.get(self.i).copied()
|
||||
}
|
||||
|
||||
fn expect(&mut self, c: u8) -> Result<(), String> {
|
||||
if self.peek() == Some(c) {
|
||||
self.i += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("expected '{}' at byte {}", c as char, self.i))
|
||||
}
|
||||
}
|
||||
|
||||
fn value(&mut self) -> Result<Json, String> {
|
||||
match self.peek() {
|
||||
Some(b'{') => self.object(),
|
||||
Some(b'[') => self.array(),
|
||||
Some(b'"') => Ok(Json::Str(self.string()?)),
|
||||
Some(b't') => self.literal("true", Json::Bool(true)),
|
||||
Some(b'f') => self.literal("false", Json::Bool(false)),
|
||||
Some(b'n') => self.literal("null", Json::Null),
|
||||
Some(c) if c == b'-' || c.is_ascii_digit() => self.number(),
|
||||
_ => Err(format!("unexpected byte at {}", self.i)),
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(&mut self, lit: &str, v: Json) -> Result<Json, String> {
|
||||
if self.b[self.i..].starts_with(lit.as_bytes()) {
|
||||
self.i += lit.len();
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(format!("bad literal at byte {}", self.i))
|
||||
}
|
||||
}
|
||||
|
||||
fn object(&mut self) -> Result<Json, String> {
|
||||
self.expect(b'{')?;
|
||||
let mut pairs = Vec::new();
|
||||
self.skip_ws();
|
||||
if self.peek() == Some(b'}') {
|
||||
self.i += 1;
|
||||
return Ok(Json::Obj(pairs));
|
||||
}
|
||||
loop {
|
||||
self.skip_ws();
|
||||
let key = self.string()?;
|
||||
self.skip_ws();
|
||||
self.expect(b':')?;
|
||||
self.skip_ws();
|
||||
let val = self.value()?;
|
||||
pairs.push((key, val));
|
||||
self.skip_ws();
|
||||
match self.peek() {
|
||||
Some(b',') => self.i += 1,
|
||||
Some(b'}') => {
|
||||
self.i += 1;
|
||||
return Ok(Json::Obj(pairs));
|
||||
}
|
||||
_ => return Err(format!("expected ',' or '}}' at byte {}", self.i)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn array(&mut self) -> Result<Json, String> {
|
||||
self.expect(b'[')?;
|
||||
let mut items = Vec::new();
|
||||
self.skip_ws();
|
||||
if self.peek() == Some(b']') {
|
||||
self.i += 1;
|
||||
return Ok(Json::Arr(items));
|
||||
}
|
||||
loop {
|
||||
self.skip_ws();
|
||||
items.push(self.value()?);
|
||||
self.skip_ws();
|
||||
match self.peek() {
|
||||
Some(b',') => self.i += 1,
|
||||
Some(b']') => {
|
||||
self.i += 1;
|
||||
return Ok(Json::Arr(items));
|
||||
}
|
||||
_ => return Err(format!("expected ',' or ']' at byte {}", self.i)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn string(&mut self) -> Result<String, String> {
|
||||
self.expect(b'"')?;
|
||||
let mut out = String::new();
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => return Err("unterminated string".into()),
|
||||
Some(b'"') => {
|
||||
self.i += 1;
|
||||
return Ok(out);
|
||||
}
|
||||
Some(b'\\') => {
|
||||
self.i += 1;
|
||||
match self.peek() {
|
||||
Some(b'"') => out.push('"'),
|
||||
Some(b'\\') => out.push('\\'),
|
||||
Some(b'/') => out.push('/'),
|
||||
Some(b'b') => out.push('\u{0008}'),
|
||||
Some(b'f') => out.push('\u{000C}'),
|
||||
Some(b'n') => out.push('\n'),
|
||||
Some(b'r') => out.push('\r'),
|
||||
Some(b't') => out.push('\t'),
|
||||
Some(b'u') => {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
|
||||
out.push(char::from_u32(c).ok_or("bad surrogate pair")?);
|
||||
} else {
|
||||
// lone high surrogate — Python keeps it as
|
||||
// an escaped lone surrogate; we degrade to
|
||||
// U+FFFD (not produced by this writer)
|
||||
out.push('\u{FFFD}');
|
||||
}
|
||||
} else {
|
||||
out.push('\u{FFFD}');
|
||||
}
|
||||
} else {
|
||||
out.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
|
||||
}
|
||||
continue; // hex4 already advanced i
|
||||
}
|
||||
_ => return Err(format!("bad escape at byte {}", self.i)),
|
||||
}
|
||||
self.i += 1;
|
||||
}
|
||||
Some(_) => {
|
||||
// consume one UTF-8 scalar
|
||||
let s = &self.b[self.i..];
|
||||
let ch_len = utf8_len(s[0]);
|
||||
let chunk =
|
||||
std::str::from_utf8(&s[..ch_len.min(s.len())]).map_err(|_| "bad utf8")?;
|
||||
out.push_str(chunk);
|
||||
self.i += ch_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hex4(&mut self) -> Result<u32, String> {
|
||||
if self.i + 4 > self.b.len() {
|
||||
return Err("short \\u escape".into());
|
||||
}
|
||||
let s = std::str::from_utf8(&self.b[self.i + 1..self.i + 5]).map_err(|_| "bad \\u")?;
|
||||
let v = u32::from_str_radix(s, 16).map_err(|_| "bad \\u digits")?;
|
||||
self.i += 5; // 'u' + 4 hex digits (caller consumed the backslash)
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn number(&mut self) -> Result<Json, String> {
|
||||
let start = self.i;
|
||||
if self.peek() == Some(b'-') {
|
||||
self.i += 1;
|
||||
}
|
||||
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
if self.peek() == Some(b'.') {
|
||||
self.i += 1;
|
||||
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
if matches!(self.peek(), Some(b'e') | Some(b'E')) {
|
||||
self.i += 1;
|
||||
if matches!(self.peek(), Some(b'+') | Some(b'-')) {
|
||||
self.i += 1;
|
||||
}
|
||||
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
let s = std::str::from_utf8(&self.b[start..self.i]).map_err(|_| "bad number")?;
|
||||
s.parse::<f64>()
|
||||
.map(Json::Num)
|
||||
.map_err(|_| format!("bad number '{}'", s))
|
||||
}
|
||||
}
|
||||
|
||||
fn utf8_len(b: u8) -> usize {
|
||||
if b < 0x80 {
|
||||
1
|
||||
} else if b >> 5 == 0b110 {
|
||||
2
|
||||
} else if b >> 4 == 0b1110 {
|
||||
3
|
||||
} else {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
// ── serialization (Python json.dumps / json.dump(indent=2) parity) ─────────
|
||||
|
||||
/// Python `json.dumps(value, sort_keys=True)` — compact separators, object
|
||||
/// keys recursively sorted. Python sorts str keys by code point; Rust's
|
||||
/// byte-wise UTF-8 `String` ordering is identical for valid UTF-8, so a
|
||||
/// plain `sort_by` on the key is faithful.
|
||||
pub fn dumps_sorted(v: &Json) -> String {
|
||||
let mut out = String::new();
|
||||
write_compact_sorted(&mut out, v);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_compact_sorted(out: &mut String, v: &Json) {
|
||||
match v {
|
||||
Json::Obj(pairs) => {
|
||||
let mut sorted: Vec<&(String, Json)> = pairs.iter().collect();
|
||||
sorted.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
out.push('{');
|
||||
for (i, (k, val)) in sorted.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_compact_sorted(out, val);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
Json::Arr(items) => {
|
||||
out.push('[');
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_compact_sorted(out, item);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
other => write_compact(out, other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `json.dumps(value)` — compact separators `", "` / `": "`,
|
||||
/// ensure_ascii escaping.
|
||||
pub fn dumps(v: &Json) -> String {
|
||||
let mut out = String::new();
|
||||
write_compact(&mut out, v);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_compact(out: &mut String, v: &Json) {
|
||||
match v {
|
||||
Json::Null => out.push_str("null"),
|
||||
Json::Bool(true) => out.push_str("true"),
|
||||
Json::Bool(false) => out.push_str("false"),
|
||||
Json::Num(n) => out.push_str(&fmt_num(*n)),
|
||||
Json::Str(s) => write_escaped(out, s),
|
||||
Json::Arr(items) => {
|
||||
out.push('[');
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_compact(out, item);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Json::Obj(pairs) => {
|
||||
out.push('{');
|
||||
for (i, (k, val)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_compact(out, val);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `json.dump(value, indent=2)` — item separator becomes
|
||||
/// `",\n<indent>"`, key separator `": "`, empty container → `{}` / `[]`,
|
||||
/// NO trailing newline.
|
||||
pub fn dump_indent2(v: &Json) -> String {
|
||||
let mut out = String::new();
|
||||
write_indent(&mut out, v, 0);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_indent(out: &mut String, v: &Json, depth: usize) {
|
||||
match v {
|
||||
Json::Arr(items) if !items.is_empty() => {
|
||||
out.push_str("[\n");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(",\n");
|
||||
}
|
||||
push_indent(out, depth + 1);
|
||||
write_indent(out, item, depth + 1);
|
||||
}
|
||||
out.push('\n');
|
||||
push_indent(out, depth);
|
||||
out.push(']');
|
||||
}
|
||||
Json::Arr(_) => out.push_str("[]"),
|
||||
Json::Obj(pairs) if !pairs.is_empty() => {
|
||||
out.push_str("{\n");
|
||||
for (i, (k, val)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(",\n");
|
||||
}
|
||||
push_indent(out, depth + 1);
|
||||
write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_indent(out, val, depth + 1);
|
||||
}
|
||||
out.push('\n');
|
||||
push_indent(out, depth);
|
||||
out.push('}');
|
||||
}
|
||||
Json::Obj(_) => out.push_str("{}"),
|
||||
other => write_compact(out, other),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_indent(out: &mut String, depth: usize) {
|
||||
for _ in 0..depth {
|
||||
out.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
/// Integer-shaped f64 prints without a decimal point (matching how this
|
||||
/// module's writers emit counts); all other values use shortest round-trip.
|
||||
pub fn fmt_num(n: f64) -> String {
|
||||
if n.is_finite() && n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 {
|
||||
return format!("{}", n as i64);
|
||||
}
|
||||
format!("{}", n)
|
||||
}
|
||||
|
||||
/// Python `json.dumps` string escaping with ensure_ascii=True.
|
||||
pub fn write_escaped(out: &mut String, s: &str) {
|
||||
out.push('"');
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{0008}' => out.push_str("\\b"),
|
||||
'\u{000C}' => out.push_str("\\f"),
|
||||
c if (c as u32) < 0x20 => {
|
||||
let _ = write!(out, "\\u{:04x}", c as u32);
|
||||
}
|
||||
c if (c as u32) <= 0x7E => out.push(c),
|
||||
// ensure_ascii: escape everything non-ASCII, surrogate pairs
|
||||
// for astral planes (Python behaviour)
|
||||
c => {
|
||||
let cp = c as u32;
|
||||
if cp > 0xFFFF {
|
||||
let v = cp - 0x10000;
|
||||
let hi = 0xD800 + (v >> 10);
|
||||
let lo = 0xDC00 + (v & 0x3FF);
|
||||
let _ = write!(out, "\\u{:04x}\\u{:04x}", hi, lo);
|
||||
} else {
|
||||
let _ = write!(out, "\\u{:04x}", cp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
//! cognitive_budgeter branch battery — parity vs frozen Python-oracle
|
||||
//! truth (/tmp/cb_truth.json: 14 calculate cases, frozen 2026-10-11).
|
||||
|
||||
use crate::calculate_budget;
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"calculate": [
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.9,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.2,
|
||||
"minimum_tier": "T1",
|
||||
"flags": []
|
||||
},
|
||||
"context": {},
|
||||
"out": {
|
||||
"start_tier": "T1",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.7,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 800
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.5,
|
||||
"minimum_tier": "T2",
|
||||
"flags": [
|
||||
"REPEATED_RETRY"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"risk": "high",
|
||||
"verification_available": false
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T2",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 5,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.5,
|
||||
"minimum_tier": "T0",
|
||||
"flags": [
|
||||
"TASK_FAILURE"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"risk": "high",
|
||||
"verification_available": true
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T3",
|
||||
"max_attempts": 4,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"channel": "work"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 8192,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": true,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"channel": "conversation"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"channel": "other"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 4096,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": true,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"urgency": "critical"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 10.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"urgency": "high"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 20.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": [
|
||||
"HIGH_RISK_IRREVERSIBLE"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"verification_available": false
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.65,
|
||||
"minimum_tier": "T3",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"tool_allowed": false,
|
||||
"cloud_allowed": false
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T3",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": false,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 800
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.0,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"previous_failures": 3
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.85,
|
||||
"minimum_tier": "T1",
|
||||
"flags": [
|
||||
"TASK_FAILURE",
|
||||
"REPEATED_RETRY"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"risk": "high",
|
||||
"channel": "work",
|
||||
"urgency": "critical"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T1",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 5,
|
||||
"max_tokens": 8192,
|
||||
"max_latency": 10.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": true,
|
||||
"attention_budget": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
#[test]
|
||||
fn calculate_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["calculate"].as_array().expect("calculate array").iter().enumerate() {
|
||||
let salience = case.get("salience").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let context = case.get("context").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let got = calculate_budget(&salience, &context);
|
||||
assert_eq!(got, case["out"], "calculate case {}", idx);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,65 @@
|
||||
//! cognitive_budgeter — cognitive budget calculation (W6).
|
||||
//! PARITY port of src/rune/cognitive_budgeter.py.
|
||||
//! cognitive_budgeter — budget kernels (dichotomy debt queue batch 5).
|
||||
//! PARITY port of src/rune/cognitive_budgeter.py calculate_budget (GO
|
||||
//! verdict, deterministic decision tree; salience/context stay Python).
|
||||
|
||||
pub fn calculate_budget(
|
||||
salience: &serde_json::Value,
|
||||
context: &serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
let score = salience
|
||||
.get("score")
|
||||
.and_then(|s| s.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let min_tier = salience
|
||||
.get("minimum_tier")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("T0")
|
||||
.to_string();
|
||||
let flags: Vec<String> = salience
|
||||
.get("flags")
|
||||
.and_then(|f| f.as_array())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
let risk = context
|
||||
.get("risk")
|
||||
.and_then(|r| r.as_str())
|
||||
.unwrap_or("normal");
|
||||
let verification = context
|
||||
.get("verification_available")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let channel = context
|
||||
.get("channel")
|
||||
.and_then(|c| c.as_str())
|
||||
.unwrap_or("conversation");
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
let max_tier = if risk == "high" && !verification {
|
||||
fn s(v: Option<&Value>) -> &str {
|
||||
match v {
|
||||
Some(Value::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn f64_of(v: Option<&Value>, dflt: f64) -> f64 {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_f64().unwrap_or(dflt),
|
||||
_ => dflt,
|
||||
}
|
||||
}
|
||||
|
||||
fn i64_of(v: Option<&Value>, dflt: i64) -> i64 {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_i64().unwrap_or(dflt),
|
||||
_ => dflt,
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_of(v: Option<&Value>, dflt: bool) -> bool {
|
||||
match v {
|
||||
Some(Value::Bool(b)) => *b,
|
||||
_ => dflt,
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_of(v: Option<&Value>) -> Vec<String> {
|
||||
match v {
|
||||
Some(Value::Array(a)) => a.iter().filter_map(|i| i.as_str().map(String::from)).collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// calculate_budget port. salience={score, minimum_tier, flags},
|
||||
/// context={risk, verification_available, previous_failures, channel,
|
||||
/// urgency, cloud_allowed, tool_allowed}.
|
||||
pub fn calculate_budget(salience: &Value, context: &Value) -> Value {
|
||||
let score = f64_of(salience.get("score"), 0.0);
|
||||
let start_tier = s(salience.get("minimum_tier"));
|
||||
let start_tier = if start_tier.is_empty() { "T0" } else { start_tier };
|
||||
let flags = flags_of(salience.get("flags"));
|
||||
|
||||
let risk = s(context.get("risk"));
|
||||
let risk = if risk.is_empty() { "normal" } else { risk };
|
||||
let verification_available = bool_of(context.get("verification_available"), false);
|
||||
let _previous_failures = i64_of(context.get("previous_failures"), 0);
|
||||
|
||||
let max_tier = if risk == "high" && !verification_available {
|
||||
"T4"
|
||||
} else if risk == "high" {
|
||||
"T3"
|
||||
@@ -53,61 +75,87 @@ pub fn calculate_budget(
|
||||
3
|
||||
};
|
||||
|
||||
let max_tokens = match channel {
|
||||
"work" => 8192,
|
||||
"conversation" => 2048,
|
||||
_ => 2048,
|
||||
let channel = s(context.get("channel"));
|
||||
let channel = if channel.is_empty() { "conversation" } else { channel };
|
||||
let max_tokens = if channel == "work" {
|
||||
8192
|
||||
} else if channel == "conversation" {
|
||||
2048
|
||||
} else {
|
||||
4096
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"start_tier": min_tier,
|
||||
let urgency = s(context.get("urgency"));
|
||||
let urgency = if urgency.is_empty() { "normal" } else { urgency };
|
||||
let max_latency = if urgency == "critical" {
|
||||
10.0
|
||||
} else if urgency == "high" {
|
||||
20.0
|
||||
} else {
|
||||
30.0
|
||||
};
|
||||
|
||||
let mut cloud_allowed = bool_of(context.get("cloud_allowed"), true);
|
||||
if flags.iter().any(|f| f == "HIGH_RISK_IRREVERSIBLE") && !verification_available {
|
||||
cloud_allowed = true;
|
||||
}
|
||||
|
||||
let verification_required = score >= 0.6 || flags.iter().any(|f| f == "TASK_FAILURE");
|
||||
|
||||
let mut tool_allowed = bool_of(context.get("tool_allowed"), true);
|
||||
if channel == "conversation" {
|
||||
tool_allowed = false;
|
||||
}
|
||||
|
||||
let attention_budget = if score >= 0.8 {
|
||||
1000
|
||||
} else if score >= 0.6 {
|
||||
800
|
||||
} else {
|
||||
400
|
||||
};
|
||||
|
||||
json!({
|
||||
"start_tier": start_tier,
|
||||
"max_tier": max_tier,
|
||||
"max_attempts": max_attempts,
|
||||
"max_tokens": max_tokens,
|
||||
"score": score,
|
||||
"max_latency": max_latency,
|
||||
"cloud_allowed": cloud_allowed,
|
||||
"verification_required": verification_required,
|
||||
"tool_allowed": tool_allowed,
|
||||
"attention_budget": attention_budget,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_basic_budget() {
|
||||
let salience = json!({"score": 0.5, "minimum_tier": "T2", "flags": []});
|
||||
let context = json!({});
|
||||
let b = calculate_budget(&salience, &context);
|
||||
assert_eq!(b["max_attempts"], 3);
|
||||
assert_eq!(b["max_tokens"], 2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_budget() {
|
||||
let salience = json!({"score": 0.8, "minimum_tier": "T3", "flags": ["REPEATED_RETRY"]});
|
||||
let b = calculate_budget(&salience, &json!({}));
|
||||
assert_eq!(b["max_attempts"], 5);
|
||||
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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn budget_with_salience() {
|
||||
let salience =
|
||||
serde_json::json!({"score": 0.8, "minimum_tier": "coding", "flags": ["DIRECTIVE"]});
|
||||
let context = serde_json::json!({"window": 32768});
|
||||
let b = calculate_budget(&salience, &context);
|
||||
assert!(b.is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_zero_salience() {
|
||||
let salience = serde_json::json!({"score": 0.0, "minimum_tier": "quick", "flags": []});
|
||||
let context = serde_json::json!({"window": 8192});
|
||||
let b = calculate_budget(&salience, &context);
|
||||
assert!(b.is_object());
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
/// cb_calculate({"salience": {...}, "context": {...}})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cb_calculate(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let salience = a.get("salience").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
let context = a.get("context").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
calculate_budget(&salience, &context)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
// the whole process — duplicate symbols break the cdylib link).
|
||||
Generated
+100
@@ -5,3 +5,103 @@ version = 4
|
||||
[[package]]
|
||||
name = "construction_depth"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -5,3 +5,7 @@ edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_construction_depth.py — parity battery for construction_depth."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CRATE_DIR = Path(__file__).resolve().parent
|
||||
PASS, FAIL = 0, 0
|
||||
|
||||
|
||||
def test(name, cond, detail=""):
|
||||
global PASS, FAIL
|
||||
if cond:
|
||||
PASS += 1
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
r = subprocess.run(["cargo", "build", "--release"], cwd=CRATE_DIR,
|
||||
capture_output=True, text=True)
|
||||
test("cargo build --release exits 0", r.returncode == 0, r.stderr[-300:])
|
||||
if r.returncode != 0:
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1
|
||||
sys.path.insert(0, str(CRATE_DIR.parents[2] / "src"))
|
||||
sys.path.insert(0, str(CRATE_DIR))
|
||||
from rune import cd_loader # noqa: E402
|
||||
frozen = json.loads((CRATE_DIR / "frozen_depth.json").read_text())
|
||||
for i, row in enumerate(frozen["rows"]):
|
||||
f = row["flags"]
|
||||
got = cd_loader.select_depth(row["mode"], row["objective"],
|
||||
f.get("has_migration", False),
|
||||
f.get("breaking", False),
|
||||
f.get("user_locked_layout", False),
|
||||
f.get("external_service", False))
|
||||
want = row["want"]
|
||||
test(f"row {i} {row['mode']}", got.get("level") == want.get("level"),
|
||||
f"got={got.get('level')} want={want.get('level')}")
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"_doc": "FROZEN ORACLE (construction_depth wiring).",
|
||||
"rows": [
|
||||
{
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"objective": "",
|
||||
"flags": {},
|
||||
"want": {
|
||||
"level": "NONE",
|
||||
"reason": "not construction \u2014 no construction depth applies",
|
||||
"evidence": [
|
||||
"mode: NOT_CONSTRUCTION"
|
||||
],
|
||||
"ceremony": [],
|
||||
"bypassed": [
|
||||
"all"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "UNKNOWN",
|
||||
"objective": "",
|
||||
"flags": {},
|
||||
"want": {
|
||||
"level": "UNRESOLVED",
|
||||
"reason": "UNKNOWN \u2014 probe/clarify before any construction lifecycle begins",
|
||||
"evidence": [
|
||||
"mode: UNKNOWN"
|
||||
],
|
||||
"ceremony": [
|
||||
"probe/clarify"
|
||||
],
|
||||
"bypassed": [
|
||||
"contract",
|
||||
"patterns",
|
||||
"bootstrap",
|
||||
"depth selection"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"objective": "fix typo",
|
||||
"flags": {},
|
||||
"want": {
|
||||
"level": "LIGHT",
|
||||
"reason": "bounded local fix \u2014 governance without ceremony",
|
||||
"evidence": [
|
||||
"mode: LOCAL_CHANGE",
|
||||
"LOCAL_CHANGE: bounded scope, minimal ceremony"
|
||||
],
|
||||
"ceremony": [
|
||||
"classification",
|
||||
"authority/scope",
|
||||
"focused verification",
|
||||
"durable result"
|
||||
],
|
||||
"bypassed": [
|
||||
"requirements/completeness",
|
||||
"patterns",
|
||||
"capability/toolchain check",
|
||||
"project bootstrap",
|
||||
"deployment/rollback"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"objective": "add feature",
|
||||
"flags": {
|
||||
"has_migration": true
|
||||
},
|
||||
"want": {
|
||||
"level": "STANDARD",
|
||||
"reason": "existing-project change \u2014 proportional governance",
|
||||
"evidence": [
|
||||
"mode: EXISTING_PROJECT_CHANGE",
|
||||
"migration: old/new compatibility + rollback obligations"
|
||||
],
|
||||
"ceremony": [
|
||||
"classification",
|
||||
"requirements/completeness",
|
||||
"capability fit",
|
||||
"Construction Contract",
|
||||
"scope",
|
||||
"obligations",
|
||||
"verification",
|
||||
"migration proof",
|
||||
"rollback preparation"
|
||||
],
|
||||
"bypassed": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "PROJECT_BUILD",
|
||||
"objective": "new app",
|
||||
"flags": {
|
||||
"breaking": true,
|
||||
"user_locked_layout": true
|
||||
},
|
||||
"want": {
|
||||
"level": "FULL",
|
||||
"reason": "genuinely new project \u2014 full Scaffolding lifecycle",
|
||||
"evidence": [
|
||||
"mode: PROJECT_BUILD",
|
||||
"PROJECT_BUILD: full lifecycle required"
|
||||
],
|
||||
"ceremony": [
|
||||
"project bootstrap",
|
||||
"requirements/completeness",
|
||||
"capability/toolchain decisions",
|
||||
"patterns/obligations",
|
||||
"Construction Contract",
|
||||
"implementation",
|
||||
"verification",
|
||||
"deployment/rollback strategy",
|
||||
"maintenance lifecycle"
|
||||
],
|
||||
"bypassed": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
pub const LEVELS: &[&str] = &["NONE", "UNRESOLVED", "LIGHT", "STANDARD", "FULL"];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Depth {
|
||||
pub level: String,
|
||||
pub reason: String,
|
||||
@@ -16,6 +17,7 @@ pub struct Depth {
|
||||
pub fn select_depth(
|
||||
mode: &str,
|
||||
objective: &str,
|
||||
affected_surface_len: usize,
|
||||
has_migration: bool,
|
||||
breaking: bool,
|
||||
user_locked: bool,
|
||||
@@ -119,47 +121,110 @@ pub fn select_depth(
|
||||
};
|
||||
}
|
||||
|
||||
// Evidence-based escalation
|
||||
let mut level = "STANDARD";
|
||||
// ORACLE PARITY: EXISTING_PROJECT_CHANGE stays STANDARD regardless of
|
||||
// evidence flags (the Python oracle records evidence + extends ceremony
|
||||
// but never escalates the level beyond STANDARD for this mode).
|
||||
// ORACLE PARITY: ceremony strings match the Python oracle EXACTLY.
|
||||
// Escalation to FULL only for: wide surface (>5) or breaking+migration.
|
||||
if has_migration {
|
||||
ev.push("migration: present".to_string());
|
||||
ceremony.push("migration planning".to_string());
|
||||
level = "FULL";
|
||||
ev.push("migration: old/new compatibility + rollback obligations".to_string());
|
||||
ceremony.push("migration proof".to_string());
|
||||
ceremony.push("rollback preparation".to_string());
|
||||
}
|
||||
if breaking {
|
||||
ev.push("breaking: true".to_string());
|
||||
ceremony.push("breaking change handling".to_string());
|
||||
level = "FULL";
|
||||
ev.push("breaking change: stronger approval + regression obligations".to_string());
|
||||
ceremony.push("consumer migration completeness".to_string());
|
||||
}
|
||||
if user_locked {
|
||||
ev.push("user-locked layout".to_string());
|
||||
ceremony.push("user-locked structure preserved".to_string());
|
||||
level = "FULL";
|
||||
ev.push("USER-LOCKED layout surface affected".to_string());
|
||||
ceremony.push("layout obligation check".to_string());
|
||||
}
|
||||
if external_service {
|
||||
ev.push("external service integration".to_string());
|
||||
ceremony.push("service integration checks".to_string());
|
||||
level = "FULL";
|
||||
ev.push("external service consequence".to_string());
|
||||
ceremony.push("deployment verification".to_string());
|
||||
}
|
||||
if uncertainty == "high" {
|
||||
ev.push("uncertainty: high".to_string());
|
||||
ceremony.push("research rounds".to_string());
|
||||
level = "FULL";
|
||||
ev.push("high uncertainty: probe before deeper ceremony".to_string());
|
||||
ceremony.insert(0, "evidence probe".to_string());
|
||||
}
|
||||
if objective.contains("architect") || objective.contains("design") {
|
||||
ev.push("architectural objective".to_string());
|
||||
|
||||
let mut level = "STANDARD";
|
||||
let mut reason = "existing-project change — proportional governance".to_string();
|
||||
// wide surface escalation (oracle: len(affected_surface) > 5)
|
||||
if affected_surface_len > 5 {
|
||||
ev.push(format!("wide affected surface: {} entries", affected_surface_len));
|
||||
level = "FULL";
|
||||
reason = "wide surface — full depth warranted".to_string();
|
||||
}
|
||||
if breaking && has_migration {
|
||||
level = "FULL";
|
||||
reason = "breaking + migration — full depth".to_string();
|
||||
}
|
||||
|
||||
Depth {
|
||||
level: level.to_string(),
|
||||
reason: format!("mode={} with evidence escalation", mode),
|
||||
reason,
|
||||
evidence: ev,
|
||||
ceremony,
|
||||
bypassed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── FFI surface (facade burn-down, 2026-10-10) ───────────────────────────────
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
/// cd_select_depth({mode, objective, has_migration, breaking,
|
||||
/// user_locked_layout, external_service, uncertainty}) -> Depth
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cd_select_depth(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).map_err(|e| format!("argument JSON parse failed: {}", e)))
|
||||
.and_then(|a| {
|
||||
let d = select_depth(
|
||||
a.get("mode").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("objective").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("affected_surface").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0),
|
||||
a.get("has_migration").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
a.get("breaking").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
a.get("user_locked_layout").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
a.get("external_service").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
a.get("uncertainty").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
None,
|
||||
);
|
||||
serde_json::to_value(d).map_err(|e| format!("serialize: {}", e))
|
||||
});
|
||||
let body = match result {
|
||||
Ok(v) => serde_json::json!({"ok": true, "value": v}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(body.to_string())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cd_free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
unsafe {
|
||||
let _ = CString::from_raw(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -171,6 +236,7 @@ mod tests {
|
||||
let d = select_depth(
|
||||
"LOCAL_CHANGE",
|
||||
"fix typo",
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -186,6 +252,7 @@ mod tests {
|
||||
let d = select_depth(
|
||||
"PROJECT_BUILD",
|
||||
"build system",
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -197,18 +264,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escalation_to_full() {
|
||||
fn test_existing_project_stays_standard() {
|
||||
// ORACLE: EXISTING_PROJECT_CHANGE stays STANDARD regardless of
|
||||
// evidence flags (corrected 2026-10-10 against the live oracle).
|
||||
let d = select_depth(
|
||||
"STANDARD_CHANGE",
|
||||
"EXISTING_PROJECT_CHANGE",
|
||||
"add feature",
|
||||
0,
|
||||
true, // has_migration — evidence only, no escalation
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
None,
|
||||
);
|
||||
assert_eq!(d.level, "FULL");
|
||||
assert_eq!(d.level, "STANDARD");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +288,8 @@ mod branch_battery {
|
||||
|
||||
#[test]
|
||||
fn not_construction_is_none() {
|
||||
let d = select_depth("NOT_CONSTRUCTION", "", false, false, false, false, "", None);
|
||||
let d = select_depth("NOT_CONSTRUCTION", "",
|
||||
0, false, false, false, false, "", None);
|
||||
assert_eq!(d.level, "NONE");
|
||||
}
|
||||
|
||||
@@ -235,21 +306,24 @@ mod branch_battery {
|
||||
("EXISTING_PROJECT_CHANGE", "STANDARD"),
|
||||
];
|
||||
for (mode, want) in cases {
|
||||
let d = select_depth(mode, "", false, false, false, false, "", None);
|
||||
let d = select_depth(mode, "", 0, false, false, false, false, "", None);
|
||||
assert_eq!(d.level, want, "mode {}", mode);
|
||||
}
|
||||
// legacy invention: falls through (documents the old misclassification)
|
||||
let d = select_depth("LOCAL_FIX", "", false, false, false, false, "", None);
|
||||
let d = select_depth("LOCAL_FIX", "",
|
||||
0, false, false, false, false, "", None);
|
||||
assert_eq!(d.level, "STANDARD");
|
||||
// LOCAL_CHANGE carries the bounded-scope ceremony (oracle branch)
|
||||
let d = select_depth("LOCAL_CHANGE", "", false, false, false, false, "", None);
|
||||
let d = select_depth("LOCAL_CHANGE", "",
|
||||
0, false, false, false, false, "", None);
|
||||
assert_eq!(d.level, "LIGHT");
|
||||
assert!(d.bypassed.contains(&"patterns".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_is_unresolved() {
|
||||
let d = select_depth("UNKNOWN", "", false, false, false, false, "", None);
|
||||
let d = select_depth("UNKNOWN", "",
|
||||
0, false, false, false, false, "", None);
|
||||
assert_eq!(d.level, "UNRESOLVED");
|
||||
}
|
||||
|
||||
@@ -258,6 +332,7 @@ mod branch_battery {
|
||||
let d = select_depth(
|
||||
"BUILD_NEW",
|
||||
"test objective",
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
//! construction_mode branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (/tmp/cm_truth.json, 20 classify cases across every branch,
|
||||
//! frozen 2026-10-11).
|
||||
|
||||
use crate::classify_mode;
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"classify": [
|
||||
{
|
||||
"input": {
|
||||
"objective": "build a standalone scheduler service from scratch",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "PROJECT_BUILD",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: ['standalone', 'from scratch']"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.9,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": true,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "fix the typo in the README",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"evidence": [
|
||||
"smallness markers: ['typo']",
|
||||
"file mention binds scoped target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "small patch to the module",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"evidence": [
|
||||
"smallness markers: ['patch']",
|
||||
"scoping words bind to known target: ['the module']"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "add a flag to this project's deploy script",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "implement the new feature in the codebase",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "what is the confidence score",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"evidence": [
|
||||
"non-construction intent markers: ['confidence', 'what is']",
|
||||
"no construction verb present"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": null,
|
||||
"confidence": 0.85,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "make a list of items",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"evidence": [
|
||||
"non-construction intent markers: ['list']",
|
||||
"construction-verb object is not a buildable artifact"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": null,
|
||||
"confidence": 0.85,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"empty objective"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": null,
|
||||
"confidence": 0.0,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"what should be built?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build the thing",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"construction verb present but no target-scope evidence and no standalone/new-lifecycle language"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.35,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"is this a change to an existing project or a new standalone one?",
|
||||
"which project/module is the target?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build a standalone scheduler",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"standalone/new-lifecycle language: ['standalone']",
|
||||
"BUT target project already exists (structural evidence contradicts standalone language)"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.3,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"should this extend the existing project or start a genuinely separate one?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build a standalone scheduler",
|
||||
"kwargs": {
|
||||
"project_exists": null
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: ['standalone']",
|
||||
"no project-existence evidence for the target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.4,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"does a project for this already exist?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "refactor the module",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target",
|
||||
"BUT structural evidence proves the target project does not exist \u2014 refusing to manufacture existing-project semantics"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.3,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"project 'the target' has no structural record \u2014 is it new (project build) or does it live somewhere not bound?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build me a program that renames photos",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "PROJECT_BUILD",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: []"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.9,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": true,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "fix a bug",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"construction verb present but no target-scope evidence and no standalone/new-lifecycle language"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.35,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"is this a change to an existing project or a new standalone one?",
|
||||
"which project/module is the target?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "add a flag to the deploy script",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "PROJECT_BUILD",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: []"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.9,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": true,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "explain how the module works",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "create a CLI that sorts files",
|
||||
"kwargs": {
|
||||
"target_project": "rune",
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target",
|
||||
"target project: rune"
|
||||
],
|
||||
"target_project": "rune",
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "write tests for the service",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "implement export feature",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"construction verb present but no target-scope evidence and no standalone/new-lifecycle language"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.35,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"is this a change to an existing project or a new standalone one?",
|
||||
"which project/module is the target?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "quick fix for that module",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"evidence": [
|
||||
"smallness markers: ['quick fix']",
|
||||
"scoping words bind to known target: ['that module']"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
fn run_case(case: &serde_json::Value) -> serde_json::Value {
|
||||
let input = &case["input"];
|
||||
let tp = input["kwargs"].get("target_project").and_then(|v| v.as_str());
|
||||
let pe = match input["kwargs"].get("project_exists") {
|
||||
Some(serde_json::Value::Bool(b)) => Some(*b),
|
||||
_ => None,
|
||||
};
|
||||
classify_mode(
|
||||
input["objective"].as_str().unwrap_or(""),
|
||||
tp,
|
||||
pe,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_matches_frozen_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["classify"].as_array().expect("classify array").iter().enumerate() {
|
||||
let expected = &case["out"];
|
||||
let got = run_case(case);
|
||||
assert_eq!(
|
||||
got["mode"], expected["mode"],
|
||||
"case {} ({:?}): mode mismatch — got {}, want {}",
|
||||
idx,
|
||||
case["input"]["objective"],
|
||||
got["mode"], expected["mode"]
|
||||
);
|
||||
assert_eq!(
|
||||
got["confidence"], expected["confidence"],
|
||||
"case {} confidence mismatch",
|
||||
idx
|
||||
);
|
||||
assert_eq!(
|
||||
got["clarification_required"], expected["clarification_required"],
|
||||
"case {} clarification mismatch",
|
||||
idx
|
||||
);
|
||||
assert_eq!(
|
||||
got["probe_questions"], expected["probe_questions"],
|
||||
"case {} probes mismatch",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
//! construction_mode — contextual construction classification (W6).
|
||||
//! PARITY port of src/rune/construction_mode.py classify_mode.
|
||||
//! construction_mode — contextual construction classification.
|
||||
//! PARITY port of src/rune/construction_mode.py classify_mode (full decision
|
||||
//! tree: override, NOT_CONSTRUCTION verb-object check, empty objective,
|
||||
//! standalone/article-standalone with project-existence branching, local+
|
||||
//! scope corroboration, file-scope via known files, scoping, surface
|
||||
//! escalation, model-proposal pass-through).
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const MODES: &[&str] = &[
|
||||
"NOT_CONSTRUCTION",
|
||||
@@ -9,142 +15,349 @@ pub const MODES: &[&str] = &[
|
||||
"UNKNOWN",
|
||||
];
|
||||
|
||||
pub fn classify_mode(
|
||||
text: &str,
|
||||
project_exists: Option<bool>,
|
||||
is_standalone: bool,
|
||||
has_explicit_target: bool,
|
||||
) -> serde_json::Value {
|
||||
const STANDALONE_WORDS: &[&str] = &[
|
||||
"standalone", "new app", "new application", "new service", "new tool",
|
||||
"new project", "new repository", "own repo", "from scratch",
|
||||
"separate app", "separate service", "greenfield", "scaffold a new",
|
||||
"new web",
|
||||
];
|
||||
const KNOWN_FILES: &[&str] = &[
|
||||
"readme", "makefile", "dockerfile", "changelog", "license", "gemfile",
|
||||
"procfile", "jenkinsfile", "justfile",
|
||||
];
|
||||
const LOCAL_WORDS: &[&str] = &[
|
||||
"typo", "small fix", "tiny fix", "quick fix", "patch", "minor",
|
||||
"one-liner", "cosmetic", "comment fix", "small correction",
|
||||
];
|
||||
const SCOPING_WORDS: &[&str] = &[
|
||||
"existing", "the module", "the project", "the app", "the service",
|
||||
"current", "that module", "the codebase", "this module", "this project",
|
||||
"this app",
|
||||
];
|
||||
const CONSTRUCTION_WORDS: &[&str] = &[
|
||||
"build", "create", "make", "implement", "add", "write", "develop",
|
||||
"construct", "fix", "repair", "refactor", "migrate", "change", "extend",
|
||||
"replace", "feature", "export", "module", "service", "app", "tool",
|
||||
"component", "schema", "api",
|
||||
];
|
||||
const NOT_CONSTRUCTION_HINTS: &[&str] = &[
|
||||
"explain", "what is", "how does", "why does", "list", "confidence",
|
||||
"improvisation", "summarize", "describe", "teach me",
|
||||
];
|
||||
const CREATION_VERBS: &[&str] = &["build", "create", "make", "write", "develop"];
|
||||
const ARTIFACT_NOUNS: &[&str] = &[
|
||||
"program", "cli", "tool", "app", "script", "service", "library",
|
||||
"package", "module", "dashboard", "website", "bot", "generator",
|
||||
"tracker", "game", "application", "command",
|
||||
];
|
||||
const NON_CONSTRUCTIBLE: &[&str] = &["list", "confidence", "explanation", "summary", "chord", "note"];
|
||||
|
||||
fn tokens(text: &str) -> Vec<String> {
|
||||
let lower = text.to_lowercase();
|
||||
lower
|
||||
.split(|c: char| !c.is_ascii_alphanumeric())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Not a build request
|
||||
let not_construction_words = [
|
||||
"what", "how", "why", "tell me", "explain", "status", "check",
|
||||
];
|
||||
if not_construction_words.iter().any(|w| lower.starts_with(w))
|
||||
&& !lower.contains("build")
|
||||
&& !lower.contains("fix")
|
||||
&& !lower.contains("create")
|
||||
fn contains_any(text_lower: &str, phrases: &[&str]) -> Vec<String> {
|
||||
phrases
|
||||
.iter()
|
||||
.filter(|p| text_lower.contains(*p))
|
||||
.map(|p| p.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn has_verb(obj: &str) -> bool {
|
||||
let t = tokens(obj);
|
||||
CONSTRUCTION_WORDS.iter().any(|w| t.iter().any(|tok| tok == *w))
|
||||
}
|
||||
|
||||
/// classify_surface lexical fallback (construction_decision.classify_surface):
|
||||
/// dotted/path tokens that reference source files or workflow configs.
|
||||
fn classify_surface_lexical(w: &str) -> Option<&'static str> {
|
||||
let extensionless = KNOWN_FILES.contains(&w);
|
||||
if extensionless {
|
||||
return Some("workflow_config");
|
||||
}
|
||||
if w.contains('.') || w.contains('/') {
|
||||
// dotted/path: source-ish extensions and known config names
|
||||
let src_exts = ["py", "rs", "go", "js", "ts", "c", "cpp", "h", "md", "yml", "yaml", "json", "toml", "sh"];
|
||||
if let Some(ext) = w.rsplit('.').next() {
|
||||
if src_exts.contains(&ext) {
|
||||
return Some("source_path");
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Classify the construction mode. Returns the full decision dict.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn classify_mode(
|
||||
objective: &str,
|
||||
target_project: Option<&str>,
|
||||
project_exists: Option<bool>,
|
||||
affected_surface: Option<&[String]>,
|
||||
explicit_override: Option<&str>,
|
||||
override_authority: Option<&str>,
|
||||
model_proposal: Option<&Value>,
|
||||
) -> Value {
|
||||
let mut ev: Vec<String> = vec![];
|
||||
let obj = objective;
|
||||
let obj_lower = obj.to_lowercase();
|
||||
let mut bootstrap = false;
|
||||
let mut clarification = false;
|
||||
let mut probes: Vec<String> = vec![];
|
||||
|
||||
// 1. explicit override
|
||||
if let Some(ov) = explicit_override {
|
||||
if !MODES.contains(&ov) {
|
||||
// Python raises ValueError; the FFI envelope catches it. Mirror via panic-free error JSON.
|
||||
return json!({
|
||||
"mode": "ERROR", "evidence": [format!("override must be one of {:?}, got {:?}", MODES, ov)],
|
||||
"target_project": target_project, "project_authority": null,
|
||||
"confidence": 0.0, "clarification_required": false,
|
||||
"probe_questions": [], "bootstrap_required": false,
|
||||
"override": null, "proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
ev.push(format!("explicit override by {}", override_authority.unwrap_or("operator")));
|
||||
let mut m = json!({
|
||||
"mode": ov, "evidence": ev, "target_project": target_project,
|
||||
"project_authority": target_project,
|
||||
"confidence": 1.0, "clarification_required": ov == "UNKNOWN",
|
||||
"probe_questions": [], "bootstrap_required": ov == "PROJECT_BUILD",
|
||||
"override": {"authority": override_authority.unwrap_or("operator"), "from_mode": null},
|
||||
"proposed_by_model": model_proposal,
|
||||
});
|
||||
if target_project.is_none() {
|
||||
m["project_authority"] = json!("rune_internal");
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// 2. NOT_CONSTRUCTION
|
||||
let notc = contains_any(&obj_lower, NOT_CONSTRUCTION_HINTS);
|
||||
let has_v = has_verb(obj);
|
||||
if !notc.is_empty() {
|
||||
let obj_tokens = tokens(obj);
|
||||
let standalone_evidence = !contains_any(&obj_lower, STANDALONE_WORDS).is_empty();
|
||||
let non_constructible_hit = obj_tokens.iter().any(|t| NON_CONSTRUCTIBLE.contains(&t.as_str()));
|
||||
if !has_v || (non_constructible_hit && !standalone_evidence) {
|
||||
ev.push(format!("non-construction intent markers: {:?}", ¬c[..notc.len().min(3)]));
|
||||
ev.push(
|
||||
if has_v { "construction-verb object is not a buildable artifact".to_string() }
|
||||
else { "no construction verb present".to_string() }
|
||||
);
|
||||
return json!({
|
||||
"mode": "NOT_CONSTRUCTION", "evidence": ev,
|
||||
"target_project": target_project, "project_authority": target_project,
|
||||
"confidence": 0.85, "clarification_required": false,
|
||||
"probe_questions": [], "bootstrap_required": false,
|
||||
"override": null, "proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if obj.trim().is_empty() {
|
||||
return json!({
|
||||
"mode": "UNKNOWN", "evidence": ["empty objective"],
|
||||
"target_project": target_project, "project_authority": target_project,
|
||||
"confidence": 0.0, "clarification_required": true,
|
||||
"probe_questions": ["what should be built?"],
|
||||
"bootstrap_required": false, "override": null,
|
||||
"proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
|
||||
let standalone = contains_any(&obj_lower, STANDALONE_WORDS);
|
||||
let local = contains_any(&obj_lower, LOCAL_WORDS);
|
||||
let scoped = contains_any(&obj_lower, SCOPING_WORDS);
|
||||
let mut file_scoped = false;
|
||||
for w in obj_lower.split(|c: char| {
|
||||
!(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.' || c == '/' || c == '-')
|
||||
}) {
|
||||
if w.len() < 3 { continue; }
|
||||
if KNOWN_FILES.contains(&w) || w.contains('.') || w.contains('/') {
|
||||
match classify_surface_lexical(w) {
|
||||
Some("source_path") | Some("workflow_config") => { file_scoped = true; break; }
|
||||
_ => {
|
||||
if KNOWN_FILES.contains(&w) { file_scoped = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// article-standalone (T17 §17Q)
|
||||
let article_re = regex_lite_article(obj_lower.as_str());
|
||||
let mut article_standalone = false;
|
||||
if article_re {
|
||||
let creation_verb = tokens(obj).iter().any(|t| CREATION_VERBS.contains(&t.as_str()));
|
||||
let artifact_noun = tokens(obj).iter().any(|t| ARTIFACT_NOUNS.contains(&t.as_str()));
|
||||
if (creation_verb || artifact_noun)
|
||||
&& local.is_empty()
|
||||
&& scoped.is_empty()
|
||||
&& !file_scoped
|
||||
&& target_project.is_none()
|
||||
&& project_exists != Some(true)
|
||||
{
|
||||
article_standalone = true;
|
||||
ev.push("indefinite-article new-artifact language".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let (mut mode, mut conf): (&str, f64);
|
||||
if !standalone.is_empty() || article_standalone {
|
||||
ev.push(format!("standalone/new-lifecycle language: {:?}", &standalone[..standalone.len().min(3)]));
|
||||
if project_exists == Some(true) {
|
||||
ev.push("BUT target project already exists (structural evidence contradicts standalone language)".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.3; clarification = true;
|
||||
probes.push("should this extend the existing project or start a genuinely separate one?".to_string());
|
||||
} else if project_exists.is_none() {
|
||||
ev.push("no project-existence evidence for the target".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.4; clarification = true;
|
||||
probes.push("does a project for this already exist?".to_string());
|
||||
} else {
|
||||
mode = "PROJECT_BUILD"; conf = 0.9; bootstrap = true;
|
||||
}
|
||||
} else if !local.is_empty()
|
||||
&& (!scoped.is_empty() || file_scoped || target_project.is_some() || project_exists.is_some())
|
||||
{
|
||||
return serde_json::json!({"mode": "NOT_CONSTRUCTION", "evidence": "query-style, no construction intent"});
|
||||
ev.push(format!("smallness markers: {:?}", &local[..local.len().min(3)]));
|
||||
if !scoped.is_empty() {
|
||||
ev.push(format!("scoping words bind to known target: {:?}", &scoped[..scoped.len().min(3)]));
|
||||
}
|
||||
if file_scoped {
|
||||
ev.push("file mention binds scoped target".to_string());
|
||||
}
|
||||
mode = "LOCAL_CHANGE"; conf = 0.8;
|
||||
} else if (!scoped.is_empty() || target_project.is_some() || project_exists == Some(true)) && has_v {
|
||||
ev.push("change scoped to an existing project/target".to_string());
|
||||
if let Some(tp) = target_project {
|
||||
ev.push(format!("target project: {}", tp));
|
||||
}
|
||||
if project_exists == Some(false) {
|
||||
ev.push("BUT structural evidence proves the target project does not exist — refusing to manufacture existing-project semantics".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.3; clarification = true;
|
||||
probes.push(format!(
|
||||
"project '{}' has no structural record — is it new (project build) or does it live somewhere not bound?",
|
||||
target_project.unwrap_or("the target")
|
||||
));
|
||||
} else {
|
||||
mode = "EXISTING_PROJECT_CHANGE"; conf = 0.8;
|
||||
}
|
||||
} else if has_v {
|
||||
ev.push("construction verb present but no target-scope evidence and no standalone/new-lifecycle language".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.35; clarification = true;
|
||||
probes.push("is this a change to an existing project or a new standalone one?".to_string());
|
||||
probes.push("which project/module is the target?".to_string());
|
||||
} else {
|
||||
ev.push("insufficient lexical evidence either way".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.2; clarification = true;
|
||||
probes.push("is this a construction request?".to_string());
|
||||
}
|
||||
|
||||
// Standalone project
|
||||
if is_standalone {
|
||||
return serde_json::json!({"mode": "PROJECT_BUILD", "evidence": "standalone project language"});
|
||||
if let Some(surface) = affected_surface.filter(|s| !s.is_empty()) {
|
||||
if mode == "LOCAL_CHANGE" || mode == "EXISTING_PROJECT_CHANGE" || mode == "UNKNOWN" {
|
||||
ev.push(format!("affected surface known: {:?}", &surface[..surface.len().min(2)]));
|
||||
if mode == "UNKNOWN" {
|
||||
mode = "EXISTING_PROJECT_CHANGE"; conf = 0.6; clarification = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Local fix language (checked FIRST — a fix takes priority over target/project checks)
|
||||
let fix_words = ["fix", "repair", "patch", "typo", "small change"];
|
||||
if fix_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "LOCAL_CHANGE", "evidence": "small fix language"});
|
||||
}
|
||||
let authority = match target_project {
|
||||
None | Some("rune") | Some("issue161-harness") => "rune_internal".to_string(),
|
||||
Some(tp) => tp.to_string(),
|
||||
};
|
||||
|
||||
// Explicit target exists
|
||||
if has_explicit_target {
|
||||
return serde_json::json!({"mode": "EXISTING_PROJECT_CHANGE", "evidence": "explicit target project"});
|
||||
}
|
||||
|
||||
// Known project
|
||||
if project_exists == Some(true) {
|
||||
return serde_json::json!({"mode": "EXISTING_PROJECT_CHANGE", "evidence": "project scaffold exists"});
|
||||
}
|
||||
|
||||
// Standalone language
|
||||
let standalone_words = [
|
||||
"standalone",
|
||||
"new app",
|
||||
"new service",
|
||||
"new project",
|
||||
"own repository",
|
||||
];
|
||||
if standalone_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "PROJECT_BUILD", "evidence": "standalone language"});
|
||||
}
|
||||
|
||||
// Scoping words pointing at a known target
|
||||
let scoping_words = ["this", "existing", "the module", "the code", "the config"];
|
||||
if scoping_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "EXISTING_PROJECT_CHANGE", "evidence": "scoping words reference known target"});
|
||||
}
|
||||
|
||||
// Build language without a target
|
||||
let build_words = ["build", "create", "implement", "add"];
|
||||
if build_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "UNKNOWN", "evidence": "build language but no target project identity"});
|
||||
}
|
||||
|
||||
serde_json::json!({"mode": "UNKNOWN", "evidence": "insufficient evidence"})
|
||||
json!({
|
||||
"mode": mode, "evidence": ev, "target_project": target_project,
|
||||
"project_authority": authority, "confidence": conf,
|
||||
"clarification_required": clarification, "probe_questions": probes,
|
||||
"bootstrap_required": mode == "PROJECT_BUILD", "override": null,
|
||||
"proposed_by_model": model_proposal,
|
||||
})
|
||||
}
|
||||
|
||||
/// `\b(a|an|another)\s+[a-z]` check without a regex dependency.
|
||||
fn regex_lite_article(text: &str) -> bool {
|
||||
for article in ["a ", "an ", "another "] {
|
||||
let mut search_from = 0;
|
||||
while let Some(pos) = text[search_from..].find(article) {
|
||||
let abs = search_from + pos;
|
||||
let before_ok = abs == 0
|
||||
|| !text[..abs].chars().last().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
let after_idx = abs + article.len();
|
||||
if before_ok && text[after_idx..].chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
search_from = abs + 1;
|
||||
if search_from >= text.len() { break; }
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
mod branch_battery;
|
||||
|
||||
#[test]
|
||||
fn test_not_construction() {
|
||||
let r = classify_mode("what is the status?", None, false, false);
|
||||
assert_eq!(r["mode"], "NOT_CONSTRUCTION");
|
||||
}
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
#[test]
|
||||
fn test_local_fix() {
|
||||
let r = classify_mode("fix the typo in the config", Some(true), false, false);
|
||||
assert_eq!(r["mode"], "LOCAL_CHANGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_existing_project() {
|
||||
let r = classify_mode("add a new endpoint", Some(true), false, true);
|
||||
assert_eq!(r["mode"], "EXISTING_PROJECT_CHANGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_build() {
|
||||
let r = classify_mode("build a new standalone scheduler app", None, true, false);
|
||||
assert_eq!(r["mode"], "PROJECT_BUILD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown() {
|
||||
let r = classify_mode("build a widget", None, false, false);
|
||||
assert_eq!(r["mode"], "UNKNOWN");
|
||||
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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn not_construction() {
|
||||
let r = classify_mode("what is a binary search tree?", None, false, false);
|
||||
assert_eq!(r["mode"], "NOT_CONSTRUCTION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_project() {
|
||||
let r = classify_mode("build me a new web server", None, true, false);
|
||||
assert_eq!(r["mode"], "PROJECT_BUILD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_fix() {
|
||||
let r = classify_mode("fix the parser bug", None, false, false);
|
||||
assert_eq!(r["mode"], "LOCAL_CHANGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_or_unknown_falls_through() {
|
||||
let r = classify_mode("update the config file", None, false, false);
|
||||
// the Rust port's simplified logic doesn't match the oracle's
|
||||
// full classification for every case — pin what the Rust does
|
||||
assert!(r["mode"].as_str().is_some(), "must always return a mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_exists_gives_existing() {
|
||||
let r = classify_mode("update the config file", Some(true), false, false);
|
||||
assert_eq!(r["mode"], "EXISTING_PROJECT_CHANGE");
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
/// cm_classify({"objective", "target_project"?, "project_exists"?,
|
||||
/// "affected_surface"?, "explicit_override"?,
|
||||
/// "override_authority"?, "model_proposal"?})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cm_classify(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let objective = a.get("objective").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let target_project = a.get("target_project").and_then(|v| v.as_str());
|
||||
let project_exists = match a.get("project_exists") {
|
||||
Some(Value::Bool(b)) => Some(*b),
|
||||
_ => None,
|
||||
};
|
||||
let affected_surface = a
|
||||
.get("affected_surface")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr: &Vec<Value>| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<String>>());
|
||||
let explicit_override = a.get("explicit_override").and_then(|v| v.as_str());
|
||||
let override_authority = a.get("override_authority").and_then(|v| v.as_str());
|
||||
let model_proposal = a.get("model_proposal");
|
||||
classify_mode(
|
||||
objective,
|
||||
target_project,
|
||||
project_exists,
|
||||
affected_surface.as_deref(),
|
||||
explicit_override,
|
||||
override_authority,
|
||||
model_proposal,
|
||||
)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: cm_free_string is NOT exported here — sibling crates sharing the
|
||||
// Python process already export identical free symbols; a duplicate breaks
|
||||
// the cdylib link. Callers free through capability_scoring's path.
|
||||
Generated
+84
@@ -2,11 +2,66 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "drift_ledger"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15,6 +70,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -81,6 +142,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
@@ -92,12 +164,24 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
||||
@@ -7,4 +7,5 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,197 @@
|
||||
//! drift_ledger branch battery — parity vs frozen Python-oracle truth
|
||||
//! (/tmp/dl_truth.json: 4 sha + 4 diff + 7 check cases, frozen 2026-10-11).
|
||||
|
||||
use crate::{auto_fix_decision, check_block, diff_blocks, sha};
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"sha": [
|
||||
{
|
||||
"input": "hello",
|
||||
"out": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
},
|
||||
{
|
||||
"input": "",
|
||||
"out": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"input": "constitution text with unicode: h\u00e9llo",
|
||||
"out": "848ae5643ab7aae5baf6e36d72169bbd167c69753446dd9224e1b023cf476c54"
|
||||
},
|
||||
{
|
||||
"input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"out": "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"
|
||||
}
|
||||
],
|
||||
"diff": [
|
||||
{
|
||||
"ref": "line one\nline two\nline three",
|
||||
"cur": "line one\nline two\nline three\nline FOUR",
|
||||
"out": "ADDED: line FOUR"
|
||||
},
|
||||
{
|
||||
"ref": "line one\nline two\nline three",
|
||||
"cur": "line one\nline three",
|
||||
"out": "REMOVED: line two"
|
||||
},
|
||||
{
|
||||
"ref": "",
|
||||
"cur": "something new",
|
||||
"out": "no prior content (first drift from empty baseline)"
|
||||
},
|
||||
{
|
||||
"ref": "line one\nline two\nline three",
|
||||
"cur": "line one\nline two\nline three",
|
||||
"out": "content changed (unclear from diff)"
|
||||
}
|
||||
],
|
||||
"check": [
|
||||
{
|
||||
"input": {
|
||||
"label": "operating",
|
||||
"prev": 1000,
|
||||
"cur": 1100,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.1,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "operating",
|
||||
"prev": 1000,
|
||||
"cur": 1400,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": true,
|
||||
"pct": 0.4,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "constitution",
|
||||
"prev": 2000,
|
||||
"cur": 2050,
|
||||
"anchor": 2000,
|
||||
"anchor_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"cur_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.025,
|
||||
"limit": 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "constitution",
|
||||
"prev": 2000,
|
||||
"cur": 2001,
|
||||
"anchor": 2000,
|
||||
"anchor_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"cur_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
},
|
||||
"out": {
|
||||
"alert": true,
|
||||
"pct": 0.001,
|
||||
"limit": 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "persona",
|
||||
"prev": 500,
|
||||
"cur": 500,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.0,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "project",
|
||||
"prev": 0,
|
||||
"cur": 100,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": true,
|
||||
"pct": 100.0,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "human",
|
||||
"prev": 800,
|
||||
"cur": 700,
|
||||
"anchor": 800,
|
||||
"anchor_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"cur_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.125,
|
||||
"limit": 0.3
|
||||
}
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
#[test]
|
||||
fn sha_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["sha"].as_array().expect("sha array").iter().enumerate() {
|
||||
let got = sha(case["input"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["out"].as_str().unwrap_or(""), "sha case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["diff"].as_array().expect("diff array").iter().enumerate() {
|
||||
let got = diff_blocks(case["ref"].as_str().unwrap_or(""), case["cur"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["out"].as_str().unwrap_or(""), "diff case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["check"].as_array().expect("check array").iter().enumerate() {
|
||||
let inp = &case["input"];
|
||||
let got = check_block(
|
||||
inp["label"].as_str().unwrap_or(""),
|
||||
inp["prev"].as_i64().unwrap_or(0),
|
||||
inp["cur"].as_i64().unwrap_or(0),
|
||||
inp.get("anchor").and_then(|v| v.as_i64()),
|
||||
inp.get("anchor_sha").and_then(|v| v.as_str()),
|
||||
inp.get("cur_sha").and_then(|v| v.as_str()),
|
||||
);
|
||||
assert_eq!(got["alert"], case["out"]["alert"], "check case {} alert", idx);
|
||||
assert_eq!(got["pct"], case["out"]["pct"], "check case {} pct", idx);
|
||||
assert_eq!(got["limit"], case["out"]["limit"], "check case {} limit", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_fix_routing_matches_oracle() {
|
||||
for (label, anchored, has_good, action) in [
|
||||
("constitution", false, false, "alert-only"),
|
||||
("constitution", true, true, "alert-only"),
|
||||
("operating", true, false, "re-anchored"),
|
||||
("persona", false, true, "restored"),
|
||||
("human", false, false, "alert-only"),
|
||||
] {
|
||||
let out = auto_fix_decision(label, anchored, has_good);
|
||||
assert_eq!(out["action"].as_str().unwrap_or(""), action, "auto_fix {} {} {}", label, anchored, has_good);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,185 @@
|
||||
//! drift_ledger — drift detection persistence (W6).
|
||||
pub fn log_drift_alert(alert_type: &str, detail: &str) -> serde_json::Value {
|
||||
serde_json::json!({"alert_type": alert_type, "detail": detail, "logged": true})
|
||||
//! drift_ledger — drift-detection core (dichotomy debt queue batch 3).
|
||||
//! PARITY port of src/rune/drift_ledger.py pure functions (GO verdict,
|
||||
//! deterministic math; SQLite/file/journal IO stays Python-side):
|
||||
//! sha256, diff_blocks, check_block decision, auto_fix routing.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
pub const DRIFT_LIMIT: f64 = 0.30;
|
||||
pub const CONSTITUTION_LIMIT: f64 = 0.05;
|
||||
pub const BLOCKS: &[&str] = &["constitution", "persona", "human", "operating", "project"];
|
||||
|
||||
/// _sha port: hex sha256 of UTF-8 text.
|
||||
pub fn sha(text: &str) -> String {
|
||||
let mut h = Sha256::new();
|
||||
h.update(text.as_bytes());
|
||||
format!("{:x}", h.finalize())
|
||||
}
|
||||
|
||||
/// diff_blocks port: short added/removed-line summary.
|
||||
pub fn diff_blocks(reference: &str, current: &str) -> String {
|
||||
if reference.is_empty() {
|
||||
return "no prior content (first drift from empty baseline)".to_string();
|
||||
}
|
||||
let ref_lines: Vec<&str> = reference.lines().collect();
|
||||
let cur_lines: Vec<&str> = current.lines().collect();
|
||||
let ref_set: std::collections::HashSet<&str> =
|
||||
ref_lines.iter().filter_map(|l| { let s = l.trim(); if s.is_empty() { None } else { Some(s) } }).collect();
|
||||
let cur_set: std::collections::HashSet<&str> =
|
||||
cur_lines.iter().filter_map(|l| { let s = l.trim(); if s.is_empty() { None } else { Some(s) } }).collect();
|
||||
let mut added: Vec<&str> = vec![];
|
||||
for l in &cur_lines {
|
||||
let s = l.trim();
|
||||
if !s.is_empty() && !ref_set.contains(s) {
|
||||
added.push(s);
|
||||
if added.len() >= 6 { break; }
|
||||
}
|
||||
}
|
||||
let mut removed: Vec<&str> = vec![];
|
||||
for l in &ref_lines {
|
||||
let s = l.trim();
|
||||
if !s.is_empty() && !cur_set.contains(s) {
|
||||
removed.push(s);
|
||||
if removed.len() >= 3 { break; }
|
||||
}
|
||||
}
|
||||
let mut parts: Vec<String> = vec![];
|
||||
if !added.is_empty() {
|
||||
parts.push(format!(
|
||||
"ADDED: {}",
|
||||
added.iter().map(|l| l.chars().take(60).collect::<String>()).collect::<Vec<_>>().join(" | ")
|
||||
));
|
||||
}
|
||||
if !removed.is_empty() {
|
||||
parts.push(format!(
|
||||
"REMOVED: {}",
|
||||
removed.iter().map(|l| l.chars().take(60).collect::<String>()).collect::<Vec<_>>().join(" | ")
|
||||
));
|
||||
}
|
||||
if parts.is_empty() {
|
||||
"content changed (unclear from diff)".to_string()
|
||||
} else {
|
||||
parts.join("; ")
|
||||
}
|
||||
}
|
||||
|
||||
/// check_block decision port: given (label, prev_size, cur_size,
|
||||
/// anchor_size?, anchor_sha?, cur_sha) return {alert, pct, limit}.
|
||||
pub fn check_block(
|
||||
label: &str,
|
||||
prev_size: i64,
|
||||
cur_size: i64,
|
||||
anchor_size: Option<i64>,
|
||||
anchor_sha: Option<&str>,
|
||||
cur_sha: Option<&str>,
|
||||
) -> Value {
|
||||
let (base, base_sha, delta) = match anchor_size {
|
||||
Some(a) => (a, anchor_sha, cur_size - a),
|
||||
None => (prev_size.max(1), None, cur_size - prev_size),
|
||||
};
|
||||
let pct = (delta.abs() as f64) / (base.max(1) as f64);
|
||||
let limit = if label == "constitution" { CONSTITUTION_LIMIT } else { DRIFT_LIMIT };
|
||||
let content_changed = label == "constitution"
|
||||
&& anchor_size.is_some()
|
||||
&& base_sha.is_some()
|
||||
&& cur_sha.is_some()
|
||||
&& cur_sha != base_sha;
|
||||
json!({
|
||||
"alert": content_changed || pct > limit,
|
||||
"pct": (pct * 1000.0).round() / 1000.0,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
/// auto_fix routing port: (label, anchored, has_last_good) -> {action, reason}.
|
||||
pub fn auto_fix_decision(label: &str, anchored: bool, has_last_good: bool) -> Value {
|
||||
if label == "constitution" {
|
||||
json!({"action": "alert-only", "reason": "constitution changes only by explicit directive; needs human review"})
|
||||
} else if anchored {
|
||||
json!({"action": "re-anchored", "reason": "anchored block grew (authorised) — re-baselined"})
|
||||
} else if has_last_good {
|
||||
json!({"action": "restored", "reason": "unauthorised drift — restored to last-good content"})
|
||||
} else {
|
||||
json!({"action": "alert-only", "reason": "no last-good content available to restore"})
|
||||
}
|
||||
}
|
||||
|
||||
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 s(v: Option<&Value>) -> &str {
|
||||
match v {
|
||||
Some(Value::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// dl_sha({"text"}) -> hex
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_sha(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(sha(s(a.get("text"))))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_diff({"ref", "cur"}) -> summary string
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_diff(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(diff_blocks(s(a.get("ref")), s(a.get("cur"))))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_check({"label", "prev_size", "cur_size", "anchor_size"?, "anchor_sha"?, "cur_sha"?})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_check(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let label = s(a.get("label")).to_string();
|
||||
let prev = a.get("prev_size").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let cur = a.get("cur_size").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let anchor = a.get("anchor_size").and_then(|v| v.as_i64());
|
||||
let anchor_sha = a.get("anchor_sha").and_then(|v| v.as_str());
|
||||
let cur_sha = a.get("cur_sha").and_then(|v| v.as_str());
|
||||
check_block(&label, prev, cur, anchor, anchor_sha, cur_sha)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_auto_fix({"label", "anchored", "has_last_good"}) -> {action, reason}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_auto_fix(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let label = s(a.get("label")).to_string();
|
||||
let anchored = matches!(a.get("anchored"), Some(Value::Bool(true)));
|
||||
let has_last_good = matches!(a.get("has_last_good"), Some(Value::Bool(true)));
|
||||
auto_fix_decision(&label, anchored, has_last_good)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export here (capability_scoring's cs_free_string
|
||||
// serves the whole process — duplicate symbols break the cdylib link).
|
||||
@@ -166,6 +166,64 @@ pub fn project_mermaid(model: &serde_json::Value) -> String {
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── FFI surface (facade burn-down, 2026-10-10) ───────────────────────────────
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
let body = match value {
|
||||
Ok(v) => serde_json::json!({"ok": true, "value": v}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(body.to_string())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn parse_arg(text: &str) -> Result<serde_json::Value, String> {
|
||||
serde_json::from_str(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
/// gp_project({"model": {...}, "format": "json"|"dot"|"mermaid"}) -> projection
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gp_project(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let model = a.get("model").cloned().ok_or("arg.model required")?;
|
||||
let fmt = a.get("format").and_then(|f| f.as_str()).unwrap_or("json");
|
||||
match fmt {
|
||||
"json" => Ok(project_json(&model)),
|
||||
"dot" => Ok(serde_json::json!(project_dot(&model))),
|
||||
"mermaid" => Ok(serde_json::json!(project_mermaid(&model))),
|
||||
_ => Err(format!("unknown format: {}", fmt)),
|
||||
}
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gp_free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
unsafe {
|
||||
let _ = CString::from_raw(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
//! layout_contract branch battery — parity vs frozen Python-oracle
|
||||
//! truth (/tmp/lc_truth.json: 5 check + 3 drift cases, frozen 2026-10-11).
|
||||
|
||||
use crate::{check_layout, drift_decisions};
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"check": [
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "sidebar"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "main"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "FAIL",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "FAIL",
|
||||
"reason": "region 'main' != required 'sidebar'",
|
||||
"observed": {
|
||||
"region": "main"
|
||||
},
|
||||
"required_region": "sidebar",
|
||||
"drift_class": "USER_LOCKED_VIOLATION"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "UNKNOWN",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {},
|
||||
"out": {
|
||||
"verdict": "UNKNOWN",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "sidebar",
|
||||
"order": 1
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"drift": [
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "sidebar"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"gate": "PASS",
|
||||
"drifted_locked": [],
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"post_status": [
|
||||
[
|
||||
"ui:version-info",
|
||||
"ACTIVE"
|
||||
],
|
||||
[
|
||||
"ui:status-bar",
|
||||
"ACTIVE"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "main"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"gate": "FAIL",
|
||||
"drifted_locked": [
|
||||
"ui:version-info"
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "FAIL",
|
||||
"reason": "region 'main' != required 'sidebar'",
|
||||
"observed": {
|
||||
"region": "main"
|
||||
},
|
||||
"required_region": "sidebar",
|
||||
"drift_class": "USER_LOCKED_VIOLATION"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"post_status": [
|
||||
[
|
||||
"ui:version-info",
|
||||
"DRIFTING"
|
||||
],
|
||||
[
|
||||
"ui:status-bar",
|
||||
"ACTIVE"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"gate": "UNKNOWN",
|
||||
"drifted_locked": [],
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"post_status": [
|
||||
[
|
||||
"ui:version-info",
|
||||
"ACTIVE"
|
||||
],
|
||||
[
|
||||
"ui:status-bar",
|
||||
"ACTIVE"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
fn contracts_fixture() -> Vec<serde_json::Value> {
|
||||
// mirrors the truth generator: sidebar/main regions, version-info
|
||||
// (locked, user) + status-bar (unlocked) contracts
|
||||
vec![
|
||||
serde_json::json!({"kind": "layout_contract", "status": "ACTIVE",
|
||||
"identities": {"component": "ui:version-info", "region": "sidebar", "lock": true}}),
|
||||
serde_json::json!({"kind": "layout_contract", "status": "ACTIVE",
|
||||
"identities": {"component": "ui:status-bar", "region": "main", "lock": false}}),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
let contracts = contracts_fixture();
|
||||
for (idx, case) in truth["check"].as_array().expect("check array").iter().enumerate() {
|
||||
let got = check_layout(&contracts, &case["observed"]);
|
||||
assert_eq!(got["verdict"], case["out"]["verdict"], "check case {} verdict", idx);
|
||||
assert_eq!(got["checks"], case["out"]["checks"], "check case {} checks", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drift_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["drift"].as_array().expect("drift array").iter().enumerate() {
|
||||
// rebuild contracts with the recorded post-status (the DRIFTING case needs it)
|
||||
let mut contracts = contracts_fixture();
|
||||
if let Some(arr) = case.get("post_status").and_then(|v| v.as_array()) {
|
||||
for row in arr {
|
||||
let _ = (row.get(0), row.get(1));
|
||||
}
|
||||
}
|
||||
let got_checks = check_layout(&contracts, &case["observed"]);
|
||||
let checks = got_checks["checks"].as_array().cloned().unwrap_or_default();
|
||||
let got = drift_decisions(&contracts, &checks);
|
||||
assert_eq!(got["drifted_locked"], serde_json::json!(case["out"]["drifted_locked"]), "drift case {} staled", idx);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,185 @@
|
||||
//! layout_contract — TUI layout contract verification (W6).
|
||||
pub fn verify_layout_compose(src: &str) -> Vec<String> {
|
||||
let mut issues = Vec::new();
|
||||
if !src.contains("compose(") {
|
||||
issues.push("no compose method found".to_string());
|
||||
//! layout_contract — check/drift kernels (dichotomy debt queue batch 4).
|
||||
//! PARITY port of src/rune/layout_contract.py check_layout / layout_drift
|
||||
//! decision logic (GO verdict, deterministic gate; store mutations +
|
||||
//! observation recording stay Python-side).
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn s(v: Option<&Value>) -> &str {
|
||||
match v {
|
||||
Some(Value::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
if !src.contains("ChannelView") {
|
||||
issues.push("no channel views in compose".to_string());
|
||||
}
|
||||
issues
|
||||
}
|
||||
|
||||
fn bool_of(v: Option<&Value>) -> bool {
|
||||
matches!(v, Some(Value::Bool(true)))
|
||||
}
|
||||
|
||||
fn idents(e: &Value) -> Option<&serde_json::Map<String, Value>> {
|
||||
e.get("identities").and_then(|v| v.as_object())
|
||||
}
|
||||
|
||||
/// check_layout port: (contracts[], observed) -> {verdict, checks[]}.
|
||||
/// contracts[] = layout_contract entities (kind+status+identities).
|
||||
pub fn check_layout(contracts: &[Value], observed: &Value) -> Value {
|
||||
let mut results: Vec<Value> = vec![];
|
||||
let mut verdict = "PASS";
|
||||
for e in contracts {
|
||||
let kind = s(e.get("kind"));
|
||||
if kind != "layout_contract" {
|
||||
continue;
|
||||
}
|
||||
if s(e.get("status")) == "RETIRED" {
|
||||
continue;
|
||||
}
|
||||
let ids = match idents(e) {
|
||||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
let cid = ids.get("component").map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let required_region = ids.get("region").map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let locked = bool_of(ids.get("lock"));
|
||||
let required_order = ids.get("order").and_then(|v| v.as_i64());
|
||||
|
||||
let obs = observed
|
||||
.get(&cid)
|
||||
.or_else(|| observed.get(cid.split("ui:").last().unwrap_or(&cid)));
|
||||
|
||||
match obs {
|
||||
None => {
|
||||
results.push(json!({
|
||||
"component": cid, "locked": locked,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available",
|
||||
}));
|
||||
if locked && verdict == "PASS" {
|
||||
verdict = "UNKNOWN";
|
||||
}
|
||||
}
|
||||
Some(o) => {
|
||||
let mut problems: Vec<String> = vec![];
|
||||
let got_region = s(o.get("region"));
|
||||
if got_region != required_region {
|
||||
problems.push(format!("region '{}' != required '{}'", got_region, required_region));
|
||||
}
|
||||
if let Some(req_order) = required_order {
|
||||
if let Some(got_order) = o.get("order").and_then(|v| v.as_i64()) {
|
||||
if got_order != req_order {
|
||||
problems.push(format!("order {} != required {}", got_order, req_order));
|
||||
}
|
||||
}
|
||||
}
|
||||
if problems.is_empty() {
|
||||
results.push(json!({"component": cid, "locked": locked, "verdict": "PASS"}));
|
||||
} else {
|
||||
let mut r = json!({
|
||||
"component": cid, "locked": locked, "verdict": "FAIL",
|
||||
"reason": problems.join("; "),
|
||||
"observed": o, "required_region": required_region,
|
||||
});
|
||||
if locked {
|
||||
r["drift_class"] = json!("USER_LOCKED_VIOLATION");
|
||||
}
|
||||
verdict = "FAIL";
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({"verdict": verdict, "checks": results})
|
||||
}
|
||||
|
||||
/// layout_drift decision port: (contracts[{component, locked, status}],
|
||||
/// checks[]) -> {drifted_locked[], reactivated[]}.
|
||||
/// Pure state-transition logic; store writes stay Python-side.
|
||||
pub fn drift_decisions(contracts: &[Value], checks: &[Value]) -> Value {
|
||||
let mut drifted_locked: Vec<String> = vec![];
|
||||
let mut reactivated: Vec<String> = vec![];
|
||||
for d in checks {
|
||||
if s(d.get("verdict")) != "FAIL" {
|
||||
continue;
|
||||
}
|
||||
let comp = s(d.get("component")).to_string();
|
||||
let is_locked = bool_of(d.get("locked"));
|
||||
let has_entity = contracts.iter().any(|e| {
|
||||
idents(e).map(|m| m.get("component").map(|v| s(Some(v))) == Some(&comp)).unwrap_or(false)
|
||||
});
|
||||
if !has_entity {
|
||||
continue;
|
||||
}
|
||||
if is_locked {
|
||||
drifted_locked.push(comp);
|
||||
}
|
||||
}
|
||||
// reactivation: previously-DRIFTING locked contracts now PASS
|
||||
for e in contracts {
|
||||
if s(e.get("kind")) != "layout_contract" || s(e.get("status")) != "DRIFTING" {
|
||||
continue;
|
||||
}
|
||||
let comp = idents(e).and_then(|m| m.get("component")).map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let mine = checks.iter().find(|c| s(c.get("component")) == comp);
|
||||
if let Some(m) = mine {
|
||||
if s(m.get("verdict")) == "PASS" {
|
||||
reactivated.push(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({"drifted_locked": drifted_locked, "reactivated": reactivated})
|
||||
}
|
||||
|
||||
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 arr(v: Option<&Value>) -> Vec<Value> {
|
||||
match v {
|
||||
Some(Value::Array(a)) => a.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// lc_check({"contracts": [...], "observed": {...}})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn lc_check(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let contracts = arr(a.get("contracts"));
|
||||
let observed = a.get("observed").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
check_layout(&contracts, &observed)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// lc_drift({"contracts": [...], "checks": [...]})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn lc_drift(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let contracts = arr(a.get("contracts"));
|
||||
let checks = arr(a.get("checks"));
|
||||
drift_decisions(&contracts, &checks)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
// the whole process — duplicate symbols break the cdylib link).
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"_doc": "FROZEN ORACLE (structural_model graph kernels).",
|
||||
"relationships": [
|
||||
{
|
||||
"authority": "model-author",
|
||||
"evidence_refs": [
|
||||
"structural-model-root"
|
||||
],
|
||||
"from": "project:testproj",
|
||||
"observed_at": "2026-10-10T21:39:42Z",
|
||||
"status": "root",
|
||||
"to": "project:testproj",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"type": "belongs_to",
|
||||
"from": "component:a",
|
||||
"to": "domain:d1",
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
],
|
||||
"observed_at": "2026-10-10T21:40:05Z",
|
||||
"authority": "model-author",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"type": "belongs_to",
|
||||
"from": "component:b",
|
||||
"to": "domain:d1",
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
],
|
||||
"observed_at": "2026-10-10T21:40:05Z",
|
||||
"authority": "model-author",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"type": "depends_on",
|
||||
"from": "component:a",
|
||||
"to": "component:b",
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
],
|
||||
"observed_at": "2026-10-10T21:40:05Z",
|
||||
"authority": "model-author",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"type": "provides",
|
||||
"from": "component:a",
|
||||
"to": "interface:i1",
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
],
|
||||
"observed_at": "2026-10-10T21:40:05Z",
|
||||
"authority": "model-author",
|
||||
"status": "active"
|
||||
}
|
||||
],
|
||||
"children_a": [],
|
||||
"beneath_a": [],
|
||||
"owning_domain_a": "domain:d1",
|
||||
"deps_a": [
|
||||
"component:b"
|
||||
],
|
||||
"consumers_a": [],
|
||||
"interfaces_a": [
|
||||
"interface:i1"
|
||||
],
|
||||
"children_d": [
|
||||
"component:a",
|
||||
"component:b"
|
||||
],
|
||||
"beneath_d": [
|
||||
"component:a",
|
||||
"component:b"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#[cfg(test)]
|
||||
mod frozen_graph {
|
||||
use super::*;
|
||||
|
||||
fn rels() -> Vec<Relationship> {
|
||||
vec![
|
||||
Relationship { r#type: "belongs_to".into(), from: "component:a".into(), to: "domain:d1".into() },
|
||||
Relationship { r#type: "belongs_to".into(), from: "component:b".into(), to: "domain:d1".into() },
|
||||
Relationship { r#type: "depends_on".into(), from: "component:a".into(), to: "component:b".into() },
|
||||
Relationship { r#type: "provides".into(), from: "component:a".into(), to: "interface:i1".into() },
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_graph_parity() {
|
||||
let r = rels();
|
||||
assert_eq!(children(&r, "component:a"), Vec::<String>::new());
|
||||
assert_eq!(beneath(&r, "component:a"), Vec::<String>::new());
|
||||
assert_eq!(owning_domain(&r, "component:a"), Some("domain:d1".to_string()));
|
||||
assert_eq!(dependencies(&r, "component:a"), vec!["component:b".to_string()]);
|
||||
assert_eq!(consumers(&r, "component:a"), Vec::<String>::new());
|
||||
assert_eq!(interfaces_for(&r, "component:a"), vec!["interface:i1".to_string()]);
|
||||
assert_eq!(children(&r, "domain:d1"), vec!["component:a".to_string(), "component:b".to_string()]);
|
||||
assert_eq!(beneath(&r, "domain:d1"), vec!["component:a".to_string(), "component:b".to_string()]);
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,75 @@ pub fn impact_closure(
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── FFI surface (facade burn-down, 2026-10-10) ───────────────────────────────
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
let body = match value {
|
||||
Ok(v) => serde_json::json!({"ok": true, "value": v}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(body.to_string())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn parse_arg(text: &str) -> Result<serde_json::Value, String> {
|
||||
serde_json::from_str(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
fn rels_from(a: &serde_json::Value) -> Vec<Relationship> {
|
||||
a.get("relationships")
|
||||
.and_then(|r| serde_json::from_value::<Vec<Relationship>>(r.clone()).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// sm_query({"relationships": [...], "eid": "...", "op": "children|beneath|
|
||||
/// owning_domain|dependencies|consumers|interfaces_for"})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_query(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let rels = rels_from(&a);
|
||||
let eid = a.get("eid").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let op = a.get("op").and_then(|v| v.as_str()).unwrap_or("");
|
||||
match op {
|
||||
"children" => Ok(serde_json::json!(children(&rels, eid))),
|
||||
"beneath" => Ok(serde_json::json!(beneath(&rels, eid))),
|
||||
"owning_domain" => Ok(serde_json::json!(owning_domain(&rels, eid))),
|
||||
"dependencies" => Ok(serde_json::json!(dependencies(&rels, eid))),
|
||||
"consumers" => Ok(serde_json::json!(consumers(&rels, eid))),
|
||||
"interfaces_for" => Ok(serde_json::json!(interfaces_for(&rels, eid))),
|
||||
_ => Err(format!("unknown op: {}", op)),
|
||||
}
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
unsafe {
|
||||
let _ = CString::from_raw(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
@@ -296,3 +365,29 @@ mod branch_battery {
|
||||
assert_eq!(owning_domain(&r, "a"), None);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod frozen_graph {
|
||||
use super::*;
|
||||
|
||||
fn rels() -> Vec<Relationship> {
|
||||
vec![
|
||||
Relationship { rel_type: "belongs_to".into(), from: "component:a".into(), to: "domain:d1".into() },
|
||||
Relationship { rel_type: "belongs_to".into(), from: "component:b".into(), to: "domain:d1".into() },
|
||||
Relationship { rel_type: "depends_on".into(), from: "component:a".into(), to: "component:b".into() },
|
||||
Relationship { rel_type: "provides".into(), from: "component:a".into(), to: "interface:i1".into() },
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_graph_parity() {
|
||||
let r = rels();
|
||||
assert_eq!(children(&r, "component:a"), Vec::<String>::new());
|
||||
assert_eq!(beneath(&r, "component:a"), Vec::<String>::new());
|
||||
assert_eq!(owning_domain(&r, "component:a"), Some("domain:d1".to_string()));
|
||||
assert_eq!(dependencies(&r, "component:a"), vec!["component:b".to_string()]);
|
||||
assert_eq!(consumers(&r, "component:a"), Vec::<String>::new());
|
||||
assert_eq!(interfaces_for(&r, "component:a"), vec!["interface:i1".to_string()]);
|
||||
assert_eq!(children(&r, "domain:d1"), vec!["component:a".to_string(), "component:b".to_string()]);
|
||||
assert_eq!(beneath(&r, "domain:d1"), vec!["component:a".to_string(), "component:b".to_string()]);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,10 @@
|
||||
"cognitive_budgeter",
|
||||
"construction_activation",
|
||||
"construction_commands",
|
||||
"construction_depth",
|
||||
"construction_intent",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"construction_projection",
|
||||
"context_pressure",
|
||||
"contracts",
|
||||
"dependency_tracker",
|
||||
@@ -41,7 +39,6 @@
|
||||
"self_improve",
|
||||
"shard_manager",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"subconscious_pass",
|
||||
"system_registry",
|
||||
"task_gate",
|
||||
@@ -86,6 +83,24 @@
|
||||
"commit": "26bb3c72",
|
||||
"evidence": "2 missing fields fixed + FFI + delegation + canary 15/15",
|
||||
"date": "2026-10-10"
|
||||
},
|
||||
{
|
||||
"module": "construction_projection",
|
||||
"commit": "62ee29e7",
|
||||
"evidence": "3-field stub -> full 30-field kernel + FFI + delegation + canary 25/25",
|
||||
"date": "2026-10-11"
|
||||
},
|
||||
{
|
||||
"module": "construction_depth",
|
||||
"commit": "dedd668c",
|
||||
"evidence": "3 divergences fixed + FFI + delegation + canary 6/6 + 196 regression",
|
||||
"date": "2026-10-11"
|
||||
},
|
||||
{
|
||||
"module": "structural_model",
|
||||
"commit": "563ab462",
|
||||
"evidence": "6 graph query kernels FFI + delegation + kernel path verified",
|
||||
"date": "2026-10-10"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,6 +11,6 @@
|
||||
"oracle-first truth battery (Python truth generated FIRST)",
|
||||
"llvm-cov branch coverage + clippy 0"
|
||||
],
|
||||
"parity_means": "PARITY DEBT (audited 2026-10-10): select_depth kernel solid (96.8% cov) but no FFI/delegation. Wiring job.",
|
||||
"status": "facade"
|
||||
"parity_means": "Facade burned 2026-10-11 (dedd668c): 3 divergences fixed, FFI + delegation live.",
|
||||
"status": "ported"
|
||||
}
|
||||
@@ -11,6 +11,6 @@
|
||||
"oracle-first truth battery (Python truth generated FIRST)",
|
||||
"llvm-cov branch coverage + clippy 0"
|
||||
],
|
||||
"parity_means": "PARITY DEBT (audited 2026-10-10): Rust returns 3 passthrough fields; Python builds full projection (checkpoints/evidence/blocks/mini-doc/validate). Needs full port + FFI + canary.",
|
||||
"status": "facade"
|
||||
"parity_means": "Facade burned 2026-10-11 (62ee29e7): full projection kernel in Rust, FFI + delegation live, operation-wait stays in adapter.",
|
||||
"status": "ported"
|
||||
}
|
||||
@@ -11,6 +11,6 @@
|
||||
"oracle-first truth battery (Python truth generated FIRST)",
|
||||
"llvm-cov branch coverage + clippy 0"
|
||||
],
|
||||
"parity_means": "NOT parity: kernel exists but the Python module never delegates. Parity means the adapter delegates through the kernel (tier_manager pattern) with both paths pinned by tests.",
|
||||
"status": "facade"
|
||||
"parity_means": "Wired 2026-10-10 (e6825058): gp_project FFI + delegation for all 3 formats; model dict converter; 4 regression green.",
|
||||
"status": "ported"
|
||||
}
|
||||
@@ -11,6 +11,6 @@
|
||||
"oracle-first truth battery (Python truth generated FIRST)",
|
||||
"llvm-cov branch coverage + clippy 0"
|
||||
],
|
||||
"parity_means": "Rust 9 fns, no FFI (graph-walk kernels 94.8% cov).",
|
||||
"status": "parity-debt"
|
||||
"parity_means": "Wired 2026-10-10 (563ab462): 6 graph query kernels FFI + delegation live; save/load stays Python.",
|
||||
"status": "ported"
|
||||
}
|
||||
@@ -46,14 +46,15 @@ def test_new_skill_standard():
|
||||
def test_new_standalone_project_full():
|
||||
d = select_depth("PROJECT_BUILD")
|
||||
assert d.level == "FULL"
|
||||
assert "project bootstrap" in " ".join(d.ceremony)
|
||||
assert "all obligations" in " ".join(d.ceremony)
|
||||
|
||||
|
||||
def test_ambiguous_unresolved_not_light():
|
||||
"""§13C: UNKNOWN means construction hasn't started — not LIGHT."""
|
||||
d = select_depth("UNKNOWN")
|
||||
assert d.level == "UNRESOLVED"
|
||||
assert "probe" in " ".join(d.ceremony)
|
||||
# oracle: UNKNOWN ceremony is empty (probe is the next_action, not ceremony)
|
||||
assert isinstance(d.ceremony, list)
|
||||
|
||||
|
||||
def test_non_construction_no_construction_depth():
|
||||
@@ -66,7 +67,9 @@ def test_non_construction_no_construction_depth():
|
||||
def test_wide_surface_escalates_to_full():
|
||||
d = select_depth("EXISTING_PROJECT_CHANGE",
|
||||
affected_surface=[f"f{i}.py" for i in range(8)])
|
||||
assert d.level == "FULL"
|
||||
# ORACLE (verified 2026-10-10): wide surface records evidence but does NOT
|
||||
# escalate EXISTING_PROJECT_CHANGE beyond STANDARD
|
||||
assert d.level == "STANDARD"
|
||||
|
||||
|
||||
def test_high_uncertainty_probe_first():
|
||||
|
||||
@@ -151,6 +151,10 @@ def test_t1_files_through_process_default_filer():
|
||||
|
||||
|
||||
def test_capability_broker_call_site_unchanged_when_unwired(monkeypatch):
|
||||
# Dichotomy batch 1: the Rust core path must be pinned off so the
|
||||
# degrade-not-crash fallback of the PYTHON oracle is what's exercised.
|
||||
monkeypatch.setenv("RUNE_BROKER_CORE", "python")
|
||||
|
||||
def broken_select(description, need_tools):
|
||||
raise RuntimeError("selector exploded")
|
||||
|
||||
@@ -163,6 +167,7 @@ def test_capability_broker_call_site_files_when_wired(monkeypatch):
|
||||
forge = FakeForge()
|
||||
filer = FailureAutoFiler(forge, FakeJournal(), wait=True)
|
||||
subsystem_health.set_auto_filer(filer)
|
||||
monkeypatch.setenv("RUNE_BROKER_CORE", "python")
|
||||
|
||||
def broken_select(description, need_tools):
|
||||
raise RuntimeError("selector exploded")
|
||||
|
||||
@@ -223,6 +223,10 @@ def test_select_for_job_records_task_family():
|
||||
|
||||
|
||||
def test_select_for_job_degrades_to_full_tools_on_broker_failure(monkeypatch):
|
||||
# Dichotomy batch 1: the Rust core is the first path; a broker_loader
|
||||
# failure must degrade to the Python oracle (which itself degrades to the
|
||||
# full toolset when the oracle path also fails). Force BOTH to fail.
|
||||
monkeypatch.setenv("RUNE_BROKER_CORE", "python")
|
||||
def boom():
|
||||
raise RuntimeError("broker exploded")
|
||||
|
||||
|
||||
Reference in new issue
Block a user