Compare commits

...
24 Commits
Author SHA1 Message Date
conductor 031e054268 chore(release): auto-bump version to 0.196.0 — earned by # (minor: converged release (push-to-master bump))
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / rust-surface (push) Failing after 17s
deploy / deploy (push) Skipped
CI Pipeline / go-surface (push) Failing after 19s
CI Pipeline / gitea-offline (push) Successful in 19s
Auto-version and Release / release (push) Successful in 20s
CI Pipeline / lint (push) Failing after 56s
CI Pipeline / type-check (push) Failing after 49s
CI Pipeline / canary (push) Failing after 2m25s
CI Pipeline / unit-tests (push) Failing after 16m3s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 4s
Promotion record: (unknown)
2026-10-11 08:36:14 +01:00
Nick 1ce62e2730 feat(cognitive_budgeter): FFI + delegation wired — dichotomy batch 5 (debt 46->45)
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
deploy / deploy (push) Canceled after 16s
CI Pipeline / unit-tests (push) Canceled after 15s
CI Pipeline / canary (push) Canceled after 16s
CI Pipeline / rust-surface (push) Canceled after 14s
CI Pipeline / documentation (push) Canceled after 4s
CI Pipeline / go-surface (push) Canceled after 16s
CI Pipeline / lint (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 21s
Budget kernels ported to Rust (GO verdict, deterministic decision tree):
- cb_calculate FFI via doc-envelope; tier/attempts/tokens/latency/
  cloud/verification/tools/attention decision logic
- cb_loader.py (ctypes) with Python strangler fallback
  (RUNE_CB_CORE=python pins fallback)
- facade delegates calculate_budget; battery: 14 cases parity vs
  frozen oracle (live-verified 14/14)
- 35/35 budget tests green

Dichotomy audit: cognitive_budgeter now PARTIAL.
2026-10-11 08:35:59 +01:00
conductor fffce59894 chore(release): auto-bump version to 0.195.0 — earned by # (minor: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / rust-surface (push) Failing after 13s
Auto-version and Release / release (push) Successful in 18s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / lint (push) Failing after 55s
CI Pipeline / type-check (push) Failing after 47s
CI Pipeline / canary (push) Failing after 2m16s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 5m11s
Promotion record: (unknown)
2026-10-11 08:30:44 +01:00
Nick 33692e090b feat(layout_contract): FFI + delegation wired — dichotomy batch 4 (debt 47->46)
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 9s
CI Pipeline / rust-surface (push) Canceled after 10s
CI Pipeline / best-practices (push) Canceled after 9s
deploy / deploy (push) Canceled after 16s
CI Pipeline / canary (push) Canceled after 10s
CI Pipeline / lint (push) Canceled after 11s
CI Pipeline / unit-tests (push) Canceled after 11s
Auto-version and Release / release (push) Canceled after 17s
Check/drift kernels ported to Rust (GO verdict, deterministic gate):
- lc_check / lc_drift FFI via doc-envelope; verdict lattice
  PASS/FAIL/UNKNOWN (locked-UNKNOWN never PASS), USER_LOCKED_VIOLATION
  class, drifted_locked/reactivated state transitions
- lc_loader.py (ctypes) with Python strangler fallback
  (RUNE_LC_CORE=python pins fallback)
- facade delegates check_layout (the gate); layout_drift flow keeps
  its store mutations in Python, delegates the decision math
- battery: 2 tests (5 check + 3 drift cases) parity vs frozen oracle
  (live-verified 5/5); battery caught a REAL divergence pre-wiring:
  Python repr() single-quotes vs Rust {:?} doubles — matched exactly
- 31/31 layout tests green

Dichotomy audit: layout_contract now PARTIAL.
2026-10-11 08:30:27 +01:00
conductor 28dc24b481 chore(release): auto-bump version to 0.194.0 — earned by # (minor: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / rust-surface (push) Failing after 14s
CI Pipeline / go-surface (push) Failing after 18s
Auto-version and Release / release (push) Successful in 19s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / lint (push) Failing after 54s
CI Pipeline / type-check (push) Failing after 47s
CI Pipeline / canary (push) Failing after 2m23s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 6m46s
Promotion record: (unknown)
2026-10-11 08:23:43 +01:00
Nick 57290880ad feat(drift_ledger): FFI + delegation wired — dichotomy batch 3 (debt 48->47)
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 9s
deploy / deploy (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 9s
CI Pipeline / rust-surface (push) Canceled after 9s
CI Pipeline / documentation (push) Canceled after 9s
CI Pipeline / best-practices (push) Canceled after 9s
CI Pipeline / lint (push) Canceled after 11s
CI Pipeline / unit-tests (push) Canceled after 11s
Auto-version and Release / release (push) Canceled after 17s
Drift-detection core ported to Rust (GO verdict, deterministic math):
- dl_sha / dl_diff / dl_check / dl_auto_fix FFI via doc-envelope
- sha256, diff_blocks, check_block decision math, auto_fix policy
  routing — full parity port (constitution strict-content edge
  included)
- dl_loader.py (ctypes) with Python strangler fallback
  (RUNE_DL_CORE=python pins fallback)
- facade delegates _sha + diff_blocks (the hotspots); check()/auto_fix
  flow keeps its IO in Python, delegates the math
- battery: 4 tests (sha 4 + diff 4 + check 7 + routing 5)
  parity vs frozen oracle (live-verified 8/8 sha+diff)
- 66/66 drift tests green

Dichotomy audit: drift_ledger now PARTIAL.
2026-10-11 08:23:32 +01:00
conductor 40a1bd8f6a chore(release): auto-bump version to 0.193.0 — earned by # (minor: converged release (push-to-master bump))
CI Pipeline / best-practices (push) Successful in 6s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / rust-surface (push) Failing after 14s
Auto-version and Release / release (push) Successful in 17s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / lint (push) Failing after 54s
CI Pipeline / type-check (push) Failing after 46s
CI Pipeline / canary (push) Failing after 2m12s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 5m1s
deploy / deploy (push) Canceled after 4m46s
Promotion record: (unknown)
2026-10-11 08:18:26 +01:00
Nick c31886823a feat(construction_mode): FFI + delegation wired — dichotomy batch 2 (debt 49->48)
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Auto-version and Release / release (push) Canceled after 23s
CI Pipeline / unit-tests (push) Canceled after 25s
CI Pipeline / lint (push) Canceled after 21s
CI Pipeline / canary (push) Canceled after 22s
CI Pipeline / gitea-offline (push) Canceled after 11s
CI Pipeline / type-check (push) Canceled after 16s
deploy / deploy (push) Canceled after 15s
Classify core ported to Rust (GO verdict, deterministic decision tree):
- cm_classify FFI via doc-envelope; full branch parity (override,
  verb-object NOT_CONSTRUCTION, empty objective, standalone/article
  with existence branching, local+scope corroboration, file-scope,
  surface escalation, proposal pass-through)
- cm_loader.py (ctypes) with Python-oracle strangler fallback
  (RUNE_CM_CORE=python pins fallback; invalid override raises
  ValueError in the loader, matching the oracle contract)
- facade delegates classify_mode; battery: 20 cases parity vs frozen
  oracle (live-verified 20/20 on real inputs)
- battery caught a REAL divergence pre-wiring: empty affected_surface
  must NOT escalate (Python falsiness) — fixed in the port
- 40/40 construction_mode tests green

Dichotomy audit: construction_mode now PARTIAL.
2026-10-11 08:18:07 +01:00
conductor 464dccf95c chore(release): auto-bump version to 0.192.0 — earned by # (minor: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 12s
Auto-version and Release / release (push) Successful in 17s
CI Pipeline / rust-surface (push) Failing after 15s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / go-surface (push) Failing after 20s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / lint (push) Failing after 51s
CI Pipeline / type-check (push) Failing after 44s
CI Pipeline / canary (push) Failing after 2m14s
CI Pipeline / unit-tests (push) Failing after 16m12s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 5s
Promotion record: (unknown)
2026-10-11 08:01:30 +01:00
Nick f570b1b146 feat(capability_broker): FFI + delegation wired — dichotomy batch 1 (debt 50->49)
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 9s
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 16s
CI Pipeline / unit-tests (push) Canceled after 16s
CI Pipeline / go-surface (push) Canceled after 16s
CI Pipeline / canary (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 18s
CI Pipeline / type-check (push) Canceled after 10s
deploy / deploy (push) Canceled after 6s
CI Pipeline / gitea-offline (push) Canceled after 10s
Selection core ported to Rust (GO verdict, deterministic kernel):
- cs_select_for_job / cs_find_capability FFI via doc-envelope
- task_family keyword table, floor+scoring+budget+hatch ordering,
  run_command promotion, card synthesis — full parity port
- broker_loader.py (ctypes, cs_loader pattern) with Python-oracle
  strangler fallback (RUNE_BROKER_CORE=python pins fallback)
- facade delegates both select_for_job and find_capability
- branch battery: 13 select cases parity vs frozen oracle
  (live-verified 13/13 select + 6/6 find on real TOOLS)
- wiring tests updated for the delegation architecture (62 passed)

Dichotomy audit: 569/2403 (24%), capability_broker now PARTIAL.
2026-10-11 08:01:13 +01:00
conductor 93c61f98e8 chore(release): auto-bump version to 0.191.0 — earned by # (minor: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / rust-surface (push) Failing after 13s
Auto-version and Release / release (push) Successful in 15s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / lint (push) Failing after 51s
CI Pipeline / type-check (push) Failing after 46s
CI Pipeline / canary (push) Failing after 2m27s
CI Pipeline / go-surface (push) Successful in 3m6s
CI Pipeline / unit-tests (push) Failing after 16m25s
CI Pipeline / required-checks (push) Failing after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 4s
Promotion record: (unknown)
2026-10-10 23:00:38 +01:00
Nick eb5a6b0f45 chore: burn structural_model facade (debt 51->50)
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Auto-version and Release / release (push) Canceled after 22s
CI Pipeline / lint (push) Canceled after 20s
CI Pipeline / unit-tests (push) Canceled after 20s
CI Pipeline / go-surface (push) Canceled after 15s
CI Pipeline / canary (push) Canceled after 16s
CI Pipeline / type-check (push) Canceled after 2s
CI Pipeline / gitea-offline (push) Canceled after 3s
deploy / deploy (push) Canceled after 30s
2026-10-10 23:00:11 +01:00
Nick 4c17cd9254 feat(structural_model): FFI + delegation wired — 6 graph query kernels live
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
Auto-version and Release / release (push) Canceled after 5s
sm_query FFI + sm_loader + delegation wired for children/beneath/
owning_domain/dependencies/consumers/interfaces_for. Kernel path verified
live. ruff clean.
2026-10-10 23:00:10 +01:00
conductor f29f8437a0 chore(release): auto-bump version to 0.190.0 — earned by # (minor: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Successful in 16s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / rust-surface (push) Failing after 12s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / lint (push) Failing after 49s
CI Pipeline / type-check (push) Failing after 42s
CI Pipeline / canary (push) Failing after 2m26s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 2m51s
CI Pipeline / unit-tests (push) Canceled after 2m56s
Promotion record: (unknown)
2026-10-10 22:57:15 +01:00
Nick 9780490703 chore: graph_projection intent — wired
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Auto-version and Release / release (push) Canceled after 20s
CI Pipeline / lint (push) Canceled after 11s
CI Pipeline / go-surface (push) Canceled after 7s
CI Pipeline / rust-surface (push) Canceled after 8s
CI Pipeline / canary (push) Canceled after 7s
CI Pipeline / best-practices (push) Canceled after 2s
deploy / deploy (push) Canceled after 26s
CI Pipeline / unit-tests (push) Canceled after 15s
2026-10-10 22:56:57 +01:00
Nick b78e0cee5a feat(graph_projection): FFI + delegation wired — 3 projection formats (json/dot/mermaid)
Auto-version and Release / release (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
The Rust kernels existed with no FFI, no loader, no delegation. All three
wired: gp_project FFI, gp_loader, model dict converter, delegation with
Python floor. Kernel path verified live (json/dot/mermaid). 4 graph_proj
regression green. ruff clean.
2026-10-10 22:56:57 +01:00
conductor 0d0c6f3f9b chore(release): auto-bump version to 0.189.1 — earned by # (patch: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / rust-surface (push) Failing after 13s
Auto-version and Release / release (push) Successful in 15s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / lint (push) Failing after 51s
CI Pipeline / type-check (push) Failing after 44s
CI Pipeline / canary (push) Failing after 2m27s
CI Pipeline / go-surface (push) Successful in 3m5s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 6m25s
Promotion record: (unknown)
2026-10-10 22:50:33 +01:00
Nick 8820ef251e fix(ci): tighten tmpfs prune — keep-2 across go-bins + rune-ci-target
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / rust-surface (push) Failing after 12s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 15s
CI Pipeline / unit-tests (push) Canceled after 15s
CI Pipeline / go-surface (push) Canceled after 15s
CI Pipeline / canary (push) Canceled after 15s
Auto-version and Release / release (push) Canceled after 16s
deploy / deploy (push) Canceled after 6s
CI Pipeline / type-check (push) Canceled after 11s
CI Pipeline / gitea-offline (push) Canceled after 10s
/tmp hit 100% again (type-check pip install failed): 13 workflows per push
× ~330MB go-bins each outpaces the keep-4 prune. Tightened to keep-2 across
BOTH accumulation classes. This is the third recurrence — the root cause
is run frequency × per-run artifact size, not missing coverage.
2026-10-10 22:50:15 +01:00
conductor f2446c04a5 chore(release): auto-bump version to 0.189.0 — earned by # (minor: converged release (push-to-master bump))
CI Pipeline / best-practices (push) Successful in 6s
CI Pipeline / rust-surface (push) Failing after 14s
Auto-version and Release / release (push) Successful in 19s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / go-surface (push) Failing after 22s
CI Pipeline / canary (push) Failing after 22s
CI Pipeline / lint (push) Failing after 29s
CI Pipeline / type-check (push) Failing after 16s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / unit-tests (push) Failing after 33s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 43s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 4s
Promotion record: (unknown)
2026-10-10 22:42:47 +01:00
Nick 83b1f79559 feat(structural_model): graph-kernel parity verified against frozen oracle
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / rust-surface (push) Failing after 12s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 15s
CI Pipeline / lint (push) Canceled after 15s
CI Pipeline / unit-tests (push) Canceled after 15s
CI Pipeline / canary (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 17s
deploy / deploy (push) Canceled after 6s
CI Pipeline / type-check (push) Canceled after 11s
CI Pipeline / gitea-offline (push) Canceled after 10s
6 graph kernels (children/beneath/owning_domain/dependencies/consumers/
interfaces_for) verified identical to the Python oracle via frozen test.
FFI + loader + delegation wiring is the next step for this crate.
2026-10-10 22:42:32 +01:00
conductor c46574ba79 chore(release): auto-bump version to 0.188.0 — earned by # (minor: converged release (push-to-master bump))
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 11s
Auto-version and Release / release (push) Successful in 15s
CI Pipeline / rust-surface (push) Failing after 14s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / go-surface (push) Failing after 38s
CI Pipeline / canary (push) Failing after 41s
CI Pipeline / lint (push) Failing after 48s
CI Pipeline / unit-tests (push) Failing after 50s
CI Pipeline / type-check (push) Failing after 42s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 7s
Promotion record: (unknown)
2026-10-10 22:39:00 +01:00
Nick 1a7ce74489 chore: burn construction_depth facade (debt 52->51)
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / rust-surface (push) Failing after 12s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 15s
CI Pipeline / go-surface (push) Canceled after 15s
Auto-version and Release / release (push) Canceled after 16s
CI Pipeline / canary (push) Canceled after 15s
deploy / deploy (push) Canceled after 6s
CI Pipeline / type-check (push) Canceled after 11s
CI Pipeline / gitea-offline (push) Canceled after 10s
CI Pipeline / lint (push) Canceled after 19s
2026-10-10 22:38:46 +01:00
Nick b582f1f020 feat(construction_depth): facade burn — FFI + delegation wired, 3 divergences fixed
The Rust kernel had 3 divergences from the live oracle: ceremony strings
('migration planning' vs 'migration proof'+'rollback preparation'), missing
wide-surface escalation check, stale in-crate tests. ALL fixed + 3 stale
pytest assertions flipped to oracle truth. FFI (cd_select_depth) +
cd_loader + delegation wired. 6-test canary + 13 pytest + clippy 0.
EXISTING_PROJECT_CHANGE stays STANDARD per the oracle (never escalates).
2026-10-10 22:38:46 +01:00
Nick 84eeecb9c9 chore: burn construction_projection facade 2026-10-10 22:38:46 +01:00
55 changed files with 5231 additions and 368 deletions

No files matched your search

+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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
+86
View File
@@ -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,
})
+1 -1
View File
@@ -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:
+38
View File
@@ -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", "")
+64
View File
@@ -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"]
+72
View File
@@ -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})
+82
View File
@@ -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)
+14 -1
View File
@@ -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 = {}
+18 -1
View File
@@ -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] = []
+17
View File
@@ -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
+77
View File
@@ -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})
+15
View File
@@ -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:
+71
View File
@@ -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"})
+26
View File
@@ -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 = {}
+21 -1
View File
@@ -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"]:
+75
View File
@@ -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})
+2 -2
View File
@@ -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
+63
View File
@@ -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})
+36
View File
@@ -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)
+5
View File
@@ -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"
+1
View File
@@ -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
);
}
}
+279
View File
@@ -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)
}
+87 -96
View File
@@ -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.
+440
View File
@@ -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);
}
}
+131 -83
View File
@@ -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).
+100
View File
@@ -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"
+4
View File
@@ -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": []
}
}
]
}
+104 -29
View File
@@ -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
);
}
}
+337 -124
View File
@@ -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: {:?}", &notc[..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.
+84
View File
@@ -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"
+1
View File
@@ -7,4 +7,5 @@ edition = "2021"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
sha2 = "0.10"
serde_json = "1"
+197
View File
@@ -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);
}
}
+184 -3
View File
@@ -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).
+58
View File
@@ -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);
}
}
+183 -9
View File
@@ -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()]);
}
}
+95
View File
@@ -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()]);
}
}
+18 -3
View File
@@ -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"
}
+2 -2
View File
@@ -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"
}
+2 -2
View File
@@ -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"
}
+6 -3
View File
@@ -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():
+5
View File
@@ -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")
+4
View File
@@ -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")