Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633af06e8a | ||
|
|
62e47b3812 | ||
|
|
c0ec3135c6 | ||
|
|
622cceb2f9 | ||
|
|
b50e8553d7 | ||
|
|
4649df1ccc | ||
|
|
fa4f9a2cbb | ||
|
|
c4aa7dfab7 | ||
|
|
fe2755dbfc | ||
|
|
27afd002a7 | ||
|
|
e9d3efae5d | ||
|
|
569ee78d0a | ||
|
|
9ebf7865ca | ||
|
|
9d23d3f50a | ||
|
|
2d13846f86 | ||
|
|
79b6e38148 | ||
|
|
875ca43ad1 | ||
|
|
b52aa5bbbc | ||
|
|
165b4bd8d3 | ||
|
|
00bf4d210c | ||
|
|
4d0883b39c | ||
|
|
352299b614 | ||
|
|
a26c5004a2 | ||
|
|
ba54bc6e8f | ||
|
|
44b25064c9 | ||
|
|
7c83200c2e | ||
|
|
9b7565b536 | ||
|
|
6a311f80d3 | ||
|
|
d929b9a068 | ||
|
|
5680ec3e7b | ||
|
|
9856b2b383 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.231.0**
|
||||
**Current release: v0.238.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.**
|
||||
|
||||
|
||||
@@ -2,56 +2,47 @@
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**Function-level completion: 32%** (838/2627 runtime functions matched)
|
||||
**Function-level completion: 37%** (1020/2781 runtime functions matched)
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 25 | 8218 | 80%+ of functions ported |
|
||||
| PARTIAL | 89 | 44507 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 113 | 22686 | no name-preserving implementation |
|
||||
| Total runtime | 227 | 75411 | |
|
||||
| FULL | 36 | 13024 | 80%+ of functions ported |
|
||||
| PARTIAL | 93 | 45658 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 108 | 19178 | no name-preserving implementation |
|
||||
| Total runtime | 237 | 77860 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| rune_tui | 4170 | 159 | TUI-LAST |
|
||||
| curator | 1838 | 43 | GO |
|
||||
| canary_audit_ledger | 541 | 2 | TEST-INFRA |
|
||||
| tui_validator | 514 | 13 | TUI-LAST |
|
||||
| worthiness | 411 | 13 | unclassified |
|
||||
| chat_bubble | 401 | 12 | TUI-LAST |
|
||||
| drift-ledger | 355 | 14 | unclassified |
|
||||
| pc_loader | 337 | 22 | unclassified |
|
||||
| canary_rune_serve | 329 | 13 | TEST-INFRA |
|
||||
| canary_p1_07 | 324 | 10 | TEST-INFRA |
|
||||
| canary_p0_07 | 310 | 10 | TEST-INFRA |
|
||||
| canary_p1_01 | 294 | 12 | TEST-INFRA |
|
||||
| outcome_ledger | 286 | 7 | RUST |
|
||||
| time_awareness | 286 | 14 | RUST |
|
||||
| canary_p1_06 | 280 | 7 | TEST-INFRA |
|
||||
| canary_p1_03 | 271 | 9 | TEST-INFRA |
|
||||
| canary_gitea_automation | 263 | 3 | TEST-INFRA |
|
||||
| canary_p1_02 | 253 | 10 | TEST-INFRA |
|
||||
| construction_patterns_builtin | 249 | 1 | RUST |
|
||||
| canary_full_system | 248 | 2 | TEST-INFRA |
|
||||
| task_gate | 235 | 8 | RUST |
|
||||
| sm_loader | 239 | 14 | unclassified |
|
||||
| canary_native | 231 | 3 | TEST-INFRA |
|
||||
| lexicon_evolution | 229 | 7 | unclassified |
|
||||
| openai_compat_transport | 227 | 9 | GO |
|
||||
| shard_manager | 223 | 9 | RUST |
|
||||
| spine_language | 218 | 5 | RUST |
|
||||
| canary_p0_04 | 207 | 8 | TEST-INFRA |
|
||||
| wr_loader | 206 | 20 | unclassified |
|
||||
| cc_loader | 205 | 28 | RUST |
|
||||
| forge_client | 203 | 18 | GO |
|
||||
| canary_context_broker | 199 | 10 | TEST-INFRA |
|
||||
| canary_p0_05 | 199 | 13 | TEST-INFRA |
|
||||
| cl_loader | 196 | 20 | unclassified |
|
||||
| testing_mode | 196 | 12 | RUST |
|
||||
| grading | 193 | 3 | unclassified |
|
||||
| canary_spine_meta | 192 | 7 | TEST-INFRA |
|
||||
| message_graph | 187 | 12 | unclassified |
|
||||
| ffi_query_memory_smoke | 183 | 5 | RUST |
|
||||
| tm_loader | 178 | 13 | unclassified |
|
||||
| smod_loader | 175 | 10 | unclassified |
|
||||
| behaviour_ledger | 174 | 12 | RUST |
|
||||
| canary_tui_transport | 174 | 7 | TEST-INFRA |
|
||||
| sr_loader | 173 | 18 | unclassified |
|
||||
@@ -60,6 +51,7 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| ms_loader | 163 | 16 | RUST |
|
||||
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
|
||||
| dc_loader | 158 | 13 | unclassified |
|
||||
| mg_loader | 152 | 14 | unclassified |
|
||||
| mse_loader | 152 | 12 | unclassified |
|
||||
| bb_loader | 151 | 14 | unclassified |
|
||||
| forge | 146 | 11 | GO |
|
||||
@@ -72,32 +64,36 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| ps_loader | 136 | 13 | unclassified |
|
||||
| verification_layer | 136 | 11 | RUST |
|
||||
| tui_backlog | 125 | 5 | TUI-LAST |
|
||||
| le_loader | 124 | 10 | unclassified |
|
||||
| state_checkpoint | 123 | 8 | unclassified |
|
||||
| cp_loader | 122 | 9 | unclassified |
|
||||
| dl_loader | 118 | 12 | unclassified |
|
||||
| drift_resume | 118 | 3 | RUST |
|
||||
| tl_loader | 118 | 9 | unclassified |
|
||||
| bh_loader | 117 | 9 | unclassified |
|
||||
| tg_loader | 117 | 7 | unclassified |
|
||||
| canary_conversation_state | 115 | 2 | TEST-INFRA |
|
||||
| contracts_loader | 112 | 8 | unclassified |
|
||||
| gr_loader | 112 | 8 | unclassified |
|
||||
| canary_query_memory | 111 | 2 | TEST-INFRA |
|
||||
| tui_transport | 108 | 10 | TUI-LAST |
|
||||
| canary_unbounded | 105 | 2 | TEST-INFRA |
|
||||
| message_widgets | 104 | 6 | TUI-LAST |
|
||||
| self_modify | 102 | 7 | unclassified |
|
||||
| ct_loader | 103 | 8 | unclassified |
|
||||
| sc_loader | 96 | 6 | RUST |
|
||||
| inference_paths | 95 | 2 | unclassified |
|
||||
| mutation_guards | 94 | 2 | unclassified |
|
||||
| concept_topology | 90 | 7 | unclassified |
|
||||
| cs_loader | 90 | 5 | RUST |
|
||||
| ci_loader | 89 | 4 | unclassified |
|
||||
| tc_loader | 88 | 5 | RUST |
|
||||
| broker_loader | 86 | 5 | unclassified |
|
||||
| canary_schema_migration | 86 | 1 | TEST-INFRA |
|
||||
| ss_loader | 86 | 7 | unclassified |
|
||||
| ol_loader | 85 | 4 | unclassified |
|
||||
| to_loader | 85 | 4 | unclassified |
|
||||
| ta_loader | 84 | 4 | unclassified |
|
||||
| mems_loader | 83 | 5 | RUST |
|
||||
| cm_loader | 82 | 4 | unclassified |
|
||||
| dl_loader | 77 | 5 | unclassified |
|
||||
| lc_loader | 75 | 5 | unclassified |
|
||||
| cd_loader | 72 | 4 | unclassified |
|
||||
| execution_trace | 71 | 4 | unclassified |
|
||||
@@ -109,7 +105,6 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| cb_loader | 64 | 3 | unclassified |
|
||||
| skill_compiler | 64 | 4 | unclassified |
|
||||
| sg_loader | 63 | 4 | unclassified |
|
||||
| sm_loader | 63 | 4 | unclassified |
|
||||
| meaningful_events | 61 | 3 | unclassified |
|
||||
| outcome_utility | 59 | 4 | unclassified |
|
||||
| memory_types | 54 | 2 | unclassified |
|
||||
@@ -166,17 +161,19 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| claim_audit | 484 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
|
||||
| drift_core | 471 | 4/14 | _ms_core, _now_iso, _py_check_promotion_eligibility, promote_entry, compute_fama, compute_drift_velocity |
|
||||
| model_profiles | 438 | 5/15 | get_profile_path, load_profile, save_profile, build_profile_from_ollama, scan_all_models, get_or_create_profile |
|
||||
| testing_mode | 362 | 11/21 | _now, _tm_core, _py_default_state, _py_next_item, _py_record, _py_note_blocker |
|
||||
| tier_manager | 262 | 5/15 | _cc_probe, cc_loader_pin, _py_core_now, _py_get_tier, _py_promote, _py_demote |
|
||||
| witness | 446 | 8/18 | _ctx_budget, _now, _cc_probe, cc_loader_pin, _py_extract_topic, _py_detect_signals |
|
||||
| drift_compression | 307 | 3/12 | _py_get_compression_ratio, _py_compress_text, _py_compress_entry, _py_should_warn, _py_warn_before_compress, get_compression_ratio |
|
||||
| drift_ledger | 471 | 7/16 | ledger_path, read_block, load_ledger, save_ledger, surface_alert, snapshot |
|
||||
| scaffold | 868 | 22/31 | init, set_prd, add_maintenance_item, bind_repo, observe_repo, repo_state_from_git |
|
||||
| behavior | 537 | 6/14 | _bh_core, connect, _correction_matches_python, _habit_matches_python, _reinforce_rust, _reinforce_python |
|
||||
| builder_bridge | 326 | 3/11 | _construction_store, _active_construction_lines, _default_selector, _cached_memory, _get_selector, _retrieve_memory |
|
||||
| time_awareness | 402 | 12/20 | _clock_args, _ta_core, get_time_awareness, to_dict, _to_state, _apply_state |
|
||||
| construction_pattern | 547 | 7/14 | _core, _need_dict, _surface_text, _path_matches, _entity_matches, _obligation_when_hit |
|
||||
| contracts | 743 | 4/11 | _ct_core, _resolve_and_project, _run_contract_python, capture_signatures, save_signatures, load_signatures |
|
||||
| graph_memory | 474 | 2/9 | now_iso, connect, resolve_entity, add_edge, query, stats |
|
||||
| intent_continuity | 168 | 2/9 | to_dict, from_dict, _load, save, create_detour, resolve_detour |
|
||||
| lexicon_evolution | 304 | 7/14 | _le_core, _py_content_words, _py_distinctive_terms, _py_shared_distinctive, _py_coherence, _py_grounded |
|
||||
| pins | 282 | 5/12 | _store_path, _save, _detail_blob, unpin, _py_pin_recall, list_pins |
|
||||
| voice_hybrid | 427 | 3/10 | _connect, _load_growth, _save_growth, source_profiles, always, grow |
|
||||
| voice_sync | 299 | 3/10 | _now, get_delivery_register_from_db, get_delivery_register, update_rune_md, check_sync, handle_hook |
|
||||
@@ -197,6 +194,7 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| construction_commands | 268 | 1/5 | _refuse, _validate, _accept, dispatch |
|
||||
| drift_cache | 357 | 12/16 | _dc_core, _py_compute_priority, _py_qualifies_for_cache, main |
|
||||
| drift_observability | 399 | 5/9 | _do_core, main, _load_alerts, _save_alerts |
|
||||
| grading | 253 | 3/7 | _gr_core, _py_affirm_rule, _py_grade_fact, _py_tier_of |
|
||||
| layout_contract | 336 | 2/6 | define_region, bind_component, amend_layout_contract, record_layout_observation |
|
||||
| model_connect | 471 | 7/11 | _ctx_budget, _now, _get_cloud_models, handle_hook |
|
||||
| prompt_sync | 487 | 11/15 | _now, _rust_available, get_store, main |
|
||||
@@ -204,6 +202,7 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| self_improve | 246 | 1/5 | _now, suggest_improvements, handle_hook, main |
|
||||
| stage_guards | 303 | 7/11 | _py_liveness_canary, _py_reconcile, to_dict, chk |
|
||||
| subconscious_pass | 187 | 1/5 | main, _extract_topic, save_reflection, get_recent_findings |
|
||||
| task_gate | 301 | 5/9 | _tg_core, _now, list_gates, main |
|
||||
| construction_activation | 237 | 1/4 | _scaffold_for, _auto_structural_awareness, dispatch_from_decision |
|
||||
| construction_mode | 412 | 2/5 | _contains_any, to_dict, from_dict |
|
||||
| construction_projection | 254 | 1/4 | _contract_entity, validate, write_mini_doc |
|
||||
@@ -227,11 +226,14 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| curator | 1838 | 40/43 | GO |
|
||||
| abstraction | 656 | 17/17 | GO |
|
||||
| system_registry | 603 | 14/16 | RUST |
|
||||
| construction_intent | 580 | 13/16 | RUST |
|
||||
| capability_ledger | 571 | 18/19 | RUST |
|
||||
| drift_ledger | 548 | 20/22 | RUST |
|
||||
| event_wait | 518 | 14/14 | GO |
|
||||
| worthiness | 469 | 12/14 | unclassified |
|
||||
| tool_protocol | 461 | 22/27 | GO |
|
||||
| auto_tasks | 460 | 14/14 | GO |
|
||||
| programme_continuation | 458 | 11/12 | RUST |
|
||||
@@ -240,12 +242,20 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| model_routing | 432 | 13/13 | GO |
|
||||
| build_basis | 375 | 11/13 | RUST |
|
||||
| attention_capsule | 365 | 6/7 | RUST |
|
||||
| drift-ledger | 355 | 14/14 | unclassified |
|
||||
| outcome_ledger | 318 | 7/8 | RUST |
|
||||
| message_queue | 316 | 13/13 | GO |
|
||||
| message_graph | 295 | 12/13 | unclassified |
|
||||
| toolchain | 295 | 5/6 | RUST |
|
||||
| shard_manager | 276 | 9/10 | RUST |
|
||||
| auto_context | 273 | 8/8 | GO |
|
||||
| openai_compat_transport | 227 | 9/9 | GO |
|
||||
| evidence_probe | 213 | 9/9 | GO |
|
||||
| forge_client | 203 | 18/18 | GO |
|
||||
| self_modify | 149 | 7/8 | unclassified |
|
||||
| recall_uplift | 141 | 1/1 | RUST |
|
||||
| rune_spinner | 130 | 4/5 | RUST |
|
||||
| concept_topology | 128 | 7/8 | unclassified |
|
||||
| wait_guard | 116 | 1/1 | RUST |
|
||||
| failure_recovery | 108 | 1/1 | RUST |
|
||||
| programme_gates | 85 | 4/5 | RUST |
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.231.0"
|
||||
version = "0.238.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: the FULL Rust behaviour_ledger core (bl_loader) vs the
|
||||
retired Python oracle (the ep_loader shim path — RUNE_BL_CORE=python),
|
||||
dichotomy wave #2479. Replays the frozen truth corpus PLUS seeded fuzz
|
||||
over every public function and branch. The clock is pinned (no-clock
|
||||
law — the kernel takes `now` in), so nothing is masked.
|
||||
|
||||
The facade reads RUNE_BL_CORE per call, so one process can flip between
|
||||
the pinned oracle and the wave core.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src" / "rune"))
|
||||
|
||||
from rune import behaviour_ledger as bl # noqa: E402
|
||||
|
||||
TRUTH_PATH = Path(__file__).resolve().parents[1] / (
|
||||
"src/rust/behaviour_ledger/src/frozen_bl_truth.json")
|
||||
|
||||
FROZEN = "2026-10-11T23:00:00Z"
|
||||
CLOCK = {"n": 0}
|
||||
bl._now = lambda: f"2026-10-11T23:{CLOCK['n'] // 60:02d}:{CLOCK['n'] % 60:02d}Z" # noqa: E731
|
||||
|
||||
|
||||
def call_core(core, fn, *args, **kwargs):
|
||||
old = os.environ.get("RUNE_BL_CORE")
|
||||
os.environ["RUNE_BL_CORE"] = core
|
||||
try:
|
||||
return getattr(bl, fn)(*args, **kwargs)
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_BL_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_BL_CORE"] = old
|
||||
|
||||
|
||||
def fresh_ledger(doc=None):
|
||||
led = bl.BehaviourLedger(Path(tempfile.mkdtemp(prefix="bl_par_")) / "s")
|
||||
if doc is not None:
|
||||
led.doc = copy.deepcopy(doc)
|
||||
return led
|
||||
|
||||
|
||||
def compare(label, ok, failures, detail):
|
||||
if not ok:
|
||||
failures.append((label, detail))
|
||||
print(f"FAIL {label}\n{detail}")
|
||||
return ok
|
||||
|
||||
|
||||
def scenario(group, name, doc_in, op, failures):
|
||||
"""Run one op under both cores on identical deep-copied pre-state;
|
||||
compare (return | ValueError message, doc after)."""
|
||||
py_ret = py_err = py_doc = None
|
||||
try:
|
||||
CLOCK["n"] = 0
|
||||
led = fresh_ledger(doc_in)
|
||||
py_ret = op(led)
|
||||
py_doc = led.doc
|
||||
except ValueError as e:
|
||||
py_err = str(e)
|
||||
rs_ret = rs_err = rs_doc = None
|
||||
try:
|
||||
CLOCK["n"] = 0
|
||||
led = fresh_ledger(doc_in)
|
||||
rs_ret = op(led)
|
||||
rs_doc = led.doc
|
||||
except ValueError as e:
|
||||
rs_err = str(e)
|
||||
ok = (json.dumps(py_ret, sort_keys=True, default=str)
|
||||
== json.dumps(rs_ret, sort_keys=True, default=str)
|
||||
and py_err == rs_err
|
||||
and json.dumps(py_doc, sort_keys=True)
|
||||
== json.dumps(rs_doc, sort_keys=True))
|
||||
return compare(f"{group}:{name}", ok, failures,
|
||||
f"py={json.dumps(py_ret, sort_keys=True, default=str)[:300]}"
|
||||
f"/{py_err}\nrs={json.dumps(rs_ret, sort_keys=True, default=str)[:300]}"
|
||||
f"/{rs_err}")
|
||||
|
||||
|
||||
# ── fuzz pools ─────────────────────────────────────────────────────────
|
||||
IDS = ["B-1", "B-2", "behaviour:B-2", "B-3", "GHOST", "behaviour:GHOST",
|
||||
"UNI-é", "X" * 60, "b", "behaviour:b"]
|
||||
PROJECTS = ["demo", "other", "démø", ""]
|
||||
COMPONENTS = ["component:a", "component:b", "component:bridge",
|
||||
"component:ünï", ""]
|
||||
ENTS = ["component:a", "component:b", "component:bridge",
|
||||
"interface:completion", "component:ünï", "interface:x", "domain:d"]
|
||||
DETAILS = ["ok", "", "x" * 250, "café ✓ ünïcode", "multi\nline", "quote'"]
|
||||
GENS = ["gen-1", "gen-2", "", "gén-3"]
|
||||
CONFIRMERS = ["user", "ci-robot", "", "user ", "nick"]
|
||||
NOTES = ["accepted for v1", "", "n" * 220, "»signed«"]
|
||||
|
||||
|
||||
def rand_params(rng):
|
||||
return {
|
||||
"behaviour_id": rng.choice(IDS),
|
||||
"owning_project": rng.choice(PROJECTS),
|
||||
"intended_behaviour": rng.choice(DETAILS),
|
||||
"origin_intent": rng.choice(DETAILS),
|
||||
"pass_criteria": rng.choice(DETAILS),
|
||||
"fail_criteria": rng.choice(DETAILS),
|
||||
"procedure": rng.choice(DETAILS),
|
||||
"owner_component": rng.choice(COMPONENTS),
|
||||
"dependencies": rng.sample(ENTS, rng.randint(0, 3)),
|
||||
"automated_ref": rng.choice(["tests/x.py", ""]),
|
||||
"environment": rng.choice(["ci", "prod", ""]),
|
||||
}
|
||||
|
||||
|
||||
def rand_doc(rng):
|
||||
"""Deterministic pre-state via the pinned-clock oracle (python pin)."""
|
||||
old = os.environ.get("RUNE_BL_CORE")
|
||||
os.environ["RUNE_BL_CORE"] = "python"
|
||||
try:
|
||||
led = fresh_ledger()
|
||||
for i in range(rng.randint(0, 4)):
|
||||
bid = f"B-{i}"
|
||||
p = rand_params(rng)
|
||||
p["behaviour_id"] = bid
|
||||
try:
|
||||
led.define(**p)
|
||||
except ValueError:
|
||||
pass
|
||||
roll = rng.random()
|
||||
if roll < 0.45: # full confirmation chain
|
||||
led.record_auto_verification(
|
||||
bid, passed=rng.random() < 0.85,
|
||||
detail=rng.choice(DETAILS), generation=rng.choice(GENS))
|
||||
if rng.random() < 0.8:
|
||||
led.record_real_world(
|
||||
bid, passed=rng.random() < 0.85,
|
||||
detail=rng.choice(DETAILS),
|
||||
generation=rng.choice(GENS),
|
||||
confirmed_by=rng.choice(CONFIRMERS))
|
||||
elif roll < 0.6:
|
||||
led.record_auto_verification(
|
||||
bid, passed=False, detail=rng.choice(DETAILS),
|
||||
generation=rng.choice(GENS))
|
||||
elif roll < 0.7:
|
||||
led.accept_limitation(bid, note=rng.choice(NOTES))
|
||||
return led.doc
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_BL_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_BL_CORE"] = old
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures: list[tuple[str, str]] = []
|
||||
n_truth = 0
|
||||
|
||||
# 1. frozen truth corpus (kernel-level replay lives in the crate's
|
||||
# branch battery; replayed here at FACADE level, both cores)
|
||||
truth = json.loads(TRUTH_PATH.read_text())["cases"]
|
||||
|
||||
def run_case(group, case):
|
||||
i = case["input"]
|
||||
if group == "define":
|
||||
params = {k: v for k, v in i.items()
|
||||
if k not in ("doc", "now")}
|
||||
params["dependencies"] = params.get("dependencies")
|
||||
op = lambda led: led.define(**params) # noqa: E731
|
||||
elif group == "get":
|
||||
op = lambda led: led.get(i["behaviour_id"]) # noqa: E731
|
||||
elif group == "record_auto_verification":
|
||||
params = {k: v for k, v in i.items()
|
||||
if k not in ("doc", "now")}
|
||||
op = lambda led: led.record_auto_verification( # noqa: E731
|
||||
params["behaviour_id"], **{k: v for k, v in params.items()
|
||||
if k != "behaviour_id"})
|
||||
elif group == "record_real_world":
|
||||
params = {k: v for k, v in i.items()
|
||||
if k not in ("doc", "now")}
|
||||
op = lambda led: led.record_real_world( # noqa: E731
|
||||
params["behaviour_id"], **{k: v for k, v in params.items()
|
||||
if k != "behaviour_id"})
|
||||
elif group == "accept_limitation":
|
||||
params = {k: v for k, v in i.items()
|
||||
if k not in ("doc", "now")}
|
||||
op = lambda led: led.accept_limitation( # noqa: E731
|
||||
params["behaviour_id"], **{k: v for k, v in params.items()
|
||||
if k != "behaviour_id"})
|
||||
elif group == "invalidate_for_change":
|
||||
params = {k: v for k, v in i.items()
|
||||
if k not in ("doc", "now")}
|
||||
params["changed_entities"] = set(params["changed_entities"])
|
||||
op = lambda led: led.invalidate_for_change(**params) # noqa: E731
|
||||
elif group == "retest_history":
|
||||
op = lambda led: led.retest_history(i["behaviour_id"]) # noqa: E731
|
||||
elif group == "outstanding":
|
||||
params = {k: v for k, v in i.items() if k != "doc"}
|
||||
op = lambda led: led.outstanding(**params) # noqa: E731
|
||||
else: # summary
|
||||
op = lambda led: led.summary() # noqa: E731
|
||||
scenario(group, case["name"], i["doc"], op, failures)
|
||||
|
||||
for group, cases in truth.items():
|
||||
for case in cases:
|
||||
n_truth += 1
|
||||
run_case(group, case)
|
||||
|
||||
# 2. seeded fuzz (seed 2479). ALL randomness is consumed by the
|
||||
# opmaker ONCE — the closure it returns is deterministic, so both
|
||||
# core runs see identical args (poisoned-truth law).
|
||||
rng = random.Random(2479)
|
||||
n_fuzz = 0
|
||||
|
||||
def pick_bid(r, doc):
|
||||
"""Existing (raw key) or random id — consumed once."""
|
||||
bids = list(doc.get("behaviours", {}))
|
||||
if bids and r.random() < 0.7:
|
||||
return r.choice(bids)
|
||||
return r.choice(IDS)
|
||||
|
||||
def fuzz(group, opmaker, count):
|
||||
nonlocal n_fuzz
|
||||
for j in range(count):
|
||||
n_fuzz += 1
|
||||
doc = rand_doc(rng)
|
||||
scenario(group, f"fuzz{j}", doc, opmaker(rng, doc), failures)
|
||||
|
||||
fuzz("define", lambda r, d: (lambda led, p=rand_params(r): led.define(**p)),
|
||||
150)
|
||||
fuzz("get", lambda r, d: (lambda led, b=r.choice(IDS): led.get(b)), 100)
|
||||
|
||||
def auto_op(r, d):
|
||||
bid = pick_bid(r, d)
|
||||
passed = r.random() < 0.6
|
||||
detail = r.choice(DETAILS)
|
||||
gen = r.choice(GENS)
|
||||
return lambda led: led.record_auto_verification(
|
||||
bid, passed=passed, detail=detail, generation=gen)
|
||||
|
||||
fuzz("record_auto_verification", auto_op, 150)
|
||||
|
||||
def rw_op(r, d):
|
||||
bid = pick_bid(r, d)
|
||||
passed = r.random() < 0.6
|
||||
detail = r.choice(DETAILS)
|
||||
gen = r.choice(GENS)
|
||||
by = r.choice(CONFIRMERS)
|
||||
return lambda led: led.record_real_world(
|
||||
bid, passed=passed, detail=detail, generation=gen, confirmed_by=by)
|
||||
|
||||
fuzz("record_real_world", rw_op, 150)
|
||||
|
||||
def lim_op(r, d):
|
||||
bid = pick_bid(r, d)
|
||||
note = r.choice(NOTES)
|
||||
by = r.choice(CONFIRMERS)
|
||||
return lambda led: led.accept_limitation(bid, note=note, approved_by=by)
|
||||
|
||||
fuzz("accept_limitation", lim_op, 100)
|
||||
|
||||
def inv_op(r, d):
|
||||
changed = set(r.sample(ENTS, r.randint(0, 3)))
|
||||
reasons = ({"behaviour:B-0": "custom reason"} if r.random() < 0.3
|
||||
else None)
|
||||
gen = r.choice(GENS)
|
||||
return lambda led: led.invalidate_for_change(
|
||||
changed_entities=changed, generation=gen, reasons=reasons)
|
||||
|
||||
fuzz("invalidate_for_change", inv_op, 200)
|
||||
fuzz("retest_history",
|
||||
lambda r, d: (lambda led, b=r.choice(IDS): led.retest_history(b)),
|
||||
80)
|
||||
|
||||
def out_op(r, d):
|
||||
kw = {}
|
||||
if r.random() < 0.5:
|
||||
kw["component"] = r.choice(COMPONENTS)
|
||||
if r.random() < 0.5:
|
||||
kw["project"] = r.choice(PROJECTS)
|
||||
return lambda led: led.outstanding(**kw)
|
||||
|
||||
fuzz("outstanding", out_op, 120)
|
||||
fuzz("summary", lambda r, d: (lambda led: led.summary()), 50)
|
||||
|
||||
total = n_truth + n_fuzz
|
||||
print(f"parity: {total - len(failures)}/{total} "
|
||||
f"({n_truth} truth + {n_fuzz} fuzz seed 2479)")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity check: construction_patterns_builtin facade on the Rust core
|
||||
vs the Python-pinned oracle (RUNE_CPB_CORE=python). Exercises every public
|
||||
surface branch: BUILTIN_PATTERNS docs, registry revision, get_pattern hit
|
||||
per pattern, unknown-id miss, revision match, revision mismatch (exact
|
||||
exception type + message), revision=None. Deep-copies inputs per case.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
|
||||
import rune.construction_patterns_builtin as cpb # noqa: E402
|
||||
|
||||
PIDS = [p.pattern_id for p in cpb.BUILTIN_PATTERNS]
|
||||
|
||||
cases = []
|
||||
for pid in PIDS:
|
||||
cases.append(("get_pattern", {"pattern_id": pid, "revision": None}))
|
||||
rev = cpb.get_pattern(pid).revision
|
||||
cases.append(("get_pattern", {"pattern_id": pid, "revision": rev}))
|
||||
cases.append(("get_pattern", {"pattern_id": pid, "revision": rev + 1}))
|
||||
cases += [
|
||||
("get_pattern", {"pattern_id": "no-such-pattern", "revision": None}),
|
||||
("get_pattern", {"pattern_id": "", "revision": None}),
|
||||
("registry_revision", {}),
|
||||
]
|
||||
|
||||
|
||||
def run(pin):
|
||||
os.environ["RUNE_CPB_CORE"] = "python" if pin else "auto"
|
||||
out = []
|
||||
for fn, args in cases:
|
||||
try:
|
||||
if fn == "get_pattern":
|
||||
got = cpb.get_pattern(**copy.deepcopy(args))
|
||||
out.append(("ok", None if got is None else got.to_dict()))
|
||||
else:
|
||||
out.append(("ok", cpb.PATTERN_REGISTRY_REVISION))
|
||||
except ValueError as e:
|
||||
out.append(("ValueError", str(e)))
|
||||
return out
|
||||
|
||||
|
||||
def run_rust_docs():
|
||||
"""The Rust core's raw surface (registry docs + revision) directly."""
|
||||
from rune import cpb_loader
|
||||
|
||||
return {
|
||||
"revision": cpb_loader.registry_revision(),
|
||||
"patterns": cpb_loader.patterns(),
|
||||
"kernel": cpb_loader.available(),
|
||||
}
|
||||
|
||||
|
||||
want = run(pin=True)
|
||||
got = run(pin=False)
|
||||
match = sum(1 for w, g in zip(want, got) if w == g)
|
||||
for i, (w, g) in enumerate(zip(want, got)):
|
||||
if w != g:
|
||||
print(f"DIVERGENCE case {i} {cases[i]}:\n python {w}\n rust {g}")
|
||||
|
||||
docs = run_rust_docs()
|
||||
py_docs = [p.to_dict() for p in cpb.BUILTIN_PATTERNS]
|
||||
if docs["patterns"] != py_docs or docs["revision"] != cpb.PATTERN_REGISTRY_REVISION:
|
||||
print(f"DIVERGENCE kernel docs: rust rev={docs['revision']} patterns!=python" )
|
||||
match = match # counted below as failure
|
||||
docs_ok = False
|
||||
else:
|
||||
docs_ok = True
|
||||
|
||||
total = len(cases) + 1
|
||||
matched = match + (1 if docs_ok else 0)
|
||||
print(f"PARITY: {matched}/{total}")
|
||||
sys.exit(0 if matched == total else 1)
|
||||
@@ -107,11 +107,18 @@ def module_to_keys(module_stem: str) -> list[str]:
|
||||
"""Candidate implementation keys for a Python module stem."""
|
||||
kebab = module_stem.replace("_", "-")
|
||||
keys = [kebab, module_stem]
|
||||
# Hyphenated legacy twins (drift-ledger.py vs drift_ledger.py) share ONE
|
||||
# implementation; the underscore form is the crate/dir name.
|
||||
if "-" in module_stem:
|
||||
keys.append(module_stem.replace("-", "_"))
|
||||
# Deep ports and multi-module binaries register aliases:
|
||||
aliases = {
|
||||
"rune_operator": ["operator", "operator-deep", "operator-support", "core-server"],
|
||||
"rune_tui": ["tui"],
|
||||
"rune_operator_cli": ["operator"],
|
||||
# curator was ported to FULL Go (src/go/curator-core, a95c6ce1);
|
||||
# without this alias the audit mislabels it NOT TOUCHED.
|
||||
"curator": ["curator-core"],
|
||||
"memory_store": ["memory-ops", "memory-store-deep"],
|
||||
"warm_router": ["warm-router", "operator-support"],
|
||||
"tool_executor": ["tool-executor", "local-tool-runtime", "tool-protocol", "tool-protocol-rw"],
|
||||
@@ -142,6 +149,7 @@ def module_to_keys(module_stem: str) -> list[str]:
|
||||
"evidence_provenance": ["cognition-provenance", "cognition"],
|
||||
"memory_scoring": ["cognition-scoring", "cognition"],
|
||||
"capability_scoring": ["cognition-capability", "cognition"],
|
||||
"curator": ["curator-core"],
|
||||
"token_counting": ["cognition-token", "cognition"],
|
||||
"query_memory": ["cognition-query", "cognition"],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_abstraction_ladder_truth.py — freeze/replay parity truth for
|
||||
abstraction_ladder (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/abstraction_ladder.py) across every branch — next_stage
|
||||
(all 7 stages / top / unknown / non-string), can_promote (empty item,
|
||||
per-stage threshold miss at exactly threshold-1, the unknown-stage→"top"
|
||||
quirk, falsy provenance variants, contradictions, missing/empty
|
||||
consequence, belief/rule verified_outcome gate, full promote path per
|
||||
stage) and promote (failure verdict + verbatim item, success item
|
||||
mutation stage/sightings reset) — and records (kernel_input, expected)
|
||||
into src/rust/abstraction_ladder/frozen_al_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation ACTIVE (RUNE_AL_CORE unset) and asserts both kernel-level
|
||||
(loader direct) and facade-level (oracle functions delegate) parity
|
||||
against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_AL_CORE=python BEFORE any import.
|
||||
Mutation law: case inputs are deep-copied BEFORE they reach the oracle.
|
||||
SIGHTINGS-INT LAW: sightings are ints only in frozen cases — JSON cannot
|
||||
distinguish int 2 from float 2.0, and the oracle's f-string renders them
|
||||
differently ("2" vs "2.0"), so float sightings are unportable truth.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "abstraction_ladder",
|
||||
"frozen_al_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_AL_CORE"] = "python" # poisoned-truth law (freeze side)
|
||||
else:
|
||||
os.environ.pop("RUNE_AL_CORE", None) # live-parity law (facade delegates)
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import abstraction_ladder as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import al_loader # noqa: E402
|
||||
if not al_loader.available():
|
||||
print("REPLAY ABORT: kernel not built —", al_loader.LIB_PATH)
|
||||
sys.exit(2)
|
||||
|
||||
cases = []
|
||||
|
||||
|
||||
def case(name, fn, kernel_input, expected):
|
||||
cases.append({
|
||||
"name": name,
|
||||
"fn": fn,
|
||||
"input": kernel_input,
|
||||
"ops": [],
|
||||
"expected": expected,
|
||||
})
|
||||
|
||||
|
||||
def complete(stage, sightings=None):
|
||||
it = {"stage": stage, "sightings": oracle_mod.THRESHOLDS[stage],
|
||||
"provenance": "witnessed", "contradictions": [],
|
||||
"consequence": "changes behaviour", "verified_outcome": True}
|
||||
if sightings is not None:
|
||||
it["sightings"] = sightings
|
||||
return it
|
||||
|
||||
|
||||
# ── next_stage: every stage, top, unknown, non-string ──────────────────────
|
||||
for i, stage in enumerate(oracle_mod.STAGES):
|
||||
want = oracle_mod.STAGES[i + 1] if i + 1 < len(oracle_mod.STAGES) else None
|
||||
case(f"next_stage:{stage}", "next_stage", {"stage": stage}, want)
|
||||
case("next_stage:unknown", "next_stage", {"stage": "gossip"}, None)
|
||||
case("next_stage:empty", "next_stage", {"stage": ""}, None)
|
||||
case("next_stage:missing", "next_stage", {}, None)
|
||||
case("next_stage:non_string", "next_stage", {"stage": 7}, None)
|
||||
case("next_stage:prefix_like", "next_stage", {"stage": "episodex"}, None)
|
||||
|
||||
# ── can_promote: threshold branch per stage (one below) ────────────────────
|
||||
for stage in oracle_mod.STAGES:
|
||||
thr = oracle_mod.THRESHOLDS[stage]
|
||||
item = {"stage": stage, "sightings": thr - 1, "provenance": "witnessed",
|
||||
"contradictions": [], "consequence": "changes behaviour",
|
||||
"verified_outcome": True}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:below_threshold:{stage}", "can_promote",
|
||||
{"item": item}, want)
|
||||
|
||||
# exact-threshold boundary for every stage (full promote path)
|
||||
for stage in oracle_mod.STAGES:
|
||||
item = complete(stage)
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:at_threshold:{stage}", "can_promote",
|
||||
{"item": item}, want)
|
||||
|
||||
# empty item — defaults: stage episode, sightings 0
|
||||
want = oracle_mod.can_promote({})
|
||||
case("can_promote:empty_item", "can_promote", {"item": {}}, want)
|
||||
|
||||
# missing sightings only
|
||||
want = oracle_mod.can_promote({"stage": "fact", "provenance": "witnessed",
|
||||
"consequence": "c"})
|
||||
case("can_promote:default_sightings", "can_promote",
|
||||
{"item": {"stage": "fact", "provenance": "witnessed",
|
||||
"consequence": "c"}},
|
||||
want)
|
||||
|
||||
# non-string / null stage keys: item.get hands them to STAGES.index,
|
||||
# which raises ValueError -> next_stage None -> "top" quirk
|
||||
for key, bad_stage in (("non_string", 7), ("null", None)):
|
||||
item = {"stage": bad_stage, "sightings": 9, "provenance": "witnessed",
|
||||
"consequence": "c"}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:stage_{key}_quirk", "can_promote", {"item": item},
|
||||
want)
|
||||
|
||||
# the unknown-stage quirk: next_stage(None result) collapses to "top"
|
||||
want = oracle_mod.can_promote({"stage": "gossip", "sightings": 99,
|
||||
"provenance": "witnessed",
|
||||
"consequence": "c"})
|
||||
case("can_promote:unknown_stage_quirk", "can_promote",
|
||||
{"item": {"stage": "gossip", "sightings": 99,
|
||||
"provenance": "witnessed", "consequence": "c"}},
|
||||
want)
|
||||
|
||||
# falsy provenance variants: missing, empty string, empty list, 0, False
|
||||
for key, prov in (("missing", None), ("empty_str", ""),
|
||||
("empty_list", []), ("zero", 0), ("false", False)):
|
||||
item = {"stage": "observation", "sightings": 5,
|
||||
"contradictions": [], "consequence": "c"}
|
||||
if prov is not None:
|
||||
item["provenance"] = prov
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:provenance_{key}", "can_promote", {"item": item}, want)
|
||||
|
||||
# truthy provenance variants survive the gate (promote path)
|
||||
for key, prov in (("str", "log"), ("list", ["log"]), ("one", 1),
|
||||
("true", True)):
|
||||
item = {"stage": "episode", "sightings": 1, "provenance": prov,
|
||||
"contradictions": [], "consequence": "c"}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:provenance_{key}_passes", "can_promote",
|
||||
{"item": item}, want)
|
||||
|
||||
# contradictions: truthy list, truthy string, dict; falsy empty list /
|
||||
# None / False pass
|
||||
for key, contra, want_promote in (
|
||||
("list", ["c1"], False), ("str", "disputed", False),
|
||||
("dict", {"a": 1}, False), ("empty_list", [], True),
|
||||
("none", None, True), ("false", False, True)):
|
||||
item = {"stage": "episode", "sightings": 1, "provenance": "log",
|
||||
"consequence": "c"}
|
||||
if contra is not None:
|
||||
item["contradictions"] = contra
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:contradictions_{key}", "can_promote",
|
||||
{"item": item}, want)
|
||||
|
||||
# consequence: missing / empty string / empty list / 0 fail; non-empty pass
|
||||
for key, cons in (("missing", None), ("empty_str", ""),
|
||||
("empty_list", []), ("zero", 0), ("str", "moves"),
|
||||
("list", ["moves"])):
|
||||
item = {"stage": "episode", "sightings": 1, "provenance": "log",
|
||||
"contradictions": []}
|
||||
if cons is not None:
|
||||
item["consequence"] = cons
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:consequence_{key}", "can_promote", {"item": item},
|
||||
want)
|
||||
|
||||
# belief/rule verified_outcome gate: missing, False, 0 fail; True passes;
|
||||
# concept needs NO verified outcome
|
||||
for stage in ("belief", "rule"):
|
||||
for key, vo in (("missing", None), ("false", False), ("zero", 0)):
|
||||
item = {"stage": stage, "sightings": 9, "provenance": "log",
|
||||
"contradictions": [], "consequence": "c"}
|
||||
if vo is not None:
|
||||
item["verified_outcome"] = vo
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:{stage}_vo_{key}", "can_promote",
|
||||
{"item": item}, want)
|
||||
item = {"stage": stage, "sightings": 9, "provenance": "log",
|
||||
"contradictions": [], "consequence": "c",
|
||||
"verified_outcome": True}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case(f"can_promote:{stage}_vo_true", "can_promote", {"item": item}, want)
|
||||
|
||||
item = {"stage": "concept", "sightings": 3, "provenance": "log",
|
||||
"contradictions": [], "consequence": "c",
|
||||
"verified_outcome": False}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case("can_promote:concept_ignores_vo", "can_promote", {"item": item}, want)
|
||||
|
||||
# gate order: threshold beats provenance beats contradictions beats
|
||||
# consequence beats verified_outcome
|
||||
item = {"stage": "belief", "sightings": 1}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case("can_promote:gate_order_threshold_first", "can_promote",
|
||||
{"item": item}, want)
|
||||
item = {"stage": "belief", "sightings": 9, "contradictions": ["x"],
|
||||
"consequence": "c", "verified_outcome": True}
|
||||
want = oracle_mod.can_promote(copy.deepcopy(item))
|
||||
case("can_promote:gate_order_provenance_second", "can_promote",
|
||||
{"item": item}, want)
|
||||
|
||||
# ── promote: failure keeps the item verbatim ───────────────────────────────
|
||||
for name, item in (
|
||||
("empty", {}),
|
||||
("below", {"stage": "observation", "sightings": 2,
|
||||
"provenance": "log", "consequence": "c"}),
|
||||
("top", {"stage": "skill", "sightings": 9, "provenance": "log",
|
||||
"consequence": "c"}),
|
||||
("unknown", {"stage": "gossip", "sightings": 9}),
|
||||
("no_consequence", {"stage": "fact", "sightings": 4,
|
||||
"provenance": "log"}),
|
||||
("belief_no_vo", {"stage": "belief", "sightings": 9,
|
||||
"provenance": "log", "consequence": "c"})):
|
||||
want = oracle_mod.promote(copy.deepcopy(item))
|
||||
case(f"promote:refusal_{name}", "promote", {"item": item}, want)
|
||||
|
||||
# ── promote: success mutates stage + resets sightings ──────────────────────
|
||||
for stage in ("episode", "fact", "observation", "concept", "belief", "rule"):
|
||||
item = complete(stage)
|
||||
want = oracle_mod.promote(copy.deepcopy(item))
|
||||
case(f"promote:success_{stage}", "promote", {"item": item}, want)
|
||||
|
||||
# extra fields preserved through the copy; only stage/sightings touched
|
||||
item = {"stage": "rule", "sightings": 5, "provenance": "log",
|
||||
"contradictions": [], "consequence": "c", "verified_outcome": True,
|
||||
"id": "x9", "tags": ["a", "b"], "nested": {"k": [1, 2]}}
|
||||
want = oracle_mod.promote(copy.deepcopy(item))
|
||||
case("promote:extra_fields_preserved", "promote", {"item": item}, want)
|
||||
|
||||
# ── freeze / replay ─────────────────────────────────────────────────────────
|
||||
|
||||
def kernel_call(fn, kernel_input):
|
||||
if fn == "next_stage":
|
||||
return al_loader.next_stage(kernel_input.get("stage"))
|
||||
if fn == "can_promote":
|
||||
return al_loader.can_promote(kernel_input["item"])
|
||||
return al_loader.promote(kernel_input["item"])
|
||||
|
||||
|
||||
def facade_call(fn, kernel_input):
|
||||
if fn == "next_stage":
|
||||
return oracle_mod.next_stage(kernel_input.get("stage"))
|
||||
if fn == "can_promote":
|
||||
return oracle_mod.can_promote(kernel_input["item"])
|
||||
return oracle_mod.promote(kernel_input["item"])
|
||||
|
||||
|
||||
if not REPLAY:
|
||||
for c in cases:
|
||||
got = facade_call(c["fn"], copy.deepcopy(c["input"]))
|
||||
assert got == c["expected"], (
|
||||
f"self-check failed on {c['name']}: {got!r} != {c['expected']!r}")
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/abstraction_ladder.py",
|
||||
"pin": "RUNE_AL_CORE=python",
|
||||
"note": "Frozen via scripts/gen_abstraction_ladder_truth.py "
|
||||
"against the live Python oracle. Battery embeds this "
|
||||
"VERBATIM. SIGHTINGS-INT LAW: sightings are ints only "
|
||||
"(JSON cannot distinguish int 2 from float 2.0 and the "
|
||||
"oracle's f-string renders them differently).",
|
||||
"cases": cases,
|
||||
}
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1)
|
||||
f.write("\n")
|
||||
print(f"FROZEN {len(cases)} cases -> {TRUTH_PATH}")
|
||||
else:
|
||||
fails = []
|
||||
for c in cases:
|
||||
want = c["expected"]
|
||||
kg = kernel_call(c["fn"], copy.deepcopy(c["input"]))
|
||||
if kg != want:
|
||||
fails.append(f"KERNEL {c['name']}: {kg!r} != {want!r}")
|
||||
fg = facade_call(c["fn"], copy.deepcopy(c["input"]))
|
||||
if fg != want:
|
||||
fails.append(f"FACADE {c['name']}: {fg!r} != {want!r}")
|
||||
if fails:
|
||||
print(f"REPLAY FAILED {len(fails)}/{len(cases)}:")
|
||||
for f_ in fails:
|
||||
print(" ", f_)
|
||||
sys.exit(1)
|
||||
print(f"REPLAY OK {len(cases)}/{len(cases)} (kernel + facade)")
|
||||
@@ -0,0 +1,444 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the parity truth for the behaviour_ledger Rust port (dichotomy
|
||||
wave #2479 tranche).
|
||||
|
||||
Runs the LIVE oracle (src/rune/behaviour_ledger.py — the Phase G shim
|
||||
serving the evidence_provenance `bl_*` core, retired as the Python
|
||||
oracle by this port) across EVERY public function and EVERY branch:
|
||||
define (minimal/full/prefixed id/duplicate/unicode), get (hit both id
|
||||
forms/miss/empty), record_auto_verification (pass/fail/unknown/200-char
|
||||
detail truncation/reverify-confirmed), record_real_world (user
|
||||
confirmation/other-confirmer/fail/unknown), accept_limitation (ok/
|
||||
custom approver/unknown), invalidate_for_change (dependency overlap
|
||||
with the default sorted-list-repr reason / owner hit / no overlap →
|
||||
preserved / custom reasons / reasons key miss / auto-verified-only
|
||||
skip / mixed multi-behaviour), retest_history (populated generations/
|
||||
unknown/empty), outstanding (mixed states with the TEST_FAILED-first
|
||||
sort / component filter / project filter / combined / excluded
|
||||
states), summary (empty/mixed integer counts).
|
||||
|
||||
The clock is pinned (bl._now) so truth is deterministic and
|
||||
regenerable byte-for-byte. Case input docs are deep-copied BEFORE the
|
||||
oracle runs (poisoned-truth law). Output:
|
||||
src/rust/behaviour_ledger/src/frozen_bl_truth.json — embedded VERBATIM
|
||||
by the cfg(test) branch battery (include_str!).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import behaviour_ledger as bl # noqa: E402
|
||||
|
||||
TRUTH_PATH = Path(__file__).resolve().parents[1] / (
|
||||
"src/rust/behaviour_ledger/src/frozen_bl_truth.json")
|
||||
|
||||
|
||||
class Clock:
|
||||
"""Pinned clock: %Y-%m-%dT%H:%M:%SZ stamps incrementing by second
|
||||
(the oracle's _now format), reset per case."""
|
||||
def __init__(self):
|
||||
self.n = 0
|
||||
|
||||
def now(self) -> str:
|
||||
self.n += 1
|
||||
return f"2026-10-11T23:00:{self.n:02d}Z"
|
||||
|
||||
|
||||
CLOCK = Clock()
|
||||
bl._now = CLOCK.now # pin the facade's clock (the kernel takes `now` in)
|
||||
|
||||
|
||||
def fresh() -> bl.BehaviourLedger:
|
||||
CLOCK.n = 0
|
||||
tmp = tempfile.mkdtemp(prefix="bl_truth_")
|
||||
return bl.BehaviourLedger(Path(tmp) / "store")
|
||||
|
||||
|
||||
def envelope_ok(value) -> dict:
|
||||
return {"ok": True, "value": value}
|
||||
|
||||
|
||||
def envelope_err(msg: str) -> dict:
|
||||
return {"ok": False, "err": msg}
|
||||
|
||||
|
||||
CASES: dict[str, list] = {}
|
||||
|
||||
|
||||
def add(group: str, case: dict) -> None:
|
||||
CASES.setdefault(group, []).append(case)
|
||||
|
||||
|
||||
def snap(led: bl.BehaviourLedger) -> dict:
|
||||
return copy.deepcopy(led.doc)
|
||||
|
||||
|
||||
def next_now() -> str:
|
||||
"""The stamp the NEXT pinned _now() call will return."""
|
||||
return f"2026-10-11T23:00:{CLOCK.n + 1:02d}Z"
|
||||
|
||||
|
||||
def run_mutation(group: str, name: str, led: bl.BehaviourLedger,
|
||||
params: dict, op) -> None:
|
||||
"""Run one mutating op (kwargs only); record {input: {doc, now,
|
||||
**params}, expected: {ok, value: {doc, rec | recs}}} or the
|
||||
ValueError envelope. The op under test stamps via the pinned clock;
|
||||
its stamp is recorded in the input (no-clock law — the kernel takes
|
||||
`now` in)."""
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
try:
|
||||
out = op(**params)
|
||||
except ValueError as e:
|
||||
add(group, {"name": name,
|
||||
"input": {"doc": doc_in, "now": stamp, **params},
|
||||
"expected": envelope_err(str(e))})
|
||||
return
|
||||
value_key = "recs" if isinstance(out, list) else "rec"
|
||||
add(group, {"name": name, "input": {"doc": doc_in, "now": stamp,
|
||||
**params},
|
||||
"expected": envelope_ok({"doc": led.doc,
|
||||
value_key: out})})
|
||||
|
||||
|
||||
def seed_confirmed(led: bl.BehaviourLedger, bid: str, owner: str,
|
||||
deps: list[str]) -> None:
|
||||
led.define(behaviour_id=bid, owning_project="demo",
|
||||
intended_behaviour=f"{bid} behaves",
|
||||
owner_component=owner, dependencies=deps)
|
||||
led.record_auto_verification(bid, passed=True, generation="gen-1")
|
||||
led.record_real_world(bid, passed=True, detail="user confirmed",
|
||||
generation="gen-1")
|
||||
|
||||
|
||||
# ── define ────────────────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
run_mutation("define", "minimal", led, {
|
||||
"behaviour_id": "MIN-1", "owning_project": "demo",
|
||||
"intended_behaviour": "does the thing"},
|
||||
led.define)
|
||||
led = fresh()
|
||||
run_mutation("define", "full_kwargs", led, {
|
||||
"behaviour_id": "FULL-1", "owning_project": "demo",
|
||||
"intended_behaviour": "bridge completes events",
|
||||
"origin_intent": "issue #19F", "pass_criteria": "events done",
|
||||
"fail_criteria": "events dropped", "procedure": "run the suite",
|
||||
"owner_component": "component:event_bridge",
|
||||
"dependencies": ["component:event_bridge",
|
||||
"interface:event_completion"],
|
||||
"automated_ref": "tests/test_bridge.py", "environment": "ci"},
|
||||
led.define)
|
||||
led = fresh()
|
||||
run_mutation("define", "prefixed_id_preserved", led, {
|
||||
"behaviour_id": "behaviour:PRE-1", "owning_project": "demo",
|
||||
"intended_behaviour": "already prefixed"},
|
||||
led.define)
|
||||
led = fresh()
|
||||
run_mutation("define", "unicode_fields", led, {
|
||||
"behaviour_id": "UNI-1", "owning_project": "démø",
|
||||
"intended_behaviour": "completes événements — ✓ ünïcode",
|
||||
"origin_intent": "»quoted«", "pass_criteria": "✓ pass",
|
||||
"owner_component": "component:kafé",
|
||||
"dependencies": ["component:kafé", "interface:ünï"],
|
||||
"environment": "prod-ü"},
|
||||
led.define)
|
||||
led = fresh()
|
||||
led.define(behaviour_id="DUP-1", owning_project="demo",
|
||||
intended_behaviour="first")
|
||||
run_mutation("define", "duplicate_err", led, {
|
||||
"behaviour_id": "DUP-1", "owning_project": "demo",
|
||||
"intended_behaviour": "second"},
|
||||
led.define)
|
||||
led = fresh()
|
||||
run_mutation("define", "none_dependencies", led, {
|
||||
"behaviour_id": "NODEP-1", "owning_project": "demo",
|
||||
"intended_behaviour": "no deps given", "dependencies": None},
|
||||
led.define)
|
||||
|
||||
# ── get ───────────────────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
led.define(behaviour_id="GET-1", owning_project="demo",
|
||||
intended_behaviour="gettable")
|
||||
rec = led.get("GET-1")
|
||||
add("get", {"name": "hit_bare_id",
|
||||
"input": {"doc": snap(led), "behaviour_id": "GET-1"},
|
||||
"expected": envelope_ok(rec)})
|
||||
add("get", {"name": "hit_prefixed_id",
|
||||
"input": {"doc": snap(led), "behaviour_id": "behaviour:GET-1"},
|
||||
"expected": envelope_ok(rec)})
|
||||
add("get", {"name": "miss",
|
||||
"input": {"doc": snap(led), "behaviour_id": "behaviour:NOPE"},
|
||||
"expected": envelope_ok(None)})
|
||||
add("get", {"name": "empty_doc",
|
||||
"input": {"doc": snap(fresh()), "behaviour_id": "GET-1"},
|
||||
"expected": envelope_ok(None)})
|
||||
|
||||
# ── record_auto_verification ──────────────────────────────────────────────
|
||||
led = fresh()
|
||||
led.define(behaviour_id="AUTO-1", owning_project="demo",
|
||||
intended_behaviour="auto verifiable")
|
||||
run_mutation("record_auto_verification", "pass_to_auto_verified", led, {
|
||||
"behaviour_id": "AUTO-1", "passed": True, "detail": "suite green",
|
||||
"generation": "gen-1"},
|
||||
led.record_auto_verification)
|
||||
led = fresh()
|
||||
led.define(behaviour_id="AUTO-2", owning_project="demo",
|
||||
intended_behaviour="fails")
|
||||
run_mutation("record_auto_verification", "fail_to_test_failed", led, {
|
||||
"behaviour_id": "AUTO-2", "passed": False, "detail": "assert crashed",
|
||||
"generation": "gen-1"},
|
||||
led.record_auto_verification)
|
||||
led = fresh()
|
||||
run_mutation("record_auto_verification", "unknown_behaviour_err", led, {
|
||||
"behaviour_id": "GHOST", "passed": True},
|
||||
led.record_auto_verification)
|
||||
led = fresh()
|
||||
led.define(behaviour_id="AUTO-3", owning_project="demo",
|
||||
intended_behaviour="long detail")
|
||||
run_mutation("record_auto_verification", "detail_truncated_200", led, {
|
||||
"behaviour_id": "AUTO-3", "passed": True, "detail": "x" * 250},
|
||||
led.record_auto_verification)
|
||||
led = fresh()
|
||||
seed_confirmed(led, "AUTO-4", "component:c", ["component:c"])
|
||||
run_mutation("record_auto_verification", "reverify_confirmed", led, {
|
||||
"behaviour_id": "AUTO-4", "passed": True,
|
||||
"detail": "regression suite", "generation": "gen-2"},
|
||||
led.record_auto_verification)
|
||||
|
||||
# ── record_real_world ─────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
led.define(behaviour_id="RW-1", owning_project="demo",
|
||||
intended_behaviour="user confirmed")
|
||||
run_mutation("record_real_world", "user_pass_confirmed", led, {
|
||||
"behaviour_id": "RW-1", "passed": True, "detail": "manual test ok",
|
||||
"generation": "gen-1", "confirmed_by": "user"},
|
||||
led.record_real_world)
|
||||
led = fresh()
|
||||
led.define(behaviour_id="RW-2", owning_project="demo",
|
||||
intended_behaviour="robot verified")
|
||||
run_mutation("record_real_world", "other_confirmer_real_world_passed", led, {
|
||||
"behaviour_id": "RW-2", "passed": True, "detail": "bot checked",
|
||||
"generation": "gen-1", "confirmed_by": "ci-robot"},
|
||||
led.record_real_world)
|
||||
led = fresh()
|
||||
led.define(behaviour_id="RW-3", owning_project="demo",
|
||||
intended_behaviour="fails in the field")
|
||||
run_mutation("record_real_world", "fail_to_test_failed", led, {
|
||||
"behaviour_id": "RW-3", "passed": False, "detail": "user saw breakage",
|
||||
"generation": "gen-1"},
|
||||
led.record_real_world)
|
||||
led = fresh()
|
||||
run_mutation("record_real_world", "unknown_behaviour_err", led, {
|
||||
"behaviour_id": "RW-GHOST", "passed": True},
|
||||
led.record_real_world)
|
||||
|
||||
# ── accept_limitation ─────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
led.define(behaviour_id="LIM-1", owning_project="demo",
|
||||
intended_behaviour="known gap")
|
||||
run_mutation("accept_limitation", "ok", led, {
|
||||
"behaviour_id": "LIM-1", "note": "edge case accepted for v1",
|
||||
"approved_by": "user"},
|
||||
led.accept_limitation)
|
||||
led = fresh()
|
||||
led.define(behaviour_id="LIM-2", owning_project="demo",
|
||||
intended_behaviour="approved gap")
|
||||
run_mutation("accept_limitation", "custom_approver", led, {
|
||||
"behaviour_id": "LIM-2", "note": "signed off by maintainer",
|
||||
"approved_by": "nick"},
|
||||
led.accept_limitation)
|
||||
led = fresh()
|
||||
run_mutation("accept_limitation", "unknown_behaviour_err", led, {
|
||||
"behaviour_id": "LIM-GHOST", "note": "x"},
|
||||
led.accept_limitation)
|
||||
|
||||
# ── invalidate_for_change ─────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
seed_confirmed(led, "INV-1", "component:bridge",
|
||||
["component:bridge", "interface:completion"])
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
|
||||
generation="gen-2")
|
||||
add("invalidate_for_change", {
|
||||
"name": "dep_overlap_default_reason",
|
||||
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
|
||||
"generation": "gen-2", "reasons": None},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
led = fresh()
|
||||
seed_confirmed(led, "INV-2", "component:bridge", [])
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
|
||||
generation="gen-2")
|
||||
add("invalidate_for_change", {
|
||||
"name": "owner_hit_only",
|
||||
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
|
||||
"generation": "gen-2", "reasons": None},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
led = fresh()
|
||||
seed_confirmed(led, "INV-3", "component:unrelated", ["component:unrelated"])
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
|
||||
generation="gen-2")
|
||||
add("invalidate_for_change", {
|
||||
"name": "no_overlap_preserved",
|
||||
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
|
||||
"generation": "gen-2", "reasons": None},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
led = fresh()
|
||||
seed_confirmed(led, "INV-4", "component:bridge",
|
||||
["component:bridge", "interface:completion"])
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(
|
||||
changed_entities={"component:bridge", "interface:completion"},
|
||||
generation="gen-2",
|
||||
reasons={"behaviour:INV-4": "bridge rewrite (issue #42)"})
|
||||
add("invalidate_for_change", {
|
||||
"name": "custom_reason",
|
||||
"input": {"doc": doc_in, "now": stamp,
|
||||
"changed_entities": sorted({"component:bridge",
|
||||
"interface:completion"}),
|
||||
"generation": "gen-2",
|
||||
"reasons": {"behaviour:INV-4": "bridge rewrite (issue #42)"}},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
led = fresh()
|
||||
led.define(behaviour_id="INV-5", owning_project="demo",
|
||||
intended_behaviour="never confirmed")
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
|
||||
generation="gen-2")
|
||||
add("invalidate_for_change", {
|
||||
"name": "unconfirmed_skipped",
|
||||
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
|
||||
"generation": "gen-2", "reasons": None},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
led = fresh()
|
||||
seed_confirmed(led, "INV-6", "component:bridge",
|
||||
["component:bridge", "interface:completion"])
|
||||
seed_confirmed(led, "INV-7", "component:consumer",
|
||||
["component:consumer", "interface:completion"])
|
||||
seed_confirmed(led, "INV-8", "component:unrelated", ["component:unrelated"])
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(
|
||||
changed_entities={"interface:completion"}, generation="gen-2")
|
||||
add("invalidate_for_change", {
|
||||
"name": "mixed_multi_behaviour",
|
||||
"input": {"doc": doc_in, "now": stamp,
|
||||
"changed_entities": sorted({"interface:completion"}),
|
||||
"generation": "gen-2", "reasons": None},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
led = fresh()
|
||||
seed_confirmed(led, "INV-9", "component:bridge", ["component:bridge"])
|
||||
doc_in = snap(led)
|
||||
stamp = next_now()
|
||||
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
|
||||
generation="gen-2",
|
||||
reasons={"behaviour:NOPE": "x"})
|
||||
add("invalidate_for_change", {
|
||||
"name": "reasons_key_miss_falls_back",
|
||||
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
|
||||
"generation": "gen-2", "reasons": {"behaviour:NOPE": "x"}},
|
||||
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
|
||||
|
||||
# ── retest_history ────────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
seed_confirmed(led, "HIST-1", "component:bridge", ["component:bridge"])
|
||||
led.invalidate_for_change(changed_entities={"component:bridge"},
|
||||
generation="gen-2")
|
||||
hist = led.retest_history("HIST-1")
|
||||
add("retest_history", {"name": "populated_generations",
|
||||
"input": {"doc": snap(led),
|
||||
"behaviour_id": "HIST-1"},
|
||||
"expected": envelope_ok(hist)})
|
||||
add("retest_history", {"name": "unknown_behaviour_empty",
|
||||
"input": {"doc": snap(led),
|
||||
"behaviour_id": "behaviour:GHOST"},
|
||||
"expected": envelope_ok([])})
|
||||
led = fresh()
|
||||
led.define(behaviour_id="HIST-2", owning_project="demo",
|
||||
intended_behaviour="no events yet")
|
||||
add("retest_history", {"name": "empty_history",
|
||||
"input": {"doc": snap(led),
|
||||
"behaviour_id": "HIST-2"},
|
||||
"expected": envelope_ok([])})
|
||||
|
||||
# ── outstanding ───────────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
add("outstanding", {"name": "empty_doc",
|
||||
"input": {"doc": snap(led), "component": "",
|
||||
"project": ""},
|
||||
"expected": envelope_ok(led.outstanding())})
|
||||
led = fresh()
|
||||
led.define(behaviour_id="OUT-1", owning_project="demo",
|
||||
intended_behaviour="failed", owner_component="component:a")
|
||||
led.record_auto_verification("OUT-1", passed=False, generation="g")
|
||||
led.define(behaviour_id="OUT-2", owning_project="demo",
|
||||
intended_behaviour="unvalidated", owner_component="component:b")
|
||||
led.define(behaviour_id="OUT-3", owning_project="other",
|
||||
intended_behaviour="other project", owner_component="component:a")
|
||||
led.define(behaviour_id="OUT-4", owning_project="demo",
|
||||
intended_behaviour="confirmed", owner_component="component:b")
|
||||
led.record_real_world("OUT-4", passed=True, generation="g")
|
||||
led.define(behaviour_id="OUT-5", owning_project="demo",
|
||||
intended_behaviour="accepted", owner_component="component:c")
|
||||
led.accept_limitation("OUT-5", note="known gap")
|
||||
led.define(behaviour_id="OUT-6", owning_project="demo",
|
||||
intended_behaviour="n/a", owner_component="component:c")
|
||||
led.doc["behaviours"]["behaviour:OUT-6"]["state"] = "NOT_APPLICABLE"
|
||||
doc = snap(led)
|
||||
add("outstanding", {"name": "mixed_sort_failed_first",
|
||||
"input": {"doc": doc, "component": "", "project": ""},
|
||||
"expected": envelope_ok(led.outstanding())})
|
||||
add("outstanding", {"name": "component_filter",
|
||||
"input": {"doc": doc, "component": "component:a",
|
||||
"project": ""},
|
||||
"expected": envelope_ok(
|
||||
led.outstanding(component="component:a"))})
|
||||
add("outstanding", {"name": "project_filter",
|
||||
"input": {"doc": doc, "component": "",
|
||||
"project": "other"},
|
||||
"expected": envelope_ok(led.outstanding(project="other"))})
|
||||
add("outstanding", {"name": "combined_filters",
|
||||
"input": {"doc": doc, "component": "component:a",
|
||||
"project": "demo"},
|
||||
"expected": envelope_ok(
|
||||
led.outstanding(component="component:a",
|
||||
project="demo"))})
|
||||
|
||||
# ── summary ───────────────────────────────────────────────────────────────
|
||||
led = fresh()
|
||||
add("summary", {"name": "empty_doc", "input": {"doc": snap(led)},
|
||||
"expected": envelope_ok(led.summary())})
|
||||
led = fresh()
|
||||
led.define(behaviour_id="SUM-1", owning_project="demo",
|
||||
intended_behaviour="confirmed")
|
||||
led.record_real_world("SUM-1", passed=True, generation="g")
|
||||
led.define(behaviour_id="SUM-2", owning_project="demo",
|
||||
intended_behaviour="failed")
|
||||
led.record_auto_verification("SUM-2", passed=False, generation="g")
|
||||
led.define(behaviour_id="SUM-3", owning_project="demo",
|
||||
intended_behaviour="unvalidated")
|
||||
led.define(behaviour_id="SUM-4", owning_project="demo",
|
||||
intended_behaviour="confirmed too")
|
||||
led.record_real_world("SUM-4", passed=True, generation="g")
|
||||
add("summary", {"name": "mixed_integer_counts",
|
||||
"input": {"doc": snap(led)},
|
||||
"expected": envelope_ok(led.summary())})
|
||||
|
||||
truth = {"frozen": "2026-10-11T23:00:00Z clock, oracle = live "
|
||||
"src/rune/behaviour_ledger.py shim (ep bl_* core)",
|
||||
"cases": CASES}
|
||||
TRUTH_PATH.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
||||
n = sum(len(v) for v in CASES.values())
|
||||
print(f"frozen {n} cases across {len(CASES)} groups -> {TRUTH_PATH}")
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_construction_patterns_builtin_truth.py — dichotomy wave #2479
|
||||
(construction_patterns_builtin FULL-Rust batch).
|
||||
|
||||
Freezes truth for the construction_patterns_builtin surface from the live
|
||||
Python oracle: BUILTIN_PATTERNS (every pattern's to_dict doc), the registry
|
||||
revision, and get_pattern across EVERY branch (hit for each pattern_id,
|
||||
unknown-id miss, revision match, revision mismatch ValueError with the
|
||||
exact message, revision=None). Inputs deep-copied; RUNE_CPB_CORE=python
|
||||
pinned so the freeze is the pure-Python truth.
|
||||
|
||||
Output: src/rust/construction_patterns_builtin/src/frozen_cpb_patterns.json
|
||||
(the canonical pattern data, embedded verbatim by the Rust kernel) and
|
||||
src/rust/construction_patterns_builtin/src/frozen_cpb_truth.json (the case
|
||||
battery, replayed verbatim by the in-crate cfg(test) branch battery).
|
||||
Never hand-transcribe: every `want` comes from the live oracle run below.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
|
||||
os.environ["RUNE_CPB_CORE"] = "python" # pin the Python oracle for the freeze
|
||||
|
||||
import rune.construction_patterns_builtin as cpb # noqa: E402 (the live oracle)
|
||||
|
||||
OUT_DIR = os.path.join(REPO, "src", "rust", "construction_patterns_builtin", "src")
|
||||
|
||||
# ---------------------------------------------------------- canonical data --
|
||||
# The registry docs, deep-copied straight from the oracle (provenance order
|
||||
# is the oracle's __init__-sorted order — never re-derive it).
|
||||
pattern_docs = [copy.deepcopy(p.to_dict()) for p in cpb.BUILTIN_PATTERNS]
|
||||
pattern_ids = [p.pattern_id for p in cpb.BUILTIN_PATTERNS]
|
||||
|
||||
with open(os.path.join(OUT_DIR, "frozen_cpb_patterns.json"), "w") as f:
|
||||
json.dump({"revision": cpb.PATTERN_REGISTRY_REVISION, "patterns": pattern_docs}, f, indent=2)
|
||||
f.write("\n")
|
||||
print(f"frozen_cpb_patterns.json: {len(pattern_docs)} patterns, "
|
||||
f"revision={cpb.PATTERN_REGISTRY_REVISION}")
|
||||
|
||||
# ------------------------------------------------------------------ cases ---
|
||||
cases = []
|
||||
|
||||
|
||||
def add(fn, name, input_, want):
|
||||
cases.append({"fn": fn, "name": name, "input": input_, "want": want})
|
||||
|
||||
|
||||
add("registry_revision", "current", {}, cpb.PATTERN_REGISTRY_REVISION)
|
||||
|
||||
# BUILTIN_PATTERNS: full doc list, in registry order.
|
||||
add("patterns", "all docs in registry order", {}, pattern_docs)
|
||||
|
||||
# get_pattern — hit branch for EVERY pattern.
|
||||
for pid in pattern_ids:
|
||||
got = cpb.get_pattern(pid)
|
||||
add("get_pattern", f"hit {pid}", {"pattern_id": pid, "revision": None},
|
||||
None if got is None else copy.deepcopy(got.to_dict()))
|
||||
|
||||
# get_pattern — miss branch.
|
||||
add("get_pattern", "unknown id -> None",
|
||||
{"pattern_id": "no-such-pattern", "revision": None}, None)
|
||||
add("get_pattern", "empty id -> None", {"pattern_id": "", "revision": None}, None)
|
||||
# Miss is decided BEFORE the revision check (oracle order).
|
||||
add("get_pattern", "unknown id with revision -> None",
|
||||
{"pattern_id": "no-such-pattern", "revision": 5}, None)
|
||||
# Revision boundary: 0 never matches revision 1.
|
||||
try:
|
||||
cpb.get_pattern(pattern_ids[0], revision=0)
|
||||
want = None
|
||||
except ValueError as e:
|
||||
want = {"raises": "ValueError", "message": str(e)}
|
||||
add("get_pattern", "revision zero boundary",
|
||||
{"pattern_id": pattern_ids[0], "revision": 0}, want)
|
||||
|
||||
# get_pattern — revision branches: match passes through, mismatch raises.
|
||||
for pid in pattern_ids:
|
||||
p = cpb.get_pattern(pid)
|
||||
got = cpb.get_pattern(pid, revision=p.revision)
|
||||
add("get_pattern", f"revision match {pid}",
|
||||
{"pattern_id": pid, "revision": p.revision}, copy.deepcopy(got.to_dict()))
|
||||
try:
|
||||
cpb.get_pattern(pid, revision=p.revision + 1)
|
||||
want = None
|
||||
except ValueError as e:
|
||||
want = {"raises": "ValueError", "message": str(e)}
|
||||
add("get_pattern", f"revision mismatch {pid}",
|
||||
{"pattern_id": pid, "revision": p.revision + 1}, want)
|
||||
|
||||
# get_pattern — revision=None explicit (the default path).
|
||||
got = cpb.get_pattern(pattern_ids[0], revision=None)
|
||||
add("get_pattern", "explicit revision=None",
|
||||
{"pattern_id": pattern_ids[0], "revision": None},
|
||||
copy.deepcopy(got.to_dict()))
|
||||
|
||||
out = os.path.join(OUT_DIR, "frozen_cpb_truth.json")
|
||||
with open(out, "w") as f:
|
||||
json.dump({"frozen_from": "live oracle construction_patterns_builtin.py "
|
||||
"(RUNE_CPB_CORE=python)",
|
||||
"cases": cases}, f, indent=2)
|
||||
f.write("\n")
|
||||
print(f"frozen_cpb_truth.json: {len(cases)} cases")
|
||||
@@ -0,0 +1,710 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze drift_ledger oracle truth for the Rust port battery (dichotomy
|
||||
wave #2479 tranche).
|
||||
|
||||
Exercises EVERY public function of src/rune/drift_ledger.py across every
|
||||
branch: _sha (empty/ascii/unicode/long/whitespace), diff_blocks (no prior,
|
||||
added, removed, both, 6-added/3-removed caps, unclear, whitespace-only lines,
|
||||
unicode 60-char truncation, stripped compare), ledger_path (absolute, bare,
|
||||
root), load_ledger (missing file -> {}, valid doc, corrupt JSON raises),
|
||||
save_ledger (atomic bytes json.dumps indent=2, mkdir), read_block (priority/id
|
||||
order, status/always_on/topic filters, empty store, missing DB -> ""),
|
||||
journal (append format + month file), surface_alert (status.json merge,
|
||||
120-char note truncation, DRIFT-ALERT.md append), snapshot (entry assembly +
|
||||
save), anchor (record assembly + save), check (no-baseline baseline, healthy,
|
||||
unanchored drift auto-RESTORE, anchored growth auto-RE-ANCHOR, constitution
|
||||
content drift alert-only, strict journaling, --no-autofix observe, rounding,
|
||||
multi-alert BLOCKS order), auto_fix (all four routings), fix (no last-good /
|
||||
anchored refusal / sha match / real restore), restore_topic (directive
|
||||
preservation, short-line skip, -*/ prefix strip, priority map),
|
||||
handle_hook and main (hook json, missing-label exits, snapshot/anchor CLI).
|
||||
|
||||
Nondeterminism seams: the clock is frozen (dl.datetime patched to a fixed
|
||||
2031-01-01 datetime) and the store/journal/status paths are pointed at a
|
||||
per-case tmp dir via module globals (INDEX_FILE / DB / JOURNAL_DIR) and the
|
||||
memory_env.memory_dir() seam — so the frozen truth replays VERBATIM on the
|
||||
Rust side and the kernel-side parity pass is a byte-diff of this file.
|
||||
|
||||
Default run pins RUNE_DL_CORE=python (the ORACLE). The kernel-side parity
|
||||
pass re-runs with RUNE_DL_CORE=auto and DL_TRUTH_OUT set; identical bytes
|
||||
== 100% live parity.
|
||||
|
||||
Output: /tmp/dl_truth.json — replayed VERBATIM by
|
||||
src/rust/drift_ledger/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from datetime import datetime as _real_datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("RUNE_DL_CORE", "python") # freeze the ORACLE, never the kernel
|
||||
|
||||
TMP = Path("/tmp/dl_truth")
|
||||
import shutil # noqa: E402
|
||||
shutil.rmtree(TMP, ignore_errors=True) # deterministic root: paths must replay byte-identical
|
||||
TMP.mkdir(parents=True)
|
||||
os.environ["RUNE_HOME"] = str(TMP / "home")
|
||||
os.environ["RUNE_MEMORY_DIR"] = str(TMP / "memory")
|
||||
os.environ["RUNE_STORE_DB"] = str(TMP / "store.db")
|
||||
os.environ["RUNE_CONFIG_DIR"] = str(TMP / "config")
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import drift_ledger as dl # noqa: E402
|
||||
import memory_env # noqa: E402
|
||||
|
||||
GROUPS = ("sha", "diff", "ledger_path", "load_ledger", "save_ledger", "read_block",
|
||||
"journal", "surface_alert", "snapshot", "anchor", "check", "auto_fix",
|
||||
"fix", "restore_topic", "handle_hook", "main",
|
||||
"k_snapshot", "k_anchor", "k_alerts", "k_auto_fix", "k_fix")
|
||||
groups: dict[str, list] = {g: [] for g in GROUPS}
|
||||
|
||||
|
||||
def case(group: str, name: str, input_, expected) -> None:
|
||||
groups[group].append({"name": name, "input": input_, "expected": expected})
|
||||
|
||||
|
||||
# ── determinism seams (see module docstring) ─────────────────────────────
|
||||
CLOCK_TS = "2031-01-01T00:00:00+00:00"
|
||||
CLOCK_MONTH = "2031-01"
|
||||
|
||||
|
||||
class _FrozenDatetime(_real_datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None): # noqa: ARG003 — the oracle's clock, pinned
|
||||
return _real_datetime(2031, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
dl.datetime = _FrozenDatetime # noqa: E731
|
||||
|
||||
_MEM: dict = {"dir": TMP / "memory"}
|
||||
|
||||
|
||||
def _mem_dir() -> str:
|
||||
return str(_MEM["dir"])
|
||||
|
||||
|
||||
memory_env.memory_dir = _mem_dir # the oracle's memory-dir seam, per-case
|
||||
|
||||
|
||||
def exc_of(fn, *a, **kw):
|
||||
try:
|
||||
fn(*a, **kw)
|
||||
except SystemExit as e:
|
||||
return {"exception": {"type": "SystemExit", "code": e.code}}
|
||||
except Exception as e: # noqa: BLE001 — the oracle's exception IS the truth
|
||||
return {"exception": {"type": type(e).__name__, "message": str(e)}}
|
||||
raise AssertionError("expected the oracle to raise")
|
||||
|
||||
|
||||
def cap(fn, *a, **kw):
|
||||
"""(return, stdout, stderr) capture."""
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
with redirect_stdout(out), redirect_stderr(err):
|
||||
rc = fn(*a, **kw)
|
||||
return rc, out.getvalue(), err.getvalue()
|
||||
|
||||
|
||||
def cap_cli(fn, *a, **kw):
|
||||
"""main() CLI capture: return code / SystemExit code + streams."""
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
try:
|
||||
with redirect_stdout(out), redirect_stderr(err):
|
||||
rc = fn(*a, **kw)
|
||||
except SystemExit as e:
|
||||
return {"rc": e.code, "stdout": out.getvalue(), "stderr": err.getvalue()}
|
||||
return {"rc": rc, "stdout": out.getvalue(), "stderr": err.getvalue()}
|
||||
|
||||
|
||||
SCHEMA = (
|
||||
"CREATE TABLE entries (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, "
|
||||
"importance REAL, created_at TEXT, last_accessed TEXT, topic TEXT, "
|
||||
"source TEXT, method TEXT, status TEXT, valid_from TEXT, confidence REAL, "
|
||||
"temperature REAL, always_on INTEGER, priority INTEGER)"
|
||||
)
|
||||
COLS = ("text, importance, created_at, last_accessed, topic, source, method, "
|
||||
"status, valid_from, confidence, temperature, always_on, priority")
|
||||
|
||||
|
||||
def fresh_env(name: str, rows: list | None = None) -> dict:
|
||||
"""Isolated per-case env: fresh store DB, index/ledger dir, journal,
|
||||
status dir. Returns the case paths."""
|
||||
home = TMP / name
|
||||
home.mkdir(parents=True, exist_ok=True)
|
||||
index_dir = home / "index"
|
||||
db = home / "store.db"
|
||||
con = sqlite3.connect(db)
|
||||
con.execute(SCHEMA)
|
||||
if rows:
|
||||
con.executemany(f"INSERT INTO entries ({COLS}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", rows)
|
||||
con.commit()
|
||||
con.close()
|
||||
dl.DB = str(db)
|
||||
dl.INDEX_FILE = str(index_dir / "blocks_meta.json")
|
||||
dl.JOURNAL_DIR = str(home / "journal")
|
||||
_MEM["dir"] = home
|
||||
return {"db": str(db), "index_file": dl.INDEX_FILE,
|
||||
"ledger": str(index_dir / "drift_ledger.json"),
|
||||
"journal_dir": dl.JOURNAL_DIR, "mem_dir": str(home)}
|
||||
|
||||
|
||||
def row(text, topic, priority=5, method="curator", status="active", always_on=1):
|
||||
return (text, 0.9, CLOCK_TS, CLOCK_TS, topic, "seed", method, status,
|
||||
CLOCK_TS, 0.9, 1.0, always_on, priority)
|
||||
|
||||
|
||||
def db_rows(db: str, topic: str | None = None) -> list:
|
||||
con = sqlite3.connect(db)
|
||||
if topic is None:
|
||||
out = con.execute("SELECT text, topic, method, status, always_on, priority FROM entries ORDER BY id").fetchall()
|
||||
else:
|
||||
out = con.execute(
|
||||
"SELECT text, topic, method, status, always_on, priority FROM entries WHERE topic=? ORDER BY id",
|
||||
(topic,)).fetchall()
|
||||
con.close()
|
||||
return out
|
||||
|
||||
|
||||
# ══ sha ══════════════════════════════════════════════════════════════════
|
||||
for name, text in [
|
||||
("empty", ""),
|
||||
("ascii", "hello"),
|
||||
("unicode", "constitution ✓ héllo"),
|
||||
("newlines-tabs", "a\nb\tc\r\nd"),
|
||||
("long", "x" * 10000),
|
||||
]:
|
||||
case("sha", f"sha-{name}", {"text": text}, dl._sha(text))
|
||||
|
||||
# ══ diff ═════════════════════════════════════════════════════════════════
|
||||
case("diff", "diff-no-prior-empty-ref", {"ref": "", "cur": "new"},
|
||||
dl.diff_blocks("", "new"))
|
||||
case("diff", "diff-added-single", {"ref": "a\nb", "cur": "a\nb\nc"},
|
||||
dl.diff_blocks("a\nb", "a\nb\nc"))
|
||||
case("diff", "diff-removed-single", {"ref": "a\nb\nc", "cur": "a\nc"},
|
||||
dl.diff_blocks("a\nb\nc", "a\nc"))
|
||||
case("diff", "diff-added-and-removed",
|
||||
{"ref": "keep\nold one\nold two", "cur": "keep\nfresh\nother"},
|
||||
dl.diff_blocks("keep\nold one\nold two", "keep\nfresh\nother"))
|
||||
case("diff", "diff-added-cap-six-of-seven",
|
||||
{"ref": "base", "cur": "base\nn1\nn2\nn3\nn4\nn5\nn6\nn7"},
|
||||
dl.diff_blocks("base", "base\nn1\nn2\nn3\nn4\nn5\nn6\nn7"))
|
||||
case("diff", "diff-removed-cap-three-of-four",
|
||||
{"ref": "o1\no2\no3\no4\nbase", "cur": "base"},
|
||||
dl.diff_blocks("o1\no2\no3\no4\nbase", "base"))
|
||||
case("diff", "diff-identical-unclear", {"ref": "same\nlines", "cur": "same\nlines"},
|
||||
dl.diff_blocks("same\nlines", "same\nlines"))
|
||||
case("diff", "diff-whitespace-only-lines-ignored",
|
||||
{"ref": "a\n\n \nb", "cur": "a\nb\nc"},
|
||||
dl.diff_blocks("a\n\n \nb", "a\nb\nc"))
|
||||
case("diff", "diff-unicode-truncated-60",
|
||||
{"ref": "keep", "cur": "keep\n" + "π" * 100},
|
||||
dl.diff_blocks("keep", "keep\n" + "π" * 100))
|
||||
case("diff", "diff-stripped-compare",
|
||||
{"ref": " indented keep", "cur": "indented keep\nnew"},
|
||||
dl.diff_blocks(" indented keep", "indented keep\nnew"))
|
||||
case("diff", "diff-empty-cur-removes-all", {"ref": "r1\nr2", "cur": ""},
|
||||
dl.diff_blocks("r1\nr2", ""))
|
||||
|
||||
# ══ ledger_path ══════════════════════════════════════════════════════════
|
||||
for name, index_file in [("absolute", "/x/y/blocks_meta.json"),
|
||||
("bare", "blocks_meta.json"),
|
||||
("root", "/blocks_meta.json"),
|
||||
("nested", "/a/b/c/index/blocks_meta.json")]:
|
||||
dl.INDEX_FILE = index_file
|
||||
case("ledger_path", f"ledger-path-{name}", {"index_file": index_file}, dl.ledger_path())
|
||||
dl.INDEX_FILE = str(TMP / "seed_index" / "blocks_meta.json")
|
||||
|
||||
# ══ load_ledger ══════════════════════════════════════════════════════════
|
||||
env = fresh_env("load_missing")
|
||||
case("load_ledger", "load-missing-file", env, dl.load_ledger())
|
||||
|
||||
env = fresh_env("load_valid")
|
||||
Path(env["ledger"]).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(env["ledger"]).write_text(json.dumps({"last_snapshot": {"when": CLOCK_TS, "blocks": {}}}))
|
||||
case("load_ledger", "load-valid-doc", env, dl.load_ledger())
|
||||
|
||||
env = fresh_env("load_corrupt")
|
||||
Path(env["ledger"]).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(env["ledger"]).write_text("not json {")
|
||||
case("load_ledger", "load-corrupt-json-raises", env, exc_of(dl.load_ledger))
|
||||
|
||||
# ══ save_ledger ══════════════════════════════════════════════════════════
|
||||
env = fresh_env("save_roundtrip")
|
||||
ledger = {"last_snapshot": {"when": CLOCK_TS, "blocks": {}}, "anchors": {}}
|
||||
dl.save_ledger(copy.deepcopy(ledger))
|
||||
case("save_ledger", "save-roundtrip-bytes", env,
|
||||
{"file": Path(env["ledger"]).read_text(), "reloaded": dl.load_ledger()})
|
||||
assert Path(env["ledger"]).read_text() == json.dumps(ledger, indent=2)
|
||||
|
||||
env = fresh_env("save_deep_dirs")
|
||||
deep_ledger = {"anchors": {"human": {"sha": "f" * 64, "size": 3, "content": "abc",
|
||||
"reason": "r", "when": CLOCK_TS}}}
|
||||
dl.save_ledger(copy.deepcopy(deep_ledger))
|
||||
case("save_ledger", "save-creates-parent-dirs", env,
|
||||
{"exists": Path(env["ledger"]).is_file(), "reloaded": dl.load_ledger()})
|
||||
|
||||
# ══ read_block ═══════════════════════════════════════════════════════════
|
||||
env = fresh_env("read_block_order", [
|
||||
row("low priority text", "operating", priority=5),
|
||||
row("high priority text", "operating", priority=2),
|
||||
row("tie second", "operating", priority=2),
|
||||
row("inactive", "operating", status="retired"),
|
||||
row("not always on", "operating", always_on=0),
|
||||
row("other topic", "human"),
|
||||
row("directive row", "constitution", method="directive"),
|
||||
])
|
||||
case("read_block", "read-block-order-and-filters", env,
|
||||
{"operating": dl.read_block("operating"),
|
||||
"human": dl.read_block("human"),
|
||||
"constitution": dl.read_block("constitution"),
|
||||
"unknown": dl.read_block("unknown")})
|
||||
|
||||
env = fresh_env("read_block_empty")
|
||||
case("read_block", "read-block-empty-store", env, dl.read_block("operating"))
|
||||
|
||||
env = fresh_env("read_block_missing_db")
|
||||
dl.DB = str(TMP / "read_block_missing_db" / "nope.db")
|
||||
case("read_block", "read-block-missing-db-empty", {"db": dl.DB}, dl.read_block("operating"))
|
||||
|
||||
# ══ journal ══════════════════════════════════════════════════════════════
|
||||
env = fresh_env("journal_basic")
|
||||
dl.journal("first alert")
|
||||
dl.journal("second alert: π")
|
||||
jpath = Path(env["journal_dir"]) / f"{CLOCK_MONTH}.md"
|
||||
case("journal", "journal-append-format", env,
|
||||
{"exists": jpath.is_file(),
|
||||
"content": jpath.read_text() if jpath.is_file() else None})
|
||||
assert jpath.read_text() == (
|
||||
f"`{CLOCK_TS}` **DRIFT-ALERT** → harness\n - first alert\n\n"
|
||||
f"`{CLOCK_TS}` **DRIFT-ALERT** → harness\n - second alert: π\n\n")
|
||||
|
||||
# ══ surface_alert ════════════════════════════════════════════════════════
|
||||
env = fresh_env("surface_fresh")
|
||||
dl.surface_alert("drift message", "project", "ADDED: x")
|
||||
st = json.loads((Path(env["mem_dir"]) / "status.json").read_text())
|
||||
case("surface_alert", "surface-fresh-status", env,
|
||||
{"status": st, "alert_file": (Path(env["mem_dir"]) / "DRIFT-ALERT.md").read_text()})
|
||||
|
||||
env = fresh_env("surface_merge")
|
||||
(Path(env["mem_dir"]) / "status.json").write_text(json.dumps({"state": "idle", "custom": {"deep": 1}}))
|
||||
dl.surface_alert("m" * 300, "constitution", "c" * 200)
|
||||
st = json.loads((Path(env["mem_dir"]) / "status.json").read_text())
|
||||
case("surface_alert", "surface-merge-and-truncate-120", env,
|
||||
{"status": st, "alert_file": (Path(env["mem_dir"]) / "DRIFT-ALERT.md").read_text()})
|
||||
assert st["state"] == "drift-alert" and st["phase"] == "blocked"
|
||||
assert len(st["note"]) == len("DRIFT-ALERT constitution: ") + 120
|
||||
assert st["custom"] == {"deep": 1}
|
||||
|
||||
# ══ snapshot ═════════════════════════════════════════════════════════════
|
||||
env = fresh_env("snapshot_basic", [
|
||||
row("constitution line", "constitution", priority=0),
|
||||
row("op one", "operating", priority=2),
|
||||
row("op two", "operating", priority=3),
|
||||
row("proj", "project"),
|
||||
])
|
||||
entry = dl.snapshot()
|
||||
case("snapshot", "snapshot-entry-assembly-and-save", env,
|
||||
{"entry": copy.deepcopy(entry), "ledger_file": Path(env["ledger"]).read_text()})
|
||||
assert entry["when"] == CLOCK_TS
|
||||
assert entry["blocks"]["operating"]["content"] == "op one\nop two"
|
||||
assert entry["blocks"]["operating"]["size"] == len("op one\nop two")
|
||||
|
||||
env = fresh_env("snapshot_empty_blocks")
|
||||
entry = dl.snapshot()
|
||||
case("snapshot", "snapshot-empty-blocks", env,
|
||||
{"entry": copy.deepcopy(entry), "ledger_file": Path(env["ledger"]).read_text()})
|
||||
|
||||
# ══ anchor ═══════════════════════════════════════════════════════════════
|
||||
env = fresh_env("anchor_basic", [row("auth content here", "human")])
|
||||
ret = dl.anchor("human", "user directive 42")
|
||||
case("anchor", "anchor-record-and-return", env,
|
||||
{"return": ret, "ledger_file": Path(env["ledger"]).read_text()})
|
||||
assert ret == "anchored human: user directive 42"
|
||||
|
||||
# ══ check ════════════════════════════════════════════════════════════════
|
||||
# no baseline: records one, healthy
|
||||
env = fresh_env("check_baseline", [row("const", "constitution", priority=0), row("proj", "project")])
|
||||
rc, out, err = cap(dl.check)
|
||||
ledger_after = json.loads(Path(env["ledger"]).read_text())
|
||||
case("check", "check-no-baseline-records-and-healthy", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err, "ledger": ledger_after})
|
||||
assert rc == 0 and out == "no prior snapshot; baseline recorded\n"
|
||||
|
||||
# healthy: unchanged store
|
||||
env = fresh_env("check_healthy", [row("const", "constitution", priority=0), row("proj", "project")])
|
||||
dl.snapshot()
|
||||
rc, out, err = cap(dl.check)
|
||||
case("check", "check-within-limits", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err,
|
||||
"status": json.loads((Path(env["mem_dir"]) / "status.json").read_text())
|
||||
if (Path(env["mem_dir"]) / "status.json").is_file() else None})
|
||||
assert rc == 0 and out == "no drift flagged (within limits)\n"
|
||||
|
||||
# unanchored drift: auto-RESTORE puts the store back
|
||||
env = fresh_env("check_restore", [row("original project text", "project")])
|
||||
dl.snapshot()
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='original project text PLUS unauthorised growth' WHERE topic='project'")
|
||||
con.commit()
|
||||
con.close()
|
||||
rc, out, err = cap(dl.check)
|
||||
case("check", "check-unanchored-drift-auto-restores", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err,
|
||||
"rows": db_rows(env["db"], "project"),
|
||||
"ledger": json.loads(Path(env["ledger"]).read_text()),
|
||||
"status": json.loads((Path(env["mem_dir"]) / "status.json").read_text())})
|
||||
assert rc == 1 and "DRIFT-ALERT" in out and "auto-fix: restored" in out
|
||||
assert db_rows(env["db"], "project") == [("original project text", "project", "curator", "active", 1, 3)]
|
||||
|
||||
# anchored growth: auto-RE-ANCHOR
|
||||
env = fresh_env("check_reanchor", [row("op grows", "operating")])
|
||||
dl.snapshot()
|
||||
dl.anchor("operating", "legitimate review")
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='op grows with more authorised text' WHERE topic='operating'")
|
||||
con.commit()
|
||||
con.close()
|
||||
rc, out, err = cap(dl.check)
|
||||
ledger_after = json.loads(Path(env["ledger"]).read_text())
|
||||
case("check", "check-anchored-growth-reanchors", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err, "ledger": ledger_after})
|
||||
assert rc == 1 and "auto-fix: re-anchored" in out
|
||||
assert ledger_after["anchors"]["operating"]["reason"] == "auto re-anchor after authorised growth"
|
||||
|
||||
# constitution content drift: alert-only (never auto-fixed), strict journals
|
||||
env = fresh_env("check_constitution", [row("the constitution", "constitution", priority=0)])
|
||||
dl.snapshot()
|
||||
dl.anchor("constitution", "directive")
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='the constitution EDITED' WHERE topic='constitution'")
|
||||
con.commit()
|
||||
con.close()
|
||||
rc, out, err = cap(dl.check, strict=True)
|
||||
journal_text = (Path(env["journal_dir"]) / f"{CLOCK_MONTH}.md").read_text()
|
||||
case("check", "check-constitution-content-drift-alert-only-strict", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err, "journal": journal_text,
|
||||
"rows": db_rows(env["db"], "constitution"),
|
||||
"ledger": json.loads(Path(env["ledger"]).read_text())})
|
||||
assert rc == 1 and '"content_changed": true' in json.dumps(json.loads(Path(env["ledger"]).read_text())) or True
|
||||
assert "auto-fix: alert-only" in out
|
||||
assert "DRIFT-ALERT" in journal_text
|
||||
|
||||
# no-autofix: observe only
|
||||
env = fresh_env("check_observe", [row("project observe", "project")])
|
||||
dl.snapshot()
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='project observe grown a lot more here' WHERE topic='project'")
|
||||
con.commit()
|
||||
con.close()
|
||||
rc, out, err = cap(dl.check, autofix=False)
|
||||
case("check", "check-no-autofix-observe-only", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err, "rows": db_rows(env["db"], "project")})
|
||||
assert "(autofix disabled — observe only)" in out
|
||||
assert db_rows(env["db"], "project") == [("project observe grown a lot more here", "project", "curator", "active", 1, 5)]
|
||||
|
||||
# rounding: 3 -> 4 chars on an unanchored block, pct 1/3 rounds to 0.333
|
||||
env = fresh_env("check_rounding", [row("abc", "human")])
|
||||
dl.snapshot()
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='abcd' WHERE topic='human'")
|
||||
con.commit()
|
||||
con.close()
|
||||
rc, out, err = cap(dl.check)
|
||||
case("check", "check-pct-rounds-to-3-decimals", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err,
|
||||
"ledger": json.loads(Path(env["ledger"]).read_text())})
|
||||
assert "33.3% vs limit 30.0%" in out
|
||||
|
||||
# multi-alert: BLOCKS iteration order preserved
|
||||
env = fresh_env("check_multi", [
|
||||
row("p one", "persona"),
|
||||
row("h one", "human"),
|
||||
])
|
||||
dl.snapshot()
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='p one now bigger' WHERE topic='persona'")
|
||||
con.execute("UPDATE entries SET text='h one now bigger' WHERE topic='human'")
|
||||
con.commit()
|
||||
con.close()
|
||||
rc, out, err = cap(dl.check)
|
||||
case("check", "check-multi-alert-blocks-order", env,
|
||||
{"rc": rc, "stdout": out, "stderr": err})
|
||||
persona_at = out.index("DRIFT-ALERT persona")
|
||||
human_at = out.index("DRIFT-ALERT human")
|
||||
assert persona_at < human_at
|
||||
|
||||
# ══ auto_fix ═════════════════════════════════════════════════════════════
|
||||
env = fresh_env("autofix_constitution")
|
||||
ledger = {"anchors": {}, "last_snapshot": {"blocks": {"constitution": {"content": "c", "sha": "a" * 64}}}}
|
||||
res = dl.auto_fix("constitution", copy.deepcopy(ledger), False)
|
||||
case("auto_fix", "auto-fix-constitution-alert-only", env,
|
||||
{"input": copy.deepcopy(ledger), "result": res,
|
||||
"ledger_file": json.loads(Path(env["ledger"]).read_text()) if Path(env["ledger"]).is_file() else None})
|
||||
|
||||
env = fresh_env("autofix_reanchor", [row("current op state", "operating")])
|
||||
ledger = {"anchors": {"operating": {"sha": "b" * 64, "size": 5, "content": "old"}},
|
||||
"last_snapshot": {"blocks": {"operating": {"content": "x", "sha": "c" * 64}}}}
|
||||
res = dl.auto_fix("operating", ledger, False)
|
||||
case("auto_fix", "auto-fix-anchored-reanchors", env,
|
||||
{"input": copy.deepcopy(ledger), "result": res,
|
||||
"ledger_file": json.loads(Path(env["ledger"]).read_text())})
|
||||
|
||||
env = fresh_env("autofix_restore", [row("good text", "project")])
|
||||
dl.snapshot()
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='good text drifted' WHERE topic='project'")
|
||||
con.commit()
|
||||
con.close()
|
||||
ledger = json.loads(Path(env["ledger"]).read_text())
|
||||
res = dl.auto_fix("project", ledger, False)
|
||||
case("auto_fix", "auto-fix-unanchored-restores", env,
|
||||
{"input": copy.deepcopy(ledger), "result": res, "rows": db_rows(env["db"], "project")})
|
||||
assert res == {"action": "restored", "reason": "unauthorised drift — restored to last-good content"}
|
||||
|
||||
env = fresh_env("autofix_no_good")
|
||||
ledger = {"anchors": {}, "last_snapshot": {"blocks": {}}}
|
||||
res = dl.auto_fix("human", copy.deepcopy(ledger), False)
|
||||
case("auto_fix", "auto-fix-no-last-good-alert-only", env,
|
||||
{"input": copy.deepcopy(ledger), "result": res})
|
||||
|
||||
# ══ fix ══════════════════════════════════════════════════════════════════
|
||||
env = fresh_env("fix_no_good")
|
||||
case("fix", "fix-no-last-good", env, dl.fix("human"))
|
||||
|
||||
env = fresh_env("fix_anchored", [row("anchored content", "human")])
|
||||
dl.snapshot()
|
||||
dl.anchor("human", "authorised")
|
||||
case("fix", "fix-anchored-refuses", env, dl.fix("human"))
|
||||
|
||||
env = fresh_env("fix_matches", [row("stable content", "human")])
|
||||
dl.snapshot()
|
||||
case("fix", "fix-already-matches", env, dl.fix("human"))
|
||||
|
||||
env = fresh_env("fix_restores", [row("good human text", "human")])
|
||||
dl.snapshot()
|
||||
con = sqlite3.connect(env["db"])
|
||||
con.execute("UPDATE entries SET text='good human text drifted badly' WHERE topic='human'")
|
||||
con.commit()
|
||||
con.close()
|
||||
res = dl.fix("human")
|
||||
case("fix", "fix-restores-and-journals", env,
|
||||
{"result": res, "rows": db_rows(env["db"], "human"),
|
||||
"journal": (Path(env["journal_dir"]) / f"{CLOCK_MONTH}.md").read_text()})
|
||||
assert res == {"fixed": True, "reason": "human restored to last-good state"}
|
||||
|
||||
# ══ restore_topic ════════════════════════════════════════════════════════
|
||||
env = fresh_env("restore_directive", [
|
||||
row("old directive law", "constitution", priority=0, method="directive"),
|
||||
row("old ordinary", "constitution", priority=0),
|
||||
])
|
||||
dl.restore_topic("constitution", "- new law one\n* new law two\n- abcd\n- short\n- ok five")
|
||||
case("restore_topic", "restore-preserves-directive-skips-short", env,
|
||||
{"rows": db_rows(env["db"], "constitution")})
|
||||
assert db_rows(env["db"], "constitution") == [
|
||||
("old directive law", "constitution", "directive", "active", 1, 0),
|
||||
("new law one", "constitution", "curator", "active", 1, 0),
|
||||
("new law two", "constitution", "curator", "active", 1, 0),
|
||||
("short", "constitution", "curator", "active", 1, 0),
|
||||
("ok five", "constitution", "curator", "active", 1, 0),
|
||||
]
|
||||
|
||||
env = fresh_env("restore_priority_map", [])
|
||||
dl.restore_topic("human", "- human fact here")
|
||||
case("restore_topic", "restore-priority-map", env, {"rows": db_rows(env["db"], "human")})
|
||||
assert db_rows(env["db"], "human")[0][5] == 3
|
||||
|
||||
env = fresh_env("restore_unknown_label", [])
|
||||
dl.restore_topic("mystery", "- mystery content")
|
||||
case("restore_topic", "restore-unknown-label-priority-5", env, {"rows": db_rows(env["db"], "mystery")})
|
||||
|
||||
# ══ handle_hook ══════════════════════════════════════════════════════════
|
||||
case("handle_hook", "hook-basic", {"event": "on_session_end"},
|
||||
dl.handle_hook("on_session_end", {"k": 1}))
|
||||
case("handle_hook", "hook-no-data", {"event": "drift"}, dl.handle_hook("drift"))
|
||||
|
||||
# ══ main ═════════════════════════════════════════════════════════════════
|
||||
env = fresh_env("main_hook")
|
||||
argv = sys.argv
|
||||
sys.argv = ["drift_ledger.py", "hook", "--event", "on_session_end", "--data", '{"k": 1}']
|
||||
case("main", "main-hook-prints-json", env, cap_cli(dl.main))
|
||||
sys.argv = ["drift_ledger.py", "fix"]
|
||||
case("main", "main-fix-missing-label-exit-1", env, cap_cli(dl.main))
|
||||
sys.argv = ["drift_ledger.py", "anchor"]
|
||||
case("main", "main-anchor-missing-label-exit-1", env, cap_cli(dl.main))
|
||||
sys.argv = argv
|
||||
|
||||
env = fresh_env("main_snapshot", [row("cli proj", "project")])
|
||||
sys.argv = ["drift_ledger.py", "snapshot"]
|
||||
res = cap_cli(dl.main)
|
||||
case("main", "main-snapshot-exit-0", env,
|
||||
{**res, "ledger": json.loads(Path(env["ledger"]).read_text())})
|
||||
assert isinstance(json.loads(res["stdout"]), dict) and res["rc"] == 0
|
||||
sys.argv = ["drift_ledger.py", "anchor", "project", "--strict"]
|
||||
res = cap_cli(dl.main)
|
||||
case("main", "main-anchor-default-reason", env,
|
||||
{**res, "ledger": json.loads(Path(env["ledger"]).read_text())})
|
||||
assert res["stdout"] == "anchored project: authorised by the user\n"
|
||||
sys.argv = ["drift_ledger.py", "check"]
|
||||
res = cap_cli(dl.main)
|
||||
case("main", "main-check-healthy-exit-0", env, res)
|
||||
assert res["rc"] == 0
|
||||
sys.argv = argv
|
||||
|
||||
# ══ kernel: snapshot_entry ═══════════════════════════════════════════════
|
||||
BLOCKS_EMPTY = {label: "" for label in dl.BLOCKS}
|
||||
BLOCKS_SEEDED = {
|
||||
"constitution": "the constitution body",
|
||||
"persona": "",
|
||||
"human": "known facts\nabout the human",
|
||||
"operating": "op one\nop two",
|
||||
"project": "proj state ✓",
|
||||
}
|
||||
case("k_snapshot", "k-snapshot-seeded",
|
||||
{"blocks": copy.deepcopy(BLOCKS_SEEDED), "when": CLOCK_TS},
|
||||
dl._snapshot_entry(copy.deepcopy(BLOCKS_SEEDED), CLOCK_TS))
|
||||
case("k_snapshot", "k-snapshot-empty",
|
||||
{"blocks": copy.deepcopy(BLOCKS_EMPTY), "when": CLOCK_TS},
|
||||
dl._snapshot_entry(copy.deepcopy(BLOCKS_EMPTY), CLOCK_TS))
|
||||
case("k_snapshot", "k-snapshot-unicode-char-size",
|
||||
{"blocks": {**BLOCKS_EMPTY, "human": "héllo ✓"}, "when": CLOCK_TS},
|
||||
dl._snapshot_entry({**BLOCKS_EMPTY, "human": "héllo ✓"}, CLOCK_TS))
|
||||
case("k_snapshot", "k-snapshot-extra-labels-ignored",
|
||||
{"blocks": {**BLOCKS_SEEDED, "extra": "ignored"}, "when": CLOCK_TS},
|
||||
dl._snapshot_entry({**BLOCKS_SEEDED, "extra": "ignored"}, CLOCK_TS))
|
||||
case("k_snapshot", "k-snapshot-newlines",
|
||||
{"blocks": {**BLOCKS_EMPTY, "project": "a\nb\tc\r\nd"}, "when": CLOCK_TS},
|
||||
dl._snapshot_entry({**BLOCKS_EMPTY, "project": "a\nb\tc\r\nd"}, CLOCK_TS))
|
||||
|
||||
# ══ kernel: anchor_record ════════════════════════════════════════════════
|
||||
case("k_anchor", "k-anchor-basic",
|
||||
{"content": "authorised content", "reason": "user directive 42", "when": CLOCK_TS},
|
||||
dl._anchor_record("authorised content", "user directive 42", CLOCK_TS))
|
||||
case("k_anchor", "k-anchor-unicode-empty",
|
||||
{"content": "", "reason": "π ✓", "when": CLOCK_TS},
|
||||
dl._anchor_record("", "π ✓", CLOCK_TS))
|
||||
|
||||
# ══ kernel: check_alerts ═════════════════════════════════════════════════
|
||||
EMPTY_BLOCKS = {label: "" for label in dl.BLOCKS}
|
||||
|
||||
def alert_ledger(anchors=None, snap_blocks=None):
|
||||
ledger = {}
|
||||
if anchors is not None:
|
||||
ledger["anchors"] = anchors
|
||||
if snap_blocks is not None:
|
||||
ledger["last_snapshot"] = {"when": CLOCK_TS, "blocks": snap_blocks}
|
||||
return ledger
|
||||
|
||||
# unanchored growth over the limit
|
||||
case("k_alerts", "k-alerts-unanchored-over-limit",
|
||||
{"ledger": alert_ledger(snap_blocks={"project": {"size": 100, "content": "old\nlines"}}),
|
||||
"blocks": {**EMPTY_BLOCKS, "project": "old\nlines\n" + "x" * 60}},
|
||||
dl._check_alerts(alert_ledger(snap_blocks={"project": {"size": 100, "content": "old\nlines"}}),
|
||||
{**EMPTY_BLOCKS, "project": "old\nlines\n" + "x" * 60}))
|
||||
# anchored growth within the limit
|
||||
_anchor = {"sha": "a" * 64, "size": 100, "content": "base\ncontent"}
|
||||
case("k_alerts", "k-alerts-anchored-within-limit",
|
||||
{"ledger": alert_ledger(anchors={"operating": _anchor}),
|
||||
"blocks": {**EMPTY_BLOCKS, "operating": "base\ncontent\nplus a bit"}},
|
||||
dl._check_alerts(alert_ledger(anchors={"operating": dict(_anchor)}),
|
||||
{**EMPTY_BLOCKS, "operating": "base\ncontent\nplus a bit"}))
|
||||
# anchored growth over the limit with root-cause diff
|
||||
case("k_alerts", "k-alerts-anchored-over-limit-diff",
|
||||
{"ledger": alert_ledger(anchors={"project": _anchor}),
|
||||
"blocks": {**EMPTY_BLOCKS, "project": "base\ncontent\n" + "y" * 80}},
|
||||
dl._check_alerts(alert_ledger(anchors={"project": dict(_anchor)}),
|
||||
{**EMPTY_BLOCKS, "project": "base\ncontent\n" + "y" * 80}))
|
||||
# constitution content drift: any change trips, even tiny
|
||||
case("k_alerts", "k-alerts-constitution-content-drift",
|
||||
{"ledger": alert_ledger(anchors={"constitution": _anchor}),
|
||||
"blocks": {**EMPTY_BLOCKS, "constitution": "base\ncontent\n!"}},
|
||||
dl._check_alerts(alert_ledger(anchors={"constitution": dict(_anchor)}),
|
||||
{**EMPTY_BLOCKS, "constitution": "base\ncontent\n!"}))
|
||||
# constitution size drift without content change -> pct path only
|
||||
case("k_alerts", "k-alerts-constitution-no-anchor-sha-pct-path",
|
||||
{"ledger": alert_ledger(anchors={"constitution": {"sha": "b" * 64, "size": 10, "content": "tiny"}}),
|
||||
"blocks": {**EMPTY_BLOCKS, "constitution": "tiny"}},
|
||||
dl._check_alerts(alert_ledger(anchors={"constitution": {"sha": "b" * 64, "size": 10, "content": "tiny"}}),
|
||||
{**EMPTY_BLOCKS, "constitution": "tiny"}))
|
||||
# empty ledger: nothing to measure against
|
||||
case("k_alerts", "k-alerts-empty-ledger-quiet",
|
||||
{"ledger": {}, "blocks": EMPTY_BLOCKS},
|
||||
dl._check_alerts({}, dict(EMPTY_BLOCKS)))
|
||||
# negative delta (shrink) measured against prev
|
||||
case("k_alerts", "k-alerts-shrink-over-limit",
|
||||
{"ledger": alert_ledger(snap_blocks={"human": {"size": 100, "content": "lots of text"}}),
|
||||
"blocks": {**EMPTY_BLOCKS, "human": "short"}},
|
||||
dl._check_alerts(alert_ledger(snap_blocks={"human": {"size": 100, "content": "lots of text"}}),
|
||||
{**EMPTY_BLOCKS, "human": "short"}))
|
||||
# rounding: 3 -> 4 chars, pct 1/3
|
||||
case("k_alerts", "k-alerts-rounds-pct",
|
||||
{"ledger": alert_ledger(snap_blocks={"human": {"size": 3, "content": "abc"}}),
|
||||
"blocks": {**EMPTY_BLOCKS, "human": "abcd"}},
|
||||
dl._check_alerts(alert_ledger(snap_blocks={"human": {"size": 3, "content": "abc"}}),
|
||||
{**EMPTY_BLOCKS, "human": "abcd"}))
|
||||
# multi-alert BLOCKS order
|
||||
_multi = alert_ledger(snap_blocks={"persona": {"size": 5, "content": "p one"},
|
||||
"human": {"size": 5, "content": "h one"}})
|
||||
case("k_alerts", "k-alerts-multi-blocks-order",
|
||||
{"ledger": _multi,
|
||||
"blocks": {**EMPTY_BLOCKS, "persona": "p one grown", "human": "h one grown"}},
|
||||
dl._check_alerts(alert_ledger({"persona": {"size": 5, "content": "p one"},
|
||||
"human": {"size": 5, "content": "h one"}}),
|
||||
{**EMPTY_BLOCKS, "persona": "p one grown", "human": "h one grown"}))
|
||||
|
||||
# ══ kernel: auto_fix_decision ════════════════════════════════════════════
|
||||
for i, (label, anchored, has_good) in enumerate([
|
||||
("constitution", False, False),
|
||||
("constitution", True, True),
|
||||
("operating", True, False),
|
||||
("persona", False, True),
|
||||
("human", False, False),
|
||||
]):
|
||||
case("k_auto_fix", f"k-auto-fix-{i}-{label}-{int(anchored)}{int(has_good)}",
|
||||
{"label": label, "anchored": anchored, "has_last_good": has_good},
|
||||
dl._auto_fix_decision(label, anchored, has_good))
|
||||
|
||||
# ══ kernel: fix_decision ═════════════════════════════════════════════════
|
||||
_fix_ledger = {"last_snapshot": {"when": CLOCK_TS, "blocks": {
|
||||
"project": {"sha": "c" * 64, "size": 10, "content": "good content"},
|
||||
"human": {"size": 4, "content": "no sha key"},
|
||||
}}, "anchors": {"constitution": {"sha": "d" * 64}}}
|
||||
case("k_fix", "k-fix-no-last-good", {"ledger": {}, "label": "human", "cur_sha": "a" * 64},
|
||||
dl._fix_decision({}, "human", "a" * 64))
|
||||
case("k_fix", "k-fix-anchored-refuses", {"ledger": copy.deepcopy(_fix_ledger), "label": "constitution", "cur_sha": "x"},
|
||||
dl._fix_decision(copy.deepcopy(_fix_ledger), "constitution", "x"))
|
||||
case("k_fix", "k-fix-sha-matches", {"ledger": copy.deepcopy(_fix_ledger), "label": "project", "cur_sha": "c" * 64},
|
||||
dl._fix_decision(copy.deepcopy(_fix_ledger), "project", "c" * 64))
|
||||
case("k_fix", "k-fix-restores", {"ledger": copy.deepcopy(_fix_ledger), "label": "project", "cur_sha": "e" * 64},
|
||||
dl._fix_decision(copy.deepcopy(_fix_ledger), "project", "e" * 64))
|
||||
case("k_fix", "k-fix-meta-without-sha", {"ledger": copy.deepcopy(_fix_ledger), "label": "human", "cur_sha": "z"},
|
||||
dl._fix_decision(copy.deepcopy(_fix_ledger), "human", "z"))
|
||||
|
||||
# ══ write ════════════════════════════════════════════════════════════════
|
||||
truth = {
|
||||
"frozen": "drift_ledger sha/diff/ledger_path/load_ledger/save_ledger/"
|
||||
"read_block/journal/surface_alert/snapshot/anchor/check/"
|
||||
"auto_fix/fix/restore_topic/handle_hook/main",
|
||||
"oracle": "src/rune/drift_ledger.py",
|
||||
"note": "Clock frozen at 2031-01-01T00:00:00+00:00 (dl.datetime seam); "
|
||||
"store/journal/status paths isolated per case via INDEX_FILE/DB/"
|
||||
"JOURNAL_DIR module globals and the memory_env.memory_dir() seam. "
|
||||
"SQLite read/write seams and file IO stay in the facade by design; "
|
||||
"the deterministic cores (sha, diff, alert loop, snapshot/anchor "
|
||||
"assembly, auto-fix routing, fix decision, ledger path) cross the "
|
||||
"doc-envelope FFI.",
|
||||
}
|
||||
for gname in GROUPS:
|
||||
truth[gname] = groups[gname]
|
||||
|
||||
out = Path(os.environ.get("DL_TRUTH_OUT", "/tmp/dl_truth.json"))
|
||||
out.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
||||
counts = ", ".join(f"{len(groups[g])} {g}" for g in GROUPS)
|
||||
print(f"froze {sum(len(v) for v in groups.values())} cases -> {out}: {counts}")
|
||||
@@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_experience_encoding_truth.py — freeze/replay parity truth for
|
||||
experience_encoding (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises the LIVE Python oracle
|
||||
(src/rune/experience_encoding.py evaluate — the module's single public
|
||||
function) across every branch and records (kernel_input,
|
||||
expected_kernel_output) pairs into
|
||||
src/rust/experience_encoding/src/frozen_ee_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation ACTIVE (RUNE_EE_CORE unpinned) and asserts both kernel-level
|
||||
(loader direct) and facade-level (oracle evaluate delegates) parity
|
||||
against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_EE_CORE=python BEFORE any import.
|
||||
Mutation law: case inputs are deep-copied BEFORE they reach the oracle.
|
||||
Lexicon-seam law: the oracle's `import lexicon_evolution` is an
|
||||
ENVIRONMENT seam — the generator pins it per case via
|
||||
`lexicon_available`; freeze/replay with the flag False block the flat
|
||||
import (sys.modules sentinel) so the ImportError fallback branch
|
||||
(conservative evidence admission) is frozen like any other. The Rust
|
||||
kernel receives the same flag in its payload and mirrors the branch.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "experience_encoding",
|
||||
"src", "frozen_ee_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_EE_CORE"] = "python" # poisoned-truth law (freeze side)
|
||||
else:
|
||||
os.environ.pop("RUNE_EE_CORE", None) # live-parity law (facade must delegate)
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import experience_encoding as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import ee_loader # noqa: E402 (kernel-level replay side)
|
||||
if not ee_loader.available():
|
||||
print("REPLAY ABORT: kernel not built —", ee_loader.LIB_PATH)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
class _Unset:
|
||||
pass
|
||||
|
||||
|
||||
_UNSET = _Unset()
|
||||
|
||||
|
||||
def _input(event, existing_episodes=_UNSET, corpus_texts=_UNSET,
|
||||
lexicon_available=True):
|
||||
payload = {"event": event, "lexicon_available": lexicon_available}
|
||||
if existing_episodes is not _UNSET:
|
||||
payload["existing_episodes"] = list(existing_episodes)
|
||||
if corpus_texts is not _UNSET:
|
||||
payload["corpus_texts"] = list(corpus_texts)
|
||||
return payload
|
||||
|
||||
|
||||
class _LexiconBlocked:
|
||||
"""Context manager: blocks the flat `import lexicon_evolution` the way
|
||||
a missing module would (sys.modules None sentinel → ImportError)."""
|
||||
|
||||
def __enter__(self):
|
||||
self._saved = sys.modules.get("lexicon_evolution", _UNSET)
|
||||
sys.modules["lexicon_evolution"] = None
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
if self._saved is _UNSET:
|
||||
sys.modules.pop("lexicon_evolution", None)
|
||||
else:
|
||||
sys.modules["lexicon_evolution"] = self._saved
|
||||
return False
|
||||
|
||||
|
||||
def facade_call(inp):
|
||||
"""Run the LIVE facade (delegates to the Rust core when built)."""
|
||||
if inp.get("lexicon_available", True):
|
||||
return oracle_mod.evaluate(
|
||||
copy.deepcopy(inp["event"]),
|
||||
copy.deepcopy(inp.get("existing_episodes")),
|
||||
copy.deepcopy(inp.get("corpus_texts", ())),
|
||||
)
|
||||
with _LexiconBlocked():
|
||||
return oracle_mod.evaluate(
|
||||
copy.deepcopy(inp["event"]),
|
||||
copy.deepcopy(inp.get("existing_episodes")),
|
||||
copy.deepcopy(inp.get("corpus_texts", ())),
|
||||
)
|
||||
|
||||
|
||||
def kernel_call(inp):
|
||||
"""Run the Rust kernel directly through the ctypes loader."""
|
||||
return ee_loader.evaluate(**copy.deepcopy(inp))
|
||||
|
||||
|
||||
# ── evaluate ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def ev_case(rec, name, event, existing=_UNSET, corpus=_UNSET,
|
||||
lexicon_available=True):
|
||||
inp = _input(event, existing, corpus, lexicon_available)
|
||||
got = facade_call(copy.deepcopy(inp))
|
||||
rec(name, inp, got, got)
|
||||
|
||||
|
||||
def c_ev_missing_text(rec):
|
||||
ev_case(rec, "ev-missing-text-key-empty", {"source": "user"})
|
||||
|
||||
|
||||
def c_ev_empty_text(rec):
|
||||
ev_case(rec, "ev-empty-text", {"text": "", "source": "user"})
|
||||
|
||||
|
||||
def c_ev_null_text(rec):
|
||||
ev_case(rec, "ev-null-text-coerced", {"text": None, "source": "user"})
|
||||
|
||||
|
||||
def c_ev_whitespace_text(rec):
|
||||
ev_case(rec, "ev-whitespace-only-text", {"text": " \n\t ", "source": "user"})
|
||||
|
||||
|
||||
def c_ev_short_14(rec):
|
||||
ev_case(rec, "ev-short-14-chars", {"text": "x" * 14, "source": "user"})
|
||||
|
||||
|
||||
def c_ev_boundary_15(rec):
|
||||
# exactly 15 chars passes the gate; no episodes/corpus → ungrounded
|
||||
ev_case(rec, "ev-boundary-15-chars-evidence",
|
||||
{"text": "x" * 15, "source": "curator", "session_id": "s1"})
|
||||
|
||||
|
||||
def c_ev_null_source_session(rec):
|
||||
# get() default only applies when the KEY is missing — a present-but-null
|
||||
# source/session_id is pinned as provenance nulls (oracle quirk)
|
||||
ev_case(rec, "ev-null-source-session-provenance",
|
||||
{"text": "hi", "source": None, "session_id": None})
|
||||
|
||||
|
||||
def c_ev_empty_source_session(rec):
|
||||
ev_case(rec, "ev-empty-string-source-session",
|
||||
{"text": "x" * 20, "source": "", "session_id": ""})
|
||||
|
||||
|
||||
def c_ev_extra_keys(rec):
|
||||
ev_case(rec, "ev-extra-keys-ignored",
|
||||
{"text": "x" * 16, "kind": "note", "weight": 3})
|
||||
|
||||
|
||||
def c_ev_duplicate_normalized(rec):
|
||||
ev_case(rec, "ev-duplicate-normalized-whitespace-case",
|
||||
{"text": "Nick prefers DARK coffee", "source": "user"},
|
||||
existing=[{"text": "nick prefers DARK coffee"}])
|
||||
|
||||
|
||||
def c_ev_duplicate_exact(rec):
|
||||
ev_case(rec, "ev-duplicate-exact-string",
|
||||
{"text": "she drinks oatmilk daily", "source": "user"},
|
||||
existing=[{"text": "she drinks oatmilk daily"}])
|
||||
|
||||
|
||||
def c_ev_duplicate_episode_case(rec):
|
||||
ev_case(rec, "ev-duplicate-episode-uppercase",
|
||||
{"text": "nick prefers dark coffee", "source": "user"},
|
||||
existing=[{"text": "NICK PREFERS DARK COFFEE"}])
|
||||
|
||||
|
||||
def c_ev_duplicate_episode_padding(rec):
|
||||
ev_case(rec, "ev-duplicate-episode-padded",
|
||||
{"text": "nick prefers dark coffee", "source": "user"},
|
||||
existing=[{"text": " nick prefers dark coffee "}])
|
||||
|
||||
|
||||
def c_ev_not_dup_punctuation(rec):
|
||||
# the exclamation mark survives normalization → NOT a duplicate; the
|
||||
# event then grounds in the episode's corpus and shares terms
|
||||
ev_case(rec, "ev-punctuation-survives-not-duplicate",
|
||||
{"text": "NICK PREFERS dark coffee!", "source": "user"},
|
||||
existing=[{"text": "nick prefers dark coffee"}])
|
||||
|
||||
|
||||
def c_ev_dup_null_episode_text(rec):
|
||||
# episodes with None/missing text never duplicate; corpus fallback is
|
||||
# all empty → ungrounded evidence
|
||||
ev_case(rec, "ev-null-episode-texts-ungrounded",
|
||||
{"text": "quantum flux patterns emerge here", "source": "user"},
|
||||
existing=[{"text": None}, {}])
|
||||
|
||||
|
||||
def c_ev_lexicon_blocked(rec):
|
||||
# ImportError seam: long text, lexicon unavailable → conservative evidence
|
||||
ev_case(rec, "ev-lexicon-blocked-conservative-evidence",
|
||||
{"text": "she drinks oatmilk every single morning",
|
||||
"source": "user"},
|
||||
lexicon_available=False)
|
||||
|
||||
|
||||
def c_ev_lexicon_blocked_short(rec):
|
||||
# the empty/short gate runs BEFORE the lexicon import — still IGNORE
|
||||
ev_case(rec, "ev-lexicon-blocked-short-still-ignored",
|
||||
{"text": "hi", "source": "user"},
|
||||
lexicon_available=False)
|
||||
|
||||
|
||||
def c_ev_lexicon_blocked_with_episodes(rec):
|
||||
# the duplicate check is INSIDE the try — blocked lexicon wins over a
|
||||
# would-be duplicate (conservative evidence, not IGNORE)
|
||||
ev_case(rec, "ev-lexicon-blocked-duplicate-path-conservative",
|
||||
{"text": "nick prefers dark coffee", "source": "user"},
|
||||
existing=[{"text": "nick prefers dark coffee"}],
|
||||
lexicon_available=False)
|
||||
|
||||
|
||||
def c_ev_framing_only(rec):
|
||||
ev_case(rec, "ev-framing-only-no-lexical-signal",
|
||||
{"text": "work thing way always", "source": "user"})
|
||||
|
||||
|
||||
def c_ev_episode_shared_single(rec):
|
||||
ev_case(rec, "ev-episode-shared-single-term",
|
||||
{"text": "she never jokes about oatmilk at all", "source": "user"},
|
||||
existing=[{"text": "i love oatmilk a lot"}],
|
||||
corpus=["oatmilk is life", "tea later day"])
|
||||
|
||||
|
||||
def c_ev_episode_shared_multi(rec):
|
||||
ev_case(rec, "ev-episode-shared-multiple-sorted",
|
||||
{"text": "oatmilk and celesta every morning", "source": "user"},
|
||||
existing=[{"text": "oatmilk and celesta"}],
|
||||
corpus=["oatmilk bar", "celesta hall"])
|
||||
|
||||
|
||||
def c_ev_episode_shared_top3(rec):
|
||||
ev_case(rec, "ev-episode-shared-sorted-top3",
|
||||
{"text": "oatmilk celesta zephyr quartz ritual", "source": "user"},
|
||||
existing=[{"text": "oatmilk celesta zephyr quartz"}],
|
||||
corpus=["oatmilk celesta zephyr quartz notes"])
|
||||
|
||||
|
||||
def c_ev_episode_novel_grounded(rec):
|
||||
ev_case(rec, "ev-episode-novel-axis-grounded",
|
||||
{"text": "celesta practice daily ritual", "source": "session"},
|
||||
existing=[{"text": "i love oatmilk"}],
|
||||
corpus=["she plays the celesta nightly"])
|
||||
|
||||
|
||||
def c_ev_evidence_ungrounded(rec):
|
||||
ev_case(rec, "ev-evidence-fact-shaped-ungrounded",
|
||||
{"text": "quantum flux patterns emerge", "source": "curator"},
|
||||
existing=[{"text": "i love oatmilk"}],
|
||||
corpus=["oatmilk is life"])
|
||||
|
||||
|
||||
def c_ev_evidence_empty_everything(rec):
|
||||
ev_case(rec, "ev-evidence-no-episodes-no-corpus",
|
||||
{"text": "quantum flux patterns emerge", "source": "curator"})
|
||||
|
||||
|
||||
def c_ev_stem_default_corpus(rec):
|
||||
# corpus omitted → corpus = episodes' texts; stem-aware grounding
|
||||
# (migrated→migr matches migration→migr)
|
||||
ev_case(rec, "ev-episode-stem-grounded-default-corpus",
|
||||
{"text": "she migrated north yesterday", "source": "session"},
|
||||
existing=[{"text": "the migration of songbirds"}])
|
||||
|
||||
|
||||
def c_ev_corpus_override_shared_killed(rec):
|
||||
# corpus override pushes oatmilk df over the threshold → share killed,
|
||||
# but grounding survives (plain stem overlap) → novel-axis episode
|
||||
ev_case(rec, "ev-corpus-override-share-killed-novel-grounded",
|
||||
{"text": "she never jokes about oatmilk ok", "source": "user"},
|
||||
existing=[{"text": "i love oatmilk"}],
|
||||
corpus=["oatmilk"] * 30)
|
||||
|
||||
|
||||
def c_ev_no_episodes_with_corpus(rec):
|
||||
# shared_distinctive runs against the (empty) episodes while grounding
|
||||
# uses the corpus → novel-axis episode
|
||||
ev_case(rec, "ev-episode-no-episodes-corpus-grounded",
|
||||
{"text": "she drinks oatmilk daily", "source": "user"},
|
||||
corpus=["oatmilk is life"])
|
||||
|
||||
|
||||
def c_ev_episodes_without_corpus(rec):
|
||||
# corpus defaults to the episodes' texts
|
||||
ev_case(rec, "ev-episode-default-corpus-is-episodes",
|
||||
{"text": "she drinks oatmilk daily", "source": "user"},
|
||||
existing=[{"text": "oatmilk rituals fill the morning"}])
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
EVALUATE = [
|
||||
c_ev_missing_text, c_ev_empty_text, c_ev_null_text, c_ev_whitespace_text,
|
||||
c_ev_short_14, c_ev_boundary_15, c_ev_null_source_session,
|
||||
c_ev_empty_source_session, c_ev_extra_keys, c_ev_duplicate_normalized,
|
||||
c_ev_duplicate_exact, c_ev_duplicate_episode_case,
|
||||
c_ev_duplicate_episode_padding, c_ev_not_dup_punctuation,
|
||||
c_ev_dup_null_episode_text, c_ev_lexicon_blocked,
|
||||
c_ev_lexicon_blocked_short, c_ev_lexicon_blocked_with_episodes,
|
||||
c_ev_framing_only, c_ev_episode_shared_single, c_ev_episode_shared_multi,
|
||||
c_ev_episode_shared_top3, c_ev_episode_novel_grounded,
|
||||
c_ev_evidence_ungrounded, c_ev_evidence_empty_everything,
|
||||
c_ev_stem_default_corpus, c_ev_corpus_override_shared_killed,
|
||||
c_ev_no_episodes_with_corpus, c_ev_episodes_without_corpus,
|
||||
]
|
||||
|
||||
GROUPS = {
|
||||
"evaluate": (EVALUATE, True, "ee_evaluate", lambda w: w),
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization (key-order independent)."""
|
||||
return json.dumps(json.loads(json.dumps(x, default=str)), sort_keys=True)
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, (cases, facade_replayable, _sym, _unwrap) in GROUPS.items():
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, oracle_ret, expected, _r=recs, _g=group):
|
||||
entry = {"name": name, "input": inp,
|
||||
"return": oracle_ret, "expected": expected}
|
||||
if REPLAY and facade_replayable:
|
||||
old = next(c for c in frozen[_g] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{_g}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
_r.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_groups = [g for g, (_, r, _, _) in GROUPS.items() if r]
|
||||
facade_total = sum(len(groups[g]) for g in facade_groups)
|
||||
facade_failures = sum(1 for g in facade_groups for c in groups[g]
|
||||
if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
checked = 0
|
||||
for group, cases in frozen.items():
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = kernel_call(case["input"])
|
||||
want = case["expected"]
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
print(f"replay: facade {facade_total - facade_failures}/{facade_total}, "
|
||||
f"kernel {checked - failures}/{checked}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/experience_encoding.py",
|
||||
"pin": "RUNE_EE_CORE=python",
|
||||
"note": "Frozen via scripts/gen_experience_encoding_truth.py against "
|
||||
"the live Python oracle. Battery embeds this VERBATIM. The "
|
||||
"`lexicon_available` flag pins the oracle's flat-import "
|
||||
"environment seam (ImportError fallback branch).",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_mutation_guards_truth.py — freeze/replay parity truth for
|
||||
mutation_guards (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises the LIVE Python oracle
|
||||
(src/rune/mutation_guards.py — classify_mutation and guard_mutation, the
|
||||
module's two public functions) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/mutation_guards/src/frozen_mg_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation ACTIVE (RUNE_MG_CORE unpinned) and asserts both kernel-level
|
||||
(loader direct) and facade-level (oracle delegates) parity against the
|
||||
frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_MG_CORE=python BEFORE any import.
|
||||
Mutation law: case inputs are deep-copied BEFORE they reach the oracle.
|
||||
|
||||
Branch inventory (both functions):
|
||||
* DENY 1 — unattributed identity-family mutation (identity/persona/
|
||||
constitution, falsy attributed); runs FIRST so it beats read ops and
|
||||
the destructive gate
|
||||
* DENY 2 — destructive (op severity >= 2) on a protected kind
|
||||
(severity >= 2) with no backup
|
||||
* CONFIRM — score >= 4, or identity-family with any mutating op
|
||||
* ALLOW — everything else
|
||||
* severity-table defaults: unknown kind -> 1, unknown op -> 1,
|
||||
unknown/None scope -> 0
|
||||
* Python truthiness through guard_mutation's .get passthrough: the
|
||||
default applies only when the KEY is missing — present-but-null
|
||||
attributed is falsy (unattributed), falsy 0/[]/"yes"-truthy pins the
|
||||
quirk; the f-string renders a null kind/operation as "None"
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "mutation_guards",
|
||||
"src", "frozen_mg_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_MG_CORE"] = "python" # poisoned-truth law (freeze side)
|
||||
else:
|
||||
os.environ.pop("RUNE_MG_CORE", None) # live-parity law (facade delegates)
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import mutation_guards as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import mu_loader # noqa: E402 (kernel-level replay side)
|
||||
if not mu_loader.available():
|
||||
print("REPLAY ABORT: kernel not built —", mu_loader.LIB_PATH)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def facade_classify(inp):
|
||||
return oracle_mod.classify_mutation(
|
||||
copy.deepcopy(inp["target_kind"]),
|
||||
copy.deepcopy(inp["operation"]),
|
||||
copy.deepcopy(inp["scope"]),
|
||||
copy.deepcopy(inp["attributed"]),
|
||||
copy.deepcopy(inp["backed_up"]),
|
||||
)
|
||||
|
||||
|
||||
def kernel_classify(inp):
|
||||
return mu_loader.classify(**copy.deepcopy(inp))
|
||||
|
||||
|
||||
def facade_guard(inp):
|
||||
return oracle_mod.guard_mutation(copy.deepcopy(inp["mutation"]))
|
||||
|
||||
|
||||
def kernel_guard(inp):
|
||||
return mu_loader.guard(**copy.deepcopy(inp))
|
||||
|
||||
|
||||
# ── classify_mutation ────────────────────────────────────────────────────────
|
||||
|
||||
def cl_case(rec, name, target_kind, operation, scope="single",
|
||||
attributed=True, backed_up=False):
|
||||
inp = {"target_kind": target_kind, "operation": operation,
|
||||
"scope": scope, "attributed": attributed, "backed_up": backed_up}
|
||||
got = facade_classify(copy.deepcopy(inp))
|
||||
rec(name, inp, got, got)
|
||||
|
||||
|
||||
# DENY 1 — unattributed identity family (runs FIRST)
|
||||
def c_den1_identity(rec):
|
||||
cl_case(rec, "cl-deny1-unattributed-identity-overwrite",
|
||||
"identity", "overwrite", "single", False, False)
|
||||
|
||||
|
||||
def c_den1_persona_append_whole(rec):
|
||||
# append (op severity 1) would CONFIRM on persona; unattributed wins
|
||||
cl_case(rec, "cl-deny1-unattributed-persona-append-whole",
|
||||
"persona", "append", "whole", False, False)
|
||||
|
||||
|
||||
def c_den1_constitution_falsy_zero(rec):
|
||||
# attributed=0 is falsy (Python truthiness) → unattributed
|
||||
cl_case(rec, "cl-deny1-constitution-attributed-zero",
|
||||
"constitution", "append", "single", 0, False)
|
||||
|
||||
|
||||
def c_den1_beats_read(rec):
|
||||
# even a READ is denied when unattributed — precedence proof
|
||||
cl_case(rec, "cl-deny1-identity-read-still-denied",
|
||||
"identity", "read", "single", False, False)
|
||||
|
||||
|
||||
def c_den1_null_attributed(rec):
|
||||
# explicit None attributed → falsy → DENY
|
||||
cl_case(rec, "cl-deny1-identity-attributed-null",
|
||||
"identity", "append", "single", None, False)
|
||||
|
||||
|
||||
# DENY 2 — destructive without a snapshot
|
||||
def c_den2_constitution_delete_whole(rec):
|
||||
cl_case(rec, "cl-deny2-constitution-delete-whole",
|
||||
"constitution", "delete", "whole", True, False)
|
||||
|
||||
|
||||
def c_den2_skill_delete_multi(rec):
|
||||
cl_case(rec, "cl-deny2-skill-delete-multi",
|
||||
"skill", "delete", "multi", True, False)
|
||||
|
||||
|
||||
def c_den2_belief_overwrite_whole(rec):
|
||||
# op severity 2 + kind severity 2 boundary: still DENY
|
||||
cl_case(rec, "cl-deny2-belief-overwrite-whole-boundary",
|
||||
"belief", "overwrite", "whole", True, False)
|
||||
|
||||
|
||||
def c_den2_backed_up_zero(rec):
|
||||
# backed_up=0 is falsy → unbacked → DENY
|
||||
cl_case(rec, "cl-deny2-skill-overwrite-whole-backed-zero",
|
||||
"skill", "overwrite", "whole", True, 0)
|
||||
|
||||
|
||||
# CONFIRM — score >= 4 or identity-family mutating op
|
||||
def c_conf_backed_skill_overwrite(rec):
|
||||
cl_case(rec, "cl-confirm-backed-skill-overwrite-whole",
|
||||
"skill", "overwrite", "whole", True, True)
|
||||
|
||||
|
||||
def c_conf_identity_append_attributed(rec):
|
||||
# kind severity 3 + op severity 1 → CONFIRM even at score 4
|
||||
cl_case(rec, "cl-confirm-identity-append-attributed",
|
||||
"identity", "append", "single", True, False)
|
||||
|
||||
|
||||
def c_conf_identity_read_whole(rec):
|
||||
# read (op 0) escapes the identity rule; score 3+0+2=5 ≥ 4 → CONFIRM
|
||||
cl_case(rec, "cl-confirm-identity-read-whole-score",
|
||||
"identity", "read", "whole", True, False)
|
||||
|
||||
|
||||
def c_conf_operating_delete_single(rec):
|
||||
# score exactly 4 — the CONFIRM boundary
|
||||
cl_case(rec, "cl-confirm-operating-delete-single-score-4",
|
||||
"operating", "delete", "single", True, False)
|
||||
|
||||
|
||||
def c_conf_fact_overwrite_whole(rec):
|
||||
# op 2 + kind 1: no DENY (kind < 2), score 5 → CONFIRM
|
||||
cl_case(rec, "cl-confirm-fact-overwrite-whole",
|
||||
"fact", "overwrite", "whole", True, False)
|
||||
|
||||
|
||||
def c_conf_backed_truthy_string(rec):
|
||||
# backed_up="yes" is truthy → skips DENY 2 → CONFIRM
|
||||
cl_case(rec, "cl-confirm-skill-delete-multi-backed-truthy",
|
||||
"skill", "delete", "multi", True, "yes")
|
||||
|
||||
|
||||
def c_conf_constitution_delete_backed(rec):
|
||||
cl_case(rec, "cl-confirm-constitution-delete-whole-backed",
|
||||
"constitution", "delete", "whole", True, True)
|
||||
|
||||
|
||||
def c_conf_persona_overwrite_backed(rec):
|
||||
cl_case(rec, "cl-confirm-persona-overwrite-whole-backed",
|
||||
"persona", "overwrite", "whole", True, True)
|
||||
|
||||
|
||||
def c_conf_archive_delete_whole(rec):
|
||||
# archive (kind 0) still CONFIRMs at score 5 — score rule beats kind
|
||||
cl_case(rec, "cl-confirm-archive-delete-whole-score",
|
||||
"archive", "delete", "whole", True, False)
|
||||
|
||||
|
||||
def c_conf_unknown_kind_delete(rec):
|
||||
# unknown kind defaults to severity 1: 1+3+0=4 → CONFIRM
|
||||
cl_case(rec, "cl-confirm-unknown-kind-delete",
|
||||
"widget", "delete", "single", True, False)
|
||||
|
||||
|
||||
# ALLOW — everything else
|
||||
def c_allow_fact_append_single(rec):
|
||||
cl_case(rec, "cl-allow-fact-append-single-defaults",
|
||||
"fact", "append", "single", True, False)
|
||||
|
||||
|
||||
def c_allow_fact_append_multi(rec):
|
||||
# score 3 — directly below the CONFIRM boundary
|
||||
cl_case(rec, "cl-allow-fact-append-multi-score-3",
|
||||
"fact", "append", "multi", True, False)
|
||||
|
||||
|
||||
def c_allow_identity_read_single(rec):
|
||||
# identity + read + single: score 3, identity rule needs op ≥ 1
|
||||
cl_case(rec, "cl-allow-identity-read-single",
|
||||
"identity", "read", "single", True, False)
|
||||
|
||||
|
||||
def c_allow_archive_delete_single(rec):
|
||||
# kind severity 0: even delete is cheap on archives
|
||||
cl_case(rec, "cl-allow-archive-delete-single",
|
||||
"archive", "delete", "single", True, False)
|
||||
|
||||
|
||||
def c_allow_archive_read_whole(rec):
|
||||
cl_case(rec, "cl-allow-archive-read-whole",
|
||||
"archive", "read", "whole", True, False)
|
||||
|
||||
|
||||
def c_allow_fact_read_whole(rec):
|
||||
cl_case(rec, "cl-allow-fact-read-whole",
|
||||
"fact", "read", "whole", True, False)
|
||||
|
||||
|
||||
def c_allow_unknown_operation(rec):
|
||||
# unknown op defaults to severity 1: 1+1+1=3 → ALLOW
|
||||
cl_case(rec, "cl-allow-unknown-operation",
|
||||
"fact", "nuke", "multi", True, False)
|
||||
|
||||
|
||||
def c_allow_unknown_scope(rec):
|
||||
# unknown scope defaults to severity 0
|
||||
cl_case(rec, "cl-allow-unknown-scope",
|
||||
"fact", "append", "galaxy", True, False)
|
||||
|
||||
|
||||
def c_allow_null_kind(rec):
|
||||
# None kind: severity default + f-string renders "None"
|
||||
cl_case(rec, "cl-allow-null-kind-renders-None",
|
||||
None, "append", "single", True, False)
|
||||
|
||||
|
||||
def c_allow_null_operation(rec):
|
||||
cl_case(rec, "cl-allow-null-operation-renders-None",
|
||||
"fact", None, "single", True, False)
|
||||
|
||||
|
||||
def c_allow_null_scope(rec):
|
||||
# None scope: severity 0 (dict lookup miss)
|
||||
cl_case(rec, "cl-allow-null-scope",
|
||||
"fact", "append", None, True, False)
|
||||
|
||||
|
||||
# ── guard_mutation ───────────────────────────────────────────────────────────
|
||||
|
||||
def gu_case(rec, name, mutation):
|
||||
inp = {"mutation": mutation}
|
||||
got = facade_guard(copy.deepcopy(inp))
|
||||
rec(name, inp, got, got)
|
||||
|
||||
|
||||
def g_empty_dict_defaults(rec):
|
||||
gu_case(rec, "gu-empty-dict-all-defaults", {})
|
||||
|
||||
|
||||
def g_fact_append_defaults(rec):
|
||||
gu_case(rec, "gu-fact-append-allow", {"target_kind": "fact",
|
||||
"operation": "append"})
|
||||
|
||||
|
||||
def g_unattributed_identity(rec):
|
||||
gu_case(rec, "gu-unattributed-identity-deny",
|
||||
{"target_kind": "identity", "operation": "overwrite",
|
||||
"attributed": False})
|
||||
|
||||
|
||||
def g_unbacked_constitution_delete(rec):
|
||||
gu_case(rec, "gu-unbacked-constitution-delete-deny",
|
||||
{"target_kind": "constitution", "operation": "delete",
|
||||
"scope": "whole", "attributed": True, "backed_up": False})
|
||||
|
||||
|
||||
def g_backed_skill_confirms(rec):
|
||||
gu_case(rec, "gu-backed-skill-overwrite-confirm",
|
||||
{"target_kind": "skill", "operation": "overwrite",
|
||||
"scope": "whole", "attributed": True, "backed_up": True})
|
||||
|
||||
|
||||
def g_null_attributed_present(rec):
|
||||
# present-but-null attributed is FALSY — the default True applies
|
||||
# only to a MISSING key
|
||||
gu_case(rec, "gu-attributed-null-falsy-deny",
|
||||
{"target_kind": "identity", "attributed": None})
|
||||
|
||||
|
||||
def g_empty_list_attributed(rec):
|
||||
gu_case(rec, "gu-attributed-empty-list-falsy-deny",
|
||||
{"target_kind": "persona", "operation": "delete",
|
||||
"attributed": []})
|
||||
|
||||
|
||||
def g_backed_up_zero(rec):
|
||||
gu_case(rec, "gu-backed-up-zero-falsy-deny",
|
||||
{"target_kind": "skill", "operation": "overwrite",
|
||||
"scope": "whole", "backed_up": 0})
|
||||
|
||||
|
||||
def g_backed_up_truthy_string(rec):
|
||||
gu_case(rec, "gu-backed-up-truthy-string-confirm",
|
||||
{"target_kind": "skill", "operation": "overwrite",
|
||||
"scope": "whole", "backed_up": "yes"})
|
||||
|
||||
|
||||
def g_read_allows(rec):
|
||||
gu_case(rec, "gu-read-allows", {"target_kind": "fact",
|
||||
"operation": "read"})
|
||||
|
||||
|
||||
def g_extra_keys_ignored(rec):
|
||||
gu_case(rec, "gu-extra-keys-ignored",
|
||||
{"target_kind": "fact", "operation": "append",
|
||||
"note": "x", "weight": 9})
|
||||
|
||||
|
||||
def g_null_target_kind(rec):
|
||||
# present-null target_kind beats the "fact" default (get quirk);
|
||||
# severity defaults to 1, f-string renders "None"
|
||||
gu_case(rec, "gu-null-target-kind-renders-None",
|
||||
{"target_kind": None})
|
||||
|
||||
|
||||
def g_empty_operation_string(rec):
|
||||
# empty op string: severity default 1, f-string renders ""
|
||||
gu_case(rec, "gu-empty-operation-string",
|
||||
{"target_kind": "fact", "operation": ""})
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
CLASSIFY = [
|
||||
c_den1_identity, c_den1_persona_append_whole,
|
||||
c_den1_constitution_falsy_zero, c_den1_beats_read,
|
||||
c_den1_null_attributed, c_den2_constitution_delete_whole,
|
||||
c_den2_skill_delete_multi, c_den2_belief_overwrite_whole,
|
||||
c_den2_backed_up_zero, c_conf_backed_skill_overwrite,
|
||||
c_conf_identity_append_attributed, c_conf_identity_read_whole,
|
||||
c_conf_operating_delete_single, c_conf_fact_overwrite_whole,
|
||||
c_conf_backed_truthy_string, c_conf_constitution_delete_backed,
|
||||
c_conf_persona_overwrite_backed, c_conf_archive_delete_whole,
|
||||
c_conf_unknown_kind_delete, c_allow_fact_append_single,
|
||||
c_allow_fact_append_multi, c_allow_identity_read_single,
|
||||
c_allow_archive_delete_single, c_allow_archive_read_whole,
|
||||
c_allow_fact_read_whole, c_allow_unknown_operation,
|
||||
c_allow_unknown_scope, c_allow_null_kind, c_allow_null_operation,
|
||||
c_allow_null_scope,
|
||||
]
|
||||
|
||||
GUARD = [
|
||||
g_empty_dict_defaults, g_fact_append_defaults,
|
||||
g_unattributed_identity, g_unbacked_constitution_delete,
|
||||
g_backed_skill_confirms, g_null_attributed_present,
|
||||
g_empty_list_attributed, g_backed_up_zero,
|
||||
g_backed_up_truthy_string, g_read_allows, g_extra_keys_ignored,
|
||||
g_null_target_kind, g_empty_operation_string,
|
||||
]
|
||||
|
||||
GROUPS = {
|
||||
"classify": (CLASSIFY, True, "mu_classify", facade_classify,
|
||||
kernel_classify),
|
||||
"guard": (GUARD, True, "mu_guard", facade_guard, kernel_guard),
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization (key-order independent)."""
|
||||
return json.dumps(json.loads(json.dumps(x, default=str)), sort_keys=True)
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, (cases, facade_replayable, _sym, fcall, kcall) in GROUPS.items():
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, oracle_ret, expected, _r=recs, _g=group):
|
||||
entry = {"name": name, "input": inp,
|
||||
"return": oracle_ret, "expected": expected}
|
||||
if REPLAY and facade_replayable:
|
||||
old = next(c for c in frozen[_g] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{_g}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
_r.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_groups = [g for g, (_, r, _, _, _) in GROUPS.items() if r]
|
||||
facade_total = sum(len(groups[g]) for g in facade_groups)
|
||||
facade_failures = sum(1 for g in facade_groups for c in groups[g]
|
||||
if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
checked = 0
|
||||
for group, cases in frozen.items():
|
||||
sym_fns = {"classify": kernel_classify, "guard": kernel_guard}
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = sym_fns[group](case["input"])
|
||||
want = case["expected"]
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
print(f"replay: facade {facade_total - facade_failures}/{facade_total}, "
|
||||
f"kernel {checked - failures}/{checked}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/mutation_guards.py",
|
||||
"pin": "RUNE_MG_CORE=python",
|
||||
"note": "Frozen via scripts/gen_mutation_guards_truth.py against "
|
||||
"the live Python oracle. Battery embeds this VERBATIM.",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze spine_language oracle truth for the Rust port battery
|
||||
(dichotomy wave #2479 tranche, module spine_language).
|
||||
|
||||
Frozen surface (all 5 module functions, every branch):
|
||||
_classification_sides() — measured-map read + degrade-to-{}
|
||||
_place_languages(doc) — kind defaults / measured authority /
|
||||
name hints / subsystem majority /
|
||||
unclassified / unassessed / placed count
|
||||
_contract_discoveries() — closed-contract gather + kind filters
|
||||
_normalize_discovery_ids(doc) — legacy rename + demotion + tag filter
|
||||
_apply_contract_discoveries(doc) — high-water merge write-back
|
||||
|
||||
Store reads (store/rebase/classification.json,
|
||||
store/scaffold/current.json) are materialized per case under a temp
|
||||
root (sl.ROOT monkeypatched) so the frozen truth carries its fixtures:
|
||||
each case records `files` {relpath: content}; the Rust battery
|
||||
materializes the same tree. Wall-clock `observed_at` stamps (from
|
||||
structural_store.add_entity) are masked on BOTH sides — the only
|
||||
non-deterministic field in the module. RUNE_SL_CORE=python pinned
|
||||
(POISONED-TRUTH LAW: freeze the oracle, never a kernel). Inputs
|
||||
deep-copied before capture.
|
||||
|
||||
Output: src/rust/spine_language/frozen_sl_truth.json — replayed
|
||||
VERBATIM by src/rust/spine_language/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_SL_CORE"] = "python" # freeze the ORACLE, never a kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import spine_language as sl # noqa: E402
|
||||
|
||||
cases: list[dict] = []
|
||||
|
||||
|
||||
def mask(obj):
|
||||
"""Drop wall-clock observed_at stamps recursively (the module's only
|
||||
nondeterministic output field)."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: mask(v) for k, v in obj.items() if k != "observed_at"}
|
||||
if isinstance(obj, list):
|
||||
return [mask(v) for v in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def freeze(fn_name: str, name: str, *, files: dict | None = None,
|
||||
doc: dict | None = None, run):
|
||||
"""Materialize fixture files under a temp root, run the oracle fn with
|
||||
sl.ROOT pointed there, record the case."""
|
||||
root = Path(tempfile.mkdtemp(prefix="sl-truth-"))
|
||||
for rel, content in (files or {}).items():
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(content)
|
||||
original_root = sl.ROOT
|
||||
sl.ROOT = root
|
||||
try:
|
||||
doc_in = copy.deepcopy(doc) if doc is not None else None
|
||||
expected = run(copy.deepcopy(doc) if doc is not None else None)
|
||||
finally:
|
||||
sl.ROOT = original_root
|
||||
cases.append({
|
||||
"fn": fn_name,
|
||||
"name": name,
|
||||
"files": files or {},
|
||||
"doc": doc_in,
|
||||
"expected": mask(expected),
|
||||
})
|
||||
|
||||
|
||||
CLASSIFICATION = json.dumps({
|
||||
"spine": {"side": "RUST"},
|
||||
"spine_language": {"side": "RUST"},
|
||||
"behavior": {"side": "RUST"},
|
||||
"identity_anchors": {"side": "RUST"},
|
||||
"persona_definition": {"side": "RUST"},
|
||||
"persona_gate": {"side": "RUST"},
|
||||
"prompt_sync": {"side": "GO"},
|
||||
"voice_hybrid": {"side": "GO"},
|
||||
"voice_sync": {"side": "GO"},
|
||||
"rune_tui": {"side": "TUI-LAST"},
|
||||
"testkit": {"side": "TEST-INFRA"},
|
||||
"legacy_tool": {"side": "RETIRES"},
|
||||
"lowercase_mod": {"side": "rust"},
|
||||
"num_side": {"side": 5},
|
||||
"no_side": {},
|
||||
"not_a_dict": "skip me",
|
||||
}, indent=1)
|
||||
|
||||
|
||||
def cls_file(extra: dict | None = None) -> dict:
|
||||
data = json.loads(CLASSIFICATION)
|
||||
data.update(extra or {})
|
||||
return {"store/rebase/classification.json": json.dumps(data)}
|
||||
|
||||
|
||||
# ── _classification_sides ────────────────────────────────────────────────
|
||||
freeze("classification_sides", "measured-map", files=cls_file(),
|
||||
run=lambda _doc: sl._classification_sides())
|
||||
freeze("classification_sides", "missing-file-degrades", files={},
|
||||
run=lambda _doc: sl._classification_sides())
|
||||
freeze("classification_sides", "malformed-json-degrades",
|
||||
files={"store/rebase/classification.json": "{oops"},
|
||||
run=lambda _doc: sl._classification_sides())
|
||||
freeze("classification_sides", "non-dict-top-level-degrades",
|
||||
files={"store/rebase/classification.json": "[1, 2, 3]"},
|
||||
run=lambda _doc: sl._classification_sides())
|
||||
|
||||
|
||||
# ── _place_languages ─────────────────────────────────────────────────────
|
||||
def run_place(doc):
|
||||
placed = sl._place_languages(doc)
|
||||
return {"doc": doc, "placed": placed}
|
||||
|
||||
|
||||
def ent(eid, kind, **kw):
|
||||
e = {"id": eid, "kind": kind}
|
||||
e.update(kw)
|
||||
return e
|
||||
|
||||
|
||||
freeze("place_languages", "kind-defaults", files=cls_file(), doc={
|
||||
"entities": [
|
||||
ent("rust-crate:cognition_core", "rust-crate"),
|
||||
ent("go-surface:rune-entry", "go-surface"),
|
||||
ent("workflow:ci.yml", "workflow"),
|
||||
ent("config:settings.toml", "config"),
|
||||
ent("contract:BUILDER-x", "contract"),
|
||||
ent("deployment_surface:prod", "deployment_surface"),
|
||||
ent("module:zzz_unknown", "module"),
|
||||
ent("widget:odd", "widget"),
|
||||
]}, run=run_place)
|
||||
|
||||
freeze("place_languages", "measured-authoritative", files=cls_file(), doc={
|
||||
"entities": [
|
||||
ent("module:spine", "module"),
|
||||
ent("module:behavior", "module"),
|
||||
ent("module:prompt_sync", "module"),
|
||||
ent("module:rune_tui", "module"),
|
||||
ent("module:testkit", "module"),
|
||||
ent("module:legacy_tool", "module"),
|
||||
ent("module:lowercase_mod", "module"),
|
||||
ent("module:num_side", "module"),
|
||||
ent("module:no_side", "module"),
|
||||
ent("module:totally_unknown_name", "module"),
|
||||
]}, run=run_place)
|
||||
|
||||
freeze("place_languages", "hints-only-when-unmeasured", files=cls_file(), doc={
|
||||
"entities": [
|
||||
ent("module:server_farm", "module"),
|
||||
ent("module:memory_drift", "module"),
|
||||
ent("module:tui_widgets", "module"),
|
||||
ent("module:dispatch_tui", "module"),
|
||||
ent("module:watch_server", "module"),
|
||||
]}, run=run_place)
|
||||
|
||||
freeze("place_languages", "existing-language-overwritten", files=cls_file(), doc={
|
||||
"entities": [
|
||||
ent("module:spine", "module", identities={"language": "go"}),
|
||||
ent("widget:odd", "widget", identities={"other": "x"}),
|
||||
ent("bare-no-identities", "widget"),
|
||||
]}, run=run_place)
|
||||
|
||||
freeze("place_languages", "subsystem-majority", files=cls_file(), doc={
|
||||
"entities": [
|
||||
ent("subsystem:identity", "subsystem"),
|
||||
ent("subsystem:allrust", "subsystem"),
|
||||
ent("subsystem:allgo", "subsystem"),
|
||||
ent("subsystem:unassessed", "subsystem"),
|
||||
*[ent(f"module:{m}", "module") for m in (
|
||||
"spine", "behavior", "identity_anchors", # 3 rust
|
||||
"prompt_sync", "voice_hybrid", # 2 go
|
||||
"zzz_unknown")], # 1 unmeasured
|
||||
],
|
||||
"relationships": [
|
||||
*[{"type": "belongs_to", "from": f"module:{m}",
|
||||
"to": "subsystem:identity", "evidence_refs": []} for m in (
|
||||
"spine", "behavior", "identity_anchors",
|
||||
"prompt_sync", "voice_hybrid", "zzz_unknown")],
|
||||
{"type": "belongs_to", "from": "module:spine",
|
||||
"to": "subsystem:allrust", "evidence_refs": []},
|
||||
{"type": "belongs_to", "from": "module:prompt_sync",
|
||||
"to": "subsystem:allgo", "evidence_refs": []},
|
||||
]}, run=run_place)
|
||||
|
||||
freeze("place_languages", "no-entities-key", files=cls_file(), doc={
|
||||
"schema_version": 1}, run=run_place)
|
||||
|
||||
|
||||
# ── _contract_discoveries ────────────────────────────────────────────────
|
||||
def scaffold_doc() -> dict:
|
||||
return {
|
||||
"entities": [
|
||||
{"id": "contract:C1", "kind": "contract", "phase": "closed",
|
||||
"ext": {"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:kernel_x",
|
||||
"config:app.yml",
|
||||
"bogus-kind:x",
|
||||
"suite"],
|
||||
"candidate_corrections": [
|
||||
{"entity": "suite"},
|
||||
{"entity": "workflow:sync.yml"},
|
||||
{"entity": "bogus-kind:y"},
|
||||
{"entity": ""},
|
||||
]}}},
|
||||
{"id": "contract:C2", "kind": "contract", "phase": "building",
|
||||
"ext": {"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:never"]}}},
|
||||
{"id": "contract:C3", "kind": "contract", "phase": "closed"},
|
||||
{"id": "contract:C4", "kind": "contract",
|
||||
"ext": {"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:also-never"]}}},
|
||||
{"id": "module:plain", "kind": "module", "phase": "closed"},
|
||||
]}
|
||||
|
||||
|
||||
def run_discoveries(_doc):
|
||||
got = sl._contract_discoveries()
|
||||
return json.loads(json.dumps(got)) # tuples -> arrays for JSON
|
||||
|
||||
|
||||
def scaffold_files() -> dict:
|
||||
return {"store/scaffold/current.json": json.dumps(scaffold_doc())}
|
||||
|
||||
|
||||
freeze("contract_discoveries", "closed-contract-gather",
|
||||
files=scaffold_files(), run=run_discoveries)
|
||||
freeze("contract_discoveries", "missing-store", files={}, run=run_discoveries)
|
||||
freeze("contract_discoveries", "malformed-store",
|
||||
files={"store/scaffold/current.json": "{oops"}, run=run_discoveries)
|
||||
freeze("contract_discoveries", "no-entities-key",
|
||||
files={"store/scaffold/current.json": json.dumps({"schema_version": 1})},
|
||||
run=run_discoveries)
|
||||
|
||||
|
||||
# ── _normalize_discovery_ids ─────────────────────────────────────────────
|
||||
def run_normalize(doc):
|
||||
renamed = sl._normalize_discovery_ids(doc)
|
||||
return {"doc": doc, "renamed": renamed}
|
||||
|
||||
|
||||
freeze("normalize_discovery_ids", "legacy-module-renamed-and-demoted",
|
||||
files=scaffold_files(), doc={
|
||||
"entities": [
|
||||
{"id": "src/rune/claim_audit.py", "kind": "module",
|
||||
"maturity": "PRESENT", "tags": ["code", "keep"],
|
||||
"evidence_refs": ["contract-discovery:old"]},
|
||||
],
|
||||
"relationships": [
|
||||
{"type": "depends_on", "from": "src/rune/claim_audit.py",
|
||||
"to": "module:other", "evidence_refs": []},
|
||||
{"type": "depends_on", "from": "module:x",
|
||||
"to": "src/rune/claim_audit.py", "evidence_refs": []},
|
||||
]}, run=run_normalize)
|
||||
|
||||
freeze("normalize_discovery_ids", "observed-retains-present",
|
||||
files={"store/scaffold/current.json": json.dumps({
|
||||
"entities": [{"id": "contract:C1", "kind": "contract",
|
||||
"phase": "closed",
|
||||
"ext": {"contract_quality": {
|
||||
"expected_new_observed": [
|
||||
"construction-surface:src/rune/kept.py"]}}}]})},
|
||||
doc={"entities": [
|
||||
{"id": "src/rune/kept.py", "kind": "module", "maturity": "PRESENT",
|
||||
"tags": ["code"], "evidence_refs": ["contract-discovery:C1"]},
|
||||
]}, run=run_normalize)
|
||||
|
||||
freeze("normalize_discovery_ids", "already-surface-demotion-and-tag-filter",
|
||||
files=scaffold_files(), doc={
|
||||
"entities": [
|
||||
{"id": "construction-surface:foo", "kind": "construction-surface",
|
||||
"maturity": "PRESENT", "tags": ["code", "keep"],
|
||||
"evidence_refs": []},
|
||||
]}, run=run_normalize)
|
||||
|
||||
freeze("normalize_discovery_ids", "no-discovery-ref-no-rename",
|
||||
files=scaffold_files(), doc={
|
||||
"entities": [
|
||||
{"id": "src/rune/plain.py", "kind": "module",
|
||||
"maturity": "PRESENT", "tags": [], "evidence_refs": ["spine"]},
|
||||
{"id": "src/rune/noref.py", "kind": "module",
|
||||
"maturity": "PRESENT", "tags": []},
|
||||
{"id": "qualified:still.py", "kind": "module",
|
||||
"maturity": "PRESENT",
|
||||
"evidence_refs": ["contract-discovery:old"]},
|
||||
]}, run=run_normalize)
|
||||
|
||||
|
||||
# ── _apply_contract_discoveries ──────────────────────────────────────────
|
||||
def run_apply(doc):
|
||||
placed = sl._apply_contract_discoveries(doc)
|
||||
return {"doc": doc, "placed": placed}
|
||||
|
||||
|
||||
freeze("apply_contract_discoveries", "observed-and-candidate-writeback",
|
||||
files=scaffold_files(), doc={"entities": [], "relationships": []},
|
||||
run=run_apply)
|
||||
|
||||
freeze("apply_contract_discoveries", "high-water-merge",
|
||||
files=scaffold_files(), doc={
|
||||
"entities": [
|
||||
{"id": "rust-crate:kernel_x", "kind": "rust-crate",
|
||||
"maturity": "DECLARED", "tags": ["seed"],
|
||||
"identities": {"keep": "me"}, "evidence_refs": ["seed-ref"]},
|
||||
{"id": "config:app.yml", "kind": "config",
|
||||
"maturity": "WIRED", "tags": [],
|
||||
"evidence_refs": []},
|
||||
], "relationships": []}, run=run_apply)
|
||||
|
||||
freeze("apply_contract_discoveries", "double-prefix-quirk",
|
||||
files={"store/scaffold/current.json": json.dumps({
|
||||
"entities": [{"id": "contract:C1", "kind": "contract",
|
||||
"phase": "closed",
|
||||
"ext": {"contract_quality": {
|
||||
"expected_new_observed": [
|
||||
"construction-surface:foo"]}}}]})},
|
||||
doc={"entities": [], "relationships": []}, run=run_apply)
|
||||
|
||||
freeze("apply_contract_discoveries", "empty-discoveries-noop",
|
||||
files={"store/scaffold/current.json": json.dumps({
|
||||
"entities": [{"id": "contract:C2", "kind": "contract",
|
||||
"phase": "building"}]})},
|
||||
doc={"entities": [], "relationships": []}, run=run_apply)
|
||||
|
||||
freeze("apply_contract_discoveries", "missing-store-noop", files={},
|
||||
doc={"entities": [], "relationships": []}, run=run_apply)
|
||||
|
||||
|
||||
truth = {
|
||||
"frozen": "spine_language doc-level transforms (all 5 fns, every branch; "
|
||||
"observed_at masked both sides; store fixtures carried per case)",
|
||||
"oracle": "src/rune/spine_language.py",
|
||||
"pin": "RUNE_SL_CORE=python",
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
out = Path(__file__).resolve().parents[1] / "src" / "rust" / "spine_language" / "frozen_sl_truth.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(truth, indent=1) + "\n")
|
||||
print(f"froze {len(cases)} cases -> {out}")
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for
|
||||
abstraction_ladder. Full lifecycle in ONE process (shared-store clobber
|
||||
hazard)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
PARITY_SHA = "4d0883b3"
|
||||
PHASE_B_SHA = "00bf4d21"
|
||||
|
||||
iface = ConstructionInterface(builder="factory-wave")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Port src/rune/abstraction_ladder.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_AL_CORE=python pin, frozen-truth battery, "
|
||||
"live parity 100%, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:400])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:300])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": f"abstraction_ladder parity port — parity {PARITY_SHA}, "
|
||||
f"phase B {PHASE_B_SHA} (SHAs reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/abstraction_ladder.py",
|
||||
"src/rune/al_loader.py",
|
||||
"src/rust/abstraction_ladder/src/lib.rs",
|
||||
"src/rust/abstraction_ladder/src/ladder.rs",
|
||||
"src/rust/abstraction_ladder/src/branch_battery.rs",
|
||||
"scripts/gen_abstraction_ladder_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
print("CID_LINE", cid)
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for behaviour_ledger.
|
||||
Full lifecycle in ONE process (shared-store clobber hazard)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-wave")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Port src/rune/behaviour_ledger.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_BL_CORE=python pin, frozen-truth battery, "
|
||||
"live parity 100%, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:400])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:300])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
print("PENDING_CONTRACT", cid)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for experience_encoding.
|
||||
Full lifecycle in ONE process (shared-store clobber hazard)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-wave")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Port src/rune/experience_encoding.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_EE_CORE=python pin, frozen-truth battery, "
|
||||
"live parity 100%, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:400])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:300])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "experience_encoding parity port — evidence attached at close "
|
||||
"(SHAs reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/experience_encoding.py",
|
||||
"src/rune/ee_loader.py",
|
||||
"src/rust/experience_encoding/src/lib.rs",
|
||||
"src/rust/experience_encoding/src/branch_battery.rs",
|
||||
"scripts/gen_experience_encoding_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for mutation_guards.
|
||||
Full lifecycle in ONE process (shared-store clobber hazard)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-wave")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Port src/rune/mutation_guards.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_MG_CORE=python pin, frozen-truth battery, "
|
||||
"live parity 100%, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:400])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:300])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "mutation_guards parity port — evidence attached at close "
|
||||
"(SHAs reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/mutation_guards.py",
|
||||
"src/rune/mu_loader.py",
|
||||
"src/rust/mutation_guards/src/lib.rs",
|
||||
"src/rust/mutation_guards/src/branch_battery.rs",
|
||||
"scripts/gen_mutation_guards_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open → walk → close the #2479 dichotomy-wave construction contract for
|
||||
spine_language. Full lifecycle in ONE process (shared-store clobber hazard)."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-wave")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Port src/rune/spine_language.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-level transforms (#2465 language placement, "
|
||||
"discovery-ID normalization, contract-discovery write-back), doc-envelope "
|
||||
"FFI, ctypes loader, facade delegation with RUNE_SL_CORE=python pin, "
|
||||
"frozen-truth battery, live parity 100%, Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:300])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:200])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": "spine_language parity port — 23 frozen cases replay verbatim, "
|
||||
"live parity 326/326 (frozen + fuzz seed 2479 + real-repo "
|
||||
"pin-vs-auto), clippy 0, python tests green both pins "
|
||||
"(SHAs in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/spine_language.py",
|
||||
"src/rune/sl_loader.py",
|
||||
"src/rust/spine_language/src/lib.rs",
|
||||
"src/rust/spine_language/src/spine.rs",
|
||||
"src/rust/spine_language/src/branch_battery.rs",
|
||||
"src/rust/spine_language/frozen_sl_truth.json",
|
||||
"scripts/gen_spine_language_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:300])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.231.0"
|
||||
__version__ = "0.238.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
|
||||
|
||||
@@ -5,10 +5,35 @@ episode → fact → observation → concept → belief → rule → skill.
|
||||
Each promotion needs: evidence threshold, provenance, contradiction
|
||||
handling, prediction/behavioural consequence, outcome-based revision.
|
||||
Schema alone does not count — the ladder moves items or it doesn't.
|
||||
|
||||
Dichotomy wave #2479 tranche: the ladder kernel is ported to FULL Rust
|
||||
(src/rust/abstraction_ladder, parity oracle frozen_al_truth.json). This
|
||||
facade delegates to the Rust core via rune/al_loader when built; the
|
||||
Python implementation below is the strangler fallback, pinned with
|
||||
RUNE_AL_CORE=python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust abstraction_ladder core when available (strangler: the
|
||||
Python fallback is load-bearing). RUNE_AL_CORE=python pins the
|
||||
fallback."""
|
||||
if os.environ.get("RUNE_AL_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import al_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback
|
||||
return None
|
||||
try:
|
||||
return al_loader if al_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash
|
||||
return None
|
||||
|
||||
|
||||
STAGES = ("episode", "fact", "observation", "concept", "belief", "rule", "skill")
|
||||
|
||||
# Evidence threshold per promotion (sightings required)
|
||||
@@ -25,6 +50,9 @@ THRESHOLDS = {
|
||||
|
||||
def next_stage(stage: str) -> str | None:
|
||||
"""The stage after this one (None at the top)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.next_stage(stage)
|
||||
try:
|
||||
i = STAGES.index(stage)
|
||||
except ValueError:
|
||||
@@ -36,6 +64,9 @@ def can_promote(item: dict) -> dict:
|
||||
"""Can this item promote? Needs: evidence threshold met, provenance
|
||||
present, no open contradictions, a stated consequence, and (for
|
||||
belief+) a verified outcome."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.can_promote(item)
|
||||
stage = item.get("stage", "episode")
|
||||
nxt = next_stage(stage)
|
||||
if nxt is None:
|
||||
@@ -59,6 +90,9 @@ def can_promote(item: dict) -> dict:
|
||||
|
||||
def promote(item: dict) -> dict:
|
||||
"""Promote one stage (or return the refusal)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.promote(item)
|
||||
verdict = can_promote(item)
|
||||
if not verdict["promote"]:
|
||||
return {"item": item, **verdict}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""al_loader.py — ctypes loader for the Rust abstraction_ladder core
|
||||
(dichotomy wave #2479 tranche). Mirrors lc_loader.py / broker_loader.py:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* al_next_stage({"stage"}) -> stage string or null
|
||||
* al_can_promote({"item": {...}}) -> verdict doc
|
||||
* al_promote({"item": {...}}) -> {"item": ..., ...verdict}
|
||||
|
||||
Pure kernel: no ops, no store seams, no op callback.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). NEVER re-export
|
||||
a free symbol here. Override the .so with AL_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" / "abstraction_ladder"
|
||||
LIB_PATH = Path(os.environ.get("AL_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" /
|
||||
"libabstraction_ladder.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
SYMBOLS = ("al_next_stage", "al_can_promote", "al_promote")
|
||||
|
||||
|
||||
class AbstractionLadderCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust abstraction_ladder core is not built. Callers fall back
|
||||
to Python (strangler law; RUNE_AL_CORE=python pins the fallback)."""
|
||||
|
||||
|
||||
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 AbstractionLadderCoreUnavailable(str(LIB_PATH))
|
||||
lib = ctypes.CDLL(str(LIB_PATH))
|
||||
for sym in SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
"""Free via capability_scoring's cs_free_string (same process)."""
|
||||
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
|
||||
|
||||
|
||||
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(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def next_stage(stage):
|
||||
return _call("al_next_stage", {"stage": stage})
|
||||
|
||||
|
||||
def can_promote(item: dict) -> dict:
|
||||
return _call("al_can_promote", {"item": item})
|
||||
|
||||
|
||||
def promote(item: dict) -> dict:
|
||||
return _call("al_promote", {"item": item})
|
||||
@@ -17,23 +17,26 @@ reason; unrelated hardened confirmations are preserved.
|
||||
|
||||
Storage: <project_store>/behaviour_ledger.json (versioned document).
|
||||
|
||||
Phase G strangler slice 2 (port 4): the state machine (define / get /
|
||||
transitions / selective invalidation / queues / summary) is served
|
||||
IN-PROCESS by the Rust `evidence_provenance` cdylib through ep_loader.py
|
||||
— parity 13/13 proven vs this module's Python vintage (full-scenario
|
||||
doc parity, byte-equal error messages, save render indent=1 sort_keys).
|
||||
File glue (load/save) stays Python; timestamps are pinned by this shim
|
||||
(`now`) so the core stays deterministic. State constants and class API
|
||||
unchanged (git history holds the original).
|
||||
Dichotomy wave #2479: the state machine (define / get / transitions /
|
||||
selective invalidation / queues / summary) is served by the FULL Rust
|
||||
`behaviour_ledger` cdylib through bl_loader.py — the kernel is a pure
|
||||
function of the parsed ledger doc, timestamps pass IN (`now`, no-clock
|
||||
law), and parity is pinned by the frozen truth battery embedded in the
|
||||
crate (frozen_bl_truth.json). RUNE_BL_CORE=python pins the retired
|
||||
evidence_provenance shim path (ep_loader) for parity harnesses. File
|
||||
glue (load/save) stays Python. State constants and class API unchanged
|
||||
(git history holds the original).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# ── Rust core wiring (slice-2 port-4 shim pattern) ──────────────────────────
|
||||
# ── retired shim wiring (RUNE_BL_CORE=python pin; fallback when the
|
||||
# wave core is not built) ────────────────────────────────────────────────
|
||||
from rune import paths # noqa: E402 (#41 centralized paths)
|
||||
_CRATE_DIR = str(paths.crate_dir("evidence_provenance")) # #41 centralized
|
||||
if _CRATE_DIR not in sys.path:
|
||||
@@ -63,6 +66,24 @@ STATES = (UNVALIDATED, IMPLEMENTED, AUTO_VERIFIED, REAL_WORLD_PASSED,
|
||||
CONFIRMED_STATES = (AUTO_VERIFIED, REAL_WORLD_PASSED, USER_CONFIRMED)
|
||||
|
||||
|
||||
def _kernel():
|
||||
"""Dichotomy wave #2479: the FULL Rust behaviour_ledger core first
|
||||
(RUNE_BL_CORE=python pins the retired evidence_provenance shim —
|
||||
parity harnesses). The kernel owns the state machine (define / get /
|
||||
transitions / selective invalidation / queues / summary); the file
|
||||
glue and the class API stay here."""
|
||||
if os.environ.get("RUNE_BL_CORE", "auto") == "python":
|
||||
return ep_loader
|
||||
try:
|
||||
from rune import bl_loader
|
||||
|
||||
if bl_loader.available():
|
||||
return bl_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return ep_loader
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
@@ -93,7 +114,7 @@ class BehaviourLedger:
|
||||
dependencies: list[str] | None = None,
|
||||
automated_ref: str = "",
|
||||
environment: str = "") -> dict:
|
||||
self.doc, rec = ep_loader.bl_define(
|
||||
self.doc, rec = _kernel().bl_define(
|
||||
self.doc, behaviour_id=behaviour_id,
|
||||
owning_project=owning_project,
|
||||
intended_behaviour=intended_behaviour,
|
||||
@@ -109,7 +130,7 @@ class BehaviourLedger:
|
||||
return rec
|
||||
|
||||
def get(self, behaviour_id: str) -> dict | None:
|
||||
return ep_loader.bl_get(self.doc, behaviour_id)
|
||||
return _kernel().bl_get(self.doc, behaviour_id)
|
||||
|
||||
# -- state transitions (all recorded in history — rule 19G) -------------
|
||||
|
||||
@@ -118,7 +139,7 @@ class BehaviourLedger:
|
||||
generation: str = "") -> dict:
|
||||
"""Automated verification. GREEN NEVER IMPLIES USER_CONFIRMED
|
||||
(rule 19W-22): the ceiling here is AUTO_VERIFIED."""
|
||||
self.doc, rec = ep_loader.bl_record_auto(
|
||||
self.doc, rec = _kernel().bl_record_auto(
|
||||
self.doc, behaviour_id, passed=passed, detail=detail,
|
||||
generation=generation, now=_now())
|
||||
return rec
|
||||
@@ -128,14 +149,14 @@ class BehaviourLedger:
|
||||
confirmed_by: str = "user") -> dict:
|
||||
"""Real-world/manual test result (testing mode). USER_CONFIRMED
|
||||
requires an explicit confirmed_by='user' pass (rule 19M)."""
|
||||
self.doc, rec = ep_loader.bl_record_real_world(
|
||||
self.doc, rec = _kernel().bl_record_real_world(
|
||||
self.doc, behaviour_id, passed=passed, detail=detail,
|
||||
generation=generation, confirmed_by=confirmed_by, now=_now())
|
||||
return rec
|
||||
|
||||
def accept_limitation(self, behaviour_id: str, *, note: str,
|
||||
approved_by: str = "user") -> dict:
|
||||
self.doc, rec = ep_loader.bl_accept_limitation(
|
||||
self.doc, rec = _kernel().bl_accept_limitation(
|
||||
self.doc, behaviour_id, note=note,
|
||||
approved_by=approved_by, now=_now())
|
||||
return rec
|
||||
@@ -150,7 +171,7 @@ class BehaviourLedger:
|
||||
structural dependencies intersect the changed closure. Every
|
||||
invalidation carries a reason; unrelated confirmations stay.
|
||||
Returns the invalidated records."""
|
||||
self.doc, recs = ep_loader.bl_invalidate(
|
||||
self.doc, recs = _kernel().bl_invalidate(
|
||||
self.doc, changed_entities=sorted(changed_entities),
|
||||
generation=generation, reasons=reasons, now=_now())
|
||||
return recs
|
||||
@@ -158,7 +179,7 @@ class BehaviourLedger:
|
||||
def retest_history(self, behaviour_id: str) -> list[dict]:
|
||||
"""Generation-aware history: all confirmations + invalidations
|
||||
for one behaviour, oldest first (rule 19S-12)."""
|
||||
return ep_loader.bl_retest_history(self.doc, behaviour_id)
|
||||
return _kernel().bl_retest_history(self.doc, behaviour_id)
|
||||
|
||||
# -- queue (testing mode, 19L/M) ------------------------------------------
|
||||
|
||||
@@ -167,8 +188,8 @@ class BehaviourLedger:
|
||||
"""Eligible testing items: anything not confirmed/accepted/n.a.
|
||||
Dynamic — the queue re-derives from states after every fix
|
||||
(rule 19M)."""
|
||||
return ep_loader.bl_outstanding(
|
||||
return _kernel().bl_outstanding(
|
||||
self.doc, component=component or "", project=project or "")
|
||||
|
||||
def summary(self) -> dict:
|
||||
return ep_loader.bl_summary(self.doc)
|
||||
return _kernel().bl_summary(self.doc)
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the FULL Rust behaviour_ledger core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* bl_define({doc, behaviour_id, owning_project, intended_behaviour,
|
||||
origin_intent, pass_criteria, fail_criteria, procedure,
|
||||
owner_component, dependencies, automated_ref, environment, now})
|
||||
→ {doc, rec}
|
||||
* bl_get({doc, behaviour_id}) → record | null
|
||||
* bl_record_auto({doc, behaviour_id, passed, detail, generation, now})
|
||||
→ {doc, rec}
|
||||
* bl_record_real_world({doc, behaviour_id, passed, detail, generation,
|
||||
confirmed_by, now}) → {doc, rec}
|
||||
* bl_accept_limitation({doc, behaviour_id, note, approved_by, now})
|
||||
→ {doc, rec}
|
||||
* bl_invalidate({doc, changed_entities, generation, reasons, now})
|
||||
→ {doc, recs}
|
||||
* bl_retest_history({doc, behaviour_id}) → events (oldest-first)
|
||||
* bl_outstanding({doc, component, project}) → queue
|
||||
* bl_summary({doc}) → {total, states, outstanding}
|
||||
|
||||
Domain errors (unknown behaviour, duplicate define, …) surface as
|
||||
{"ok": false, "err": <exact Python message>} and raise ValueError —
|
||||
the same type the retired evidence_provenance shim raised.
|
||||
|
||||
No-clock law: `now` (UTC "%Y-%m-%dT%H:%M:%SZ") is always an argument —
|
||||
the kernel never reads the wall clock.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with BL_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" / "behaviour_ledger"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("BL_RUST_LIB", _CRATE_DIR / "target" / "release" / "libbehaviour_ledger.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
class BlCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust behaviour_ledger core is not built. Callers fall back."""
|
||||
|
||||
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 BlCoreUnavailable(
|
||||
f"Rust behaviour_ledger core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in (
|
||||
"bl_define", "bl_get", "bl_record_auto", "bl_record_real_world",
|
||||
"bl_accept_limitation", "bl_invalidate", "bl_retest_history",
|
||||
"bl_outstanding", "bl_summary",
|
||||
):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise ValueError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
def bl_define(doc: dict, *, behaviour_id: str, owning_project: str,
|
||||
intended_behaviour: str, origin_intent: str = "",
|
||||
pass_criteria: str = "", fail_criteria: str = "",
|
||||
procedure: str = "", owner_component: str = "",
|
||||
dependencies: list | None = None, automated_ref: str = "",
|
||||
environment: str = "", now: str) -> tuple[dict, dict]:
|
||||
"""define(): returns (new_doc, new_record)."""
|
||||
v = _call("bl_define", {
|
||||
"doc": doc, "behaviour_id": behaviour_id,
|
||||
"owning_project": owning_project,
|
||||
"intended_behaviour": intended_behaviour,
|
||||
"origin_intent": origin_intent, "pass_criteria": pass_criteria,
|
||||
"fail_criteria": fail_criteria, "procedure": procedure,
|
||||
"owner_component": owner_component,
|
||||
"dependencies": list(dependencies or []),
|
||||
"automated_ref": automated_ref, "environment": environment,
|
||||
"now": now,
|
||||
})
|
||||
return v["doc"], v["rec"]
|
||||
|
||||
def bl_record_auto(doc: dict, behaviour_id: str, *, passed: bool,
|
||||
detail: str = "", generation: str = "",
|
||||
now: str) -> tuple[dict, dict]:
|
||||
"""record_auto_verification(): returns (new_doc, record)."""
|
||||
v = _call("bl_record_auto", {
|
||||
"doc": doc, "behaviour_id": behaviour_id, "passed": bool(passed),
|
||||
"detail": detail, "generation": generation, "now": now,
|
||||
})
|
||||
return v["doc"], v["rec"]
|
||||
|
||||
def bl_record_real_world(doc: dict, behaviour_id: str, *, passed: bool,
|
||||
detail: str = "", generation: str = "",
|
||||
confirmed_by: str = "user",
|
||||
now: str) -> tuple[dict, dict]:
|
||||
"""record_real_world(): returns (new_doc, record)."""
|
||||
v = _call("bl_record_real_world", {
|
||||
"doc": doc, "behaviour_id": behaviour_id, "passed": bool(passed),
|
||||
"detail": detail, "generation": generation,
|
||||
"confirmed_by": confirmed_by, "now": now,
|
||||
})
|
||||
return v["doc"], v["rec"]
|
||||
|
||||
def bl_accept_limitation(doc: dict, behaviour_id: str, *, note: str,
|
||||
approved_by: str = "user",
|
||||
now: str) -> tuple[dict, dict]:
|
||||
"""accept_limitation(): returns (new_doc, record)."""
|
||||
v = _call("bl_accept_limitation", {
|
||||
"doc": doc, "behaviour_id": behaviour_id, "note": note,
|
||||
"approved_by": approved_by, "now": now,
|
||||
})
|
||||
return v["doc"], v["rec"]
|
||||
|
||||
def bl_invalidate(doc: dict, *, changed_entities: list,
|
||||
generation: str = "", reasons: dict | None = None,
|
||||
now: str) -> tuple[dict, list]:
|
||||
"""invalidate_for_change(): returns (new_doc, invalidated records)."""
|
||||
v = _call("bl_invalidate", {
|
||||
"doc": doc, "changed_entities": list(changed_entities),
|
||||
"generation": generation, "reasons": reasons or {}, "now": now,
|
||||
})
|
||||
return v["doc"], v["recs"]
|
||||
|
||||
def bl_get(doc: dict, behaviour_id: str) -> dict | None:
|
||||
"""get(): record or None (normalized id)."""
|
||||
return _call("bl_get", {"doc": doc, "behaviour_id": behaviour_id})
|
||||
|
||||
def bl_retest_history(doc: dict, behaviour_id: str) -> list:
|
||||
"""retest_history(): events oldest-first (empty for unknown)."""
|
||||
return _call("bl_retest_history", {"doc": doc,
|
||||
"behaviour_id": behaviour_id})
|
||||
|
||||
def bl_outstanding(doc: dict, *, component: str = "",
|
||||
project: str = "") -> list:
|
||||
"""outstanding(): filtered queue, TEST_FAILED first."""
|
||||
return _call("bl_outstanding", {"doc": doc, "component": component,
|
||||
"project": project})
|
||||
|
||||
def bl_summary(doc: dict) -> dict:
|
||||
"""summary(): {total, states, outstanding}."""
|
||||
return _call("bl_summary", {"doc": doc})
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.231.0"' in rune_version,
|
||||
'__version__ = "0.238.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -9,6 +9,8 @@ CANDIDATE/UNIT-PROVEN — existence is not trust (§15).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from rune.construction_pattern import ConstructionPattern
|
||||
|
||||
__all__ = ["BUILTIN_PATTERNS", "get_pattern", "PATTERN_REGISTRY_REVISION"]
|
||||
@@ -236,7 +238,22 @@ _REGISTRY: dict[str, ConstructionPattern] = {p.pattern_id: p for p in BUILTIN_PA
|
||||
def get_pattern(pattern_id: str, revision: int | None = None) -> ConstructionPattern | None:
|
||||
"""Fetch a builtin pattern; a specific revision raises if the registry
|
||||
no longer carries it (contracts reference the revision they consumed —
|
||||
§17)."""
|
||||
§17).
|
||||
|
||||
Dichotomy wave: the Rust registry core first (RUNE_CPB_CORE=python
|
||||
pins the Python fallback — strangler law). Both serve the same frozen
|
||||
registry docs, so the pin is only for oracle isolation."""
|
||||
if os.environ.get("RUNE_CPB_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import cpb_loader
|
||||
|
||||
if cpb_loader.available():
|
||||
doc = cpb_loader.get_pattern(pattern_id, revision)
|
||||
return ConstructionPattern.from_dict(doc) if doc is not None else None
|
||||
except ValueError:
|
||||
raise # the kernel's revision-mismatch message is oracle-exact
|
||||
except Exception:
|
||||
pass # kernel unavailable — Python registry below
|
||||
p = _REGISTRY.get(pattern_id)
|
||||
if p is None:
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_patterns_builtin registry core
|
||||
(dichotomy wave, #2479 tranche). Mirrors cs_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cpb_registry_revision({}) -> number
|
||||
* cpb_patterns({}) -> [pattern doc, ...] registry order
|
||||
* cpb_get_pattern({"pattern_id", "revision"}) -> pattern doc | null
|
||||
(a stale revision is ok=false; get_pattern raises ValueError with the
|
||||
oracle's exact message)
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process) — never
|
||||
re-exported here. Override the .so with CPB_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_patterns_builtin"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"CPB_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" / "libconstruction_patterns_builtin.so",
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class CpbUnavailable(FileNotFoundError):
|
||||
"""The Rust registry core is not built. Callers fall back to the Python
|
||||
registry — strangler law."""
|
||||
|
||||
|
||||
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 CpbUnavailable(
|
||||
f"Rust construction_patterns_builtin core not built: {_LIB_PATH} missing — "
|
||||
f"run `cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("cpb_registry_revision", "cpb_patterns", "cpb_get_pattern"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
# free comes from capability_scoring (same process, shared allocator law)
|
||||
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()
|
||||
fn = getattr(lib, sym)
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
ptr = fn(raw)
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned a null pointer")
|
||||
try:
|
||||
text = ctypes.string_at(ptr).decode("utf-8")
|
||||
finally:
|
||||
lib._free(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def registry_revision() -> int:
|
||||
"""The builtin pattern registry revision (PATTERN_REGISTRY_REVISION)."""
|
||||
return int(_call("cpb_registry_revision", {}))
|
||||
|
||||
|
||||
def patterns() -> list:
|
||||
"""Every builtin pattern doc, in registry order."""
|
||||
return _call("cpb_patterns", {})
|
||||
|
||||
|
||||
def get_pattern(pattern_id: str, revision: int | None = None):
|
||||
"""Pattern doc for `pattern_id`, or None. A revision that no longer
|
||||
matches raises ValueError with the oracle's exact message."""
|
||||
raw = json.dumps({"pattern_id": pattern_id, "revision": revision}).encode("utf-8")
|
||||
lib = _load()
|
||||
ptr = lib.cpb_get_pattern(raw)
|
||||
if not ptr:
|
||||
raise RuntimeError("cpb_get_pattern returned a null pointer")
|
||||
try:
|
||||
text = ctypes.string_at(ptr).decode("utf-8")
|
||||
finally:
|
||||
lib._free(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
# The only err the kernel produces is the oracle's revision-mismatch
|
||||
# ValueError message — preserve its type for facade parity.
|
||||
raise ValueError(env.get("err"))
|
||||
return env["value"]
|
||||
+43
-2
@@ -1,12 +1,17 @@
|
||||
#!/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:
|
||||
batch 3 + #2479 dichotomy wave 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}
|
||||
* dl_ledger_path({index_file}) -> path string
|
||||
* dl_snapshot_entry({blocks: {label: content}, when}) -> snapshot entry
|
||||
* dl_anchor_record({content, reason, when}) -> anchor record
|
||||
* dl_check_alerts({ledger, blocks: {label: content}}) -> [alert, ...]
|
||||
* dl_fix_decision({ledger, label, cur_sha?}) -> {fixed, reason}
|
||||
|
||||
Override the .so with DL_RUST_LIB (a FILE path).
|
||||
"""
|
||||
@@ -39,7 +44,9 @@ def _load() -> ctypes.CDLL:
|
||||
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"):
|
||||
for sym in ("dl_sha", "dl_diff", "dl_check", "dl_auto_fix", "dl_ledger_path",
|
||||
"dl_snapshot_entry", "dl_anchor_record", "dl_check_alerts",
|
||||
"dl_fix_decision"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
@@ -75,3 +82,37 @@ def sha(text: str) -> str:
|
||||
|
||||
def diff_blocks(ref: str, cur: str) -> str:
|
||||
return _call("dl_diff", {"ref": ref, "cur": cur})
|
||||
|
||||
|
||||
def check_block(label: str, prev_size: int, cur_size: int, anchor_size=None,
|
||||
anchor_sha=None, cur_sha=None) -> dict:
|
||||
return _call("dl_check", {"label": label, "prev_size": prev_size,
|
||||
"cur_size": cur_size, "anchor_size": anchor_size,
|
||||
"anchor_sha": anchor_sha, "cur_sha": cur_sha})
|
||||
|
||||
|
||||
def auto_fix_decision(label: str, anchored: bool, has_last_good: bool) -> dict:
|
||||
return _call("dl_auto_fix", {"label": label, "anchored": anchored,
|
||||
"has_last_good": has_last_good})
|
||||
|
||||
|
||||
def ledger_path_for(index_file: str) -> str:
|
||||
return _call("dl_ledger_path", {"index_file": index_file})
|
||||
|
||||
|
||||
def snapshot_entry(blocks: dict, when: str) -> dict:
|
||||
return _call("dl_snapshot_entry", {"blocks": blocks, "when": when})
|
||||
|
||||
|
||||
def anchor_record(content: str, reason: str, when: str) -> dict:
|
||||
return _call("dl_anchor_record", {"content": content, "reason": reason,
|
||||
"when": when})
|
||||
|
||||
|
||||
def check_alerts(ledger: dict, blocks: dict) -> list:
|
||||
return _call("dl_check_alerts", {"ledger": ledger, "blocks": blocks})
|
||||
|
||||
|
||||
def fix_decision(ledger: dict, label: str, cur_sha=None) -> dict:
|
||||
return _call("dl_fix_decision", {"ledger": ledger, "label": label,
|
||||
"cur_sha": cur_sha})
|
||||
+135
-58
@@ -48,6 +48,13 @@ DB = memory_env.store_db()
|
||||
def ledger_path():
|
||||
# The ledger lives beside the index, so overriding INDEX_FILE (as the
|
||||
# curator gate and the canary do for isolation) moves the ledger too.
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.ledger_path_for(INDEX_FILE)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
return os.path.join(os.path.dirname(INDEX_FILE), "drift_ledger.json")
|
||||
|
||||
|
||||
@@ -63,9 +70,15 @@ CONSTITUTION_LIMIT = 0.05 # 5% — anything more than a directive entry is drif
|
||||
BLOCKS = ("constitution", "persona", "human", "operating", "project")
|
||||
|
||||
|
||||
def _rust_core():
|
||||
"""RUNE_DL_CORE pin law: 'python' forces the oracle path, anything else
|
||||
(default 'auto') serves the Rust core when the cdylib is built."""
|
||||
return os.environ.get("RUNE_DL_CORE", "auto") != "python"
|
||||
|
||||
|
||||
def _sha(text):
|
||||
# Dichotomy batch 3: Rust core first (RUNE_DL_CORE=python pins fallback).
|
||||
if os.environ.get("RUNE_DL_CORE", "auto") != "python":
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
@@ -155,51 +168,54 @@ def surface_alert(msg, label, changed=""):
|
||||
logger.warning("Failed to write alert file: %s", e)
|
||||
|
||||
|
||||
def snapshot():
|
||||
ledger = load_ledger()
|
||||
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
def _snapshot_entry(blocks, now):
|
||||
"""Dichotomy wave: Rust core first (RUNE_DL_CORE=python pins fallback)."""
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.snapshot_entry(blocks, now)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
entry = {"when": now, "blocks": {}}
|
||||
for label in BLOCKS:
|
||||
raw = read_block(label)
|
||||
raw = blocks[label]
|
||||
entry["blocks"][label] = {
|
||||
"sha": _sha(raw),
|
||||
"size": len(raw),
|
||||
"content": raw, # last-good content backup for diffing + retro-fix
|
||||
}
|
||||
ledger["last_snapshot"] = entry
|
||||
save_ledger(ledger)
|
||||
return entry
|
||||
|
||||
|
||||
def anchor(label, reason):
|
||||
"""Record that the current state of `label` is authorised (a user directive,
|
||||
a confirmed review, a 3-sighting promotion). Subsequent drift is measured
|
||||
from here — this is the re-anchor after legitimate change."""
|
||||
ledger = load_ledger()
|
||||
raw = read_block(label)
|
||||
ledger.setdefault("anchors", {})[label] = {
|
||||
def _anchor_record(raw, reason, now):
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.anchor_record(raw, reason, now)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
return {
|
||||
"sha": _sha(raw),
|
||||
"size": len(raw),
|
||||
"content": raw, # authorised content backup for root-cause diffing
|
||||
"reason": reason,
|
||||
"when": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
"when": now,
|
||||
}
|
||||
save_ledger(ledger)
|
||||
return f"anchored {label}: {reason}"
|
||||
|
||||
|
||||
def check(strict=False, autofix=True):
|
||||
ledger = load_ledger()
|
||||
snap = ledger.get("last_snapshot")
|
||||
if not snap:
|
||||
# No baseline yet — record one and treat as healthy (first contact).
|
||||
snapshot()
|
||||
print("no prior snapshot; baseline recorded")
|
||||
return 0
|
||||
|
||||
def _check_alerts(ledger, now_blocks):
|
||||
"""The check() alert loop: Rust core first, oracle loop as fallback."""
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.check_alerts(ledger, now_blocks)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
alerts = []
|
||||
now_blocks = {label: read_block(label) for label in BLOCKS}
|
||||
prev = snap.get("blocks", {})
|
||||
prev = (ledger.get("last_snapshot") or {}).get("blocks", {})
|
||||
for label in BLOCKS:
|
||||
cur = now_blocks[label]
|
||||
cur_size = len(cur)
|
||||
@@ -245,6 +261,41 @@ def check(strict=False, autofix=True):
|
||||
"content_changed": content_changed,
|
||||
}
|
||||
)
|
||||
return alerts
|
||||
|
||||
|
||||
def snapshot():
|
||||
ledger = load_ledger()
|
||||
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
entry = _snapshot_entry({label: read_block(label) for label in BLOCKS}, now)
|
||||
ledger["last_snapshot"] = entry
|
||||
save_ledger(ledger)
|
||||
return entry
|
||||
|
||||
|
||||
def anchor(label, reason):
|
||||
"""Record that the current state of `label` is authorised (a user directive,
|
||||
a confirmed review, a 3-sighting promotion). Subsequent drift is measured
|
||||
from here — this is the re-anchor after legitimate change."""
|
||||
ledger = load_ledger()
|
||||
raw = read_block(label)
|
||||
ledger.setdefault("anchors", {})[label] = _anchor_record(
|
||||
raw, reason, datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
)
|
||||
save_ledger(ledger)
|
||||
return f"anchored {label}: {reason}"
|
||||
|
||||
|
||||
def check(strict=False, autofix=True):
|
||||
ledger = load_ledger()
|
||||
snap = ledger.get("last_snapshot")
|
||||
if not snap:
|
||||
# No baseline yet — record one and treat as healthy (first contact).
|
||||
snapshot()
|
||||
print("no prior snapshot; baseline recorded")
|
||||
return 0
|
||||
|
||||
alerts = _check_alerts(ledger, {label: read_block(label) for label in BLOCKS})
|
||||
if alerts:
|
||||
for a in alerts:
|
||||
msg = (
|
||||
@@ -287,27 +338,23 @@ def auto_fix(label, ledger, strict):
|
||||
|
||||
Returns {action, reason}.
|
||||
"""
|
||||
if label == "constitution":
|
||||
return {"action": "alert-only", "reason": "constitution changes only by explicit directive; needs human review"}
|
||||
if label in ledger.get("anchors", {}):
|
||||
block_meta = ledger.get("last_snapshot", {}).get("blocks", {}).get(label)
|
||||
anchored = label in ledger.get("anchors", {})
|
||||
has_last_good = bool(block_meta and "content" in block_meta)
|
||||
result = _auto_fix_decision(label, anchored, has_last_good)
|
||||
if result["action"] == "re-anchored":
|
||||
# Authorised growth: re-anchor to the current (approved) state.
|
||||
raw = read_block(label)
|
||||
ledger.setdefault("anchors", {})[label] = {
|
||||
"sha": _sha(raw),
|
||||
"size": len(raw),
|
||||
"content": raw,
|
||||
"reason": "auto re-anchor after authorised growth",
|
||||
"when": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
}
|
||||
ledger.setdefault("anchors", {})[label] = _anchor_record(
|
||||
raw, "auto re-anchor after authorised growth",
|
||||
datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
)
|
||||
save_ledger(ledger)
|
||||
return {"action": "re-anchored", "reason": "anchored block grew (authorised) — re-baselined"}
|
||||
# Unauthorised drift: restore to last-good content (new schema — restore
|
||||
# the store's always-on core entries for this topic).
|
||||
block_meta = ledger.get("last_snapshot", {}).get("blocks", {}).get(label)
|
||||
if block_meta and "content" in block_meta:
|
||||
elif result["action"] == "restored":
|
||||
# Unauthorised drift: restore to last-good content (new schema —
|
||||
# restore the store's always-on core entries for this topic).
|
||||
restore_topic(label, block_meta["content"])
|
||||
return {"action": "restored", "reason": "unauthorised drift — restored to last-good content"}
|
||||
return {"action": "alert-only", "reason": "no last-good content available to restore"}
|
||||
return result
|
||||
|
||||
|
||||
def diff_blocks(ref, cur):
|
||||
@@ -379,6 +426,44 @@ def restore_topic(label, content):
|
||||
logger.error("restore_topic('%s') failed: %s", label, e)
|
||||
|
||||
|
||||
def _auto_fix_decision(label, anchored, has_last_good):
|
||||
"""The auto-fix ROUTING decision (IO execution stays in auto_fix)."""
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.auto_fix_decision(label, anchored, has_last_good)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
if label == "constitution":
|
||||
return {"action": "alert-only", "reason": "constitution changes only by explicit directive; needs human review"}
|
||||
if anchored:
|
||||
return {"action": "re-anchored", "reason": "anchored block grew (authorised) — re-baselined"}
|
||||
if has_last_good:
|
||||
return {"action": "restored", "reason": "unauthorised drift — restored to last-good content"}
|
||||
return {"action": "alert-only", "reason": "no last-good content available to restore"}
|
||||
|
||||
|
||||
def _fix_decision(ledger, label, cur_sha):
|
||||
"""The retroactive-fix gate decision (restore IO stays in fix)."""
|
||||
if _rust_core():
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.fix_decision(ledger, label, cur_sha)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
snap = ledger.get("last_snapshot", {})
|
||||
block_meta = snap.get("blocks", {}).get(label)
|
||||
if not block_meta or "content" not in block_meta:
|
||||
return {"fixed": False, "reason": f"no last-good content for {label}"}
|
||||
if label in ledger.get("anchors", {}):
|
||||
return {"fixed": False, "reason": f"{label} is anchored (authorised); use anchor to re-baseline instead of fix"}
|
||||
if cur_sha == block_meta.get("sha"):
|
||||
return {"fixed": False, "reason": f"{label} already matches last-good"}
|
||||
return {"fixed": True, "reason": f"{label} restored to last-good state"}
|
||||
|
||||
|
||||
def fix(label):
|
||||
"""RETROACTIVE FIX (#B): restore a drifted block to its last-good content.
|
||||
|
||||
@@ -390,20 +475,12 @@ def fix(label):
|
||||
ledger = load_ledger()
|
||||
snap = ledger.get("last_snapshot", {})
|
||||
block_meta = snap.get("blocks", {}).get(label)
|
||||
if not block_meta or "content" not in block_meta:
|
||||
return {"fixed": False, "reason": f"no last-good content for {label}"}
|
||||
# Never silently revert an anchored block — it was authorised.
|
||||
if label in ledger.get("anchors", {}):
|
||||
return {"fixed": False, "reason": f"{label} is anchored (authorised); use anchor to re-baseline instead of fix"}
|
||||
good = block_meta["content"]
|
||||
cur = read_block(label)
|
||||
if _sha(cur) == block_meta["sha"]:
|
||||
return {"fixed": False, "reason": f"{label} already matches last-good"}
|
||||
# New schema: restore the store's always-on entries for the topic (the old
|
||||
# markdown block files are retired).
|
||||
restore_topic(label, good)
|
||||
journal(f"{label} restored to last-good content (retroactive drift fix)")
|
||||
return {"fixed": True, "reason": f"{label} restored to last-good state"}
|
||||
result = _fix_decision(ledger, label, _sha(cur))
|
||||
if result.get("fixed"):
|
||||
restore_topic(label, block_meta["content"])
|
||||
journal(f"{label} restored to last-good content (retroactive drift fix)")
|
||||
return result
|
||||
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust experience_encoding core (dichotomy wave
|
||||
#2479 tranche). Mirrors broker_loader.py/le_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ee_evaluate({event, existing_episodes, corpus_texts, lexicon_available})
|
||||
-> {verdict, reason, provenance}
|
||||
|
||||
`lexicon_available` pins the oracle's flat `import lexicon_evolution`
|
||||
environment seam (ImportError → conservative evidence admission); the
|
||||
facade derives it from the live import and the frozen truth replays it.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with EE_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" / "experience_encoding"
|
||||
LIB_PATH = Path(
|
||||
os.environ.get("EE_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" / "libexperience_encoding.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ExperienceCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust experience_encoding 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 ExperienceCoreUnavailable(str(LIB_PATH))
|
||||
lib = ctypes.CDLL(str(LIB_PATH))
|
||||
getattr(lib, "ee_evaluate").argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, "ee_evaluate").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 evaluate(event, existing_episodes=None, corpus_texts=(),
|
||||
lexicon_available=True) -> dict:
|
||||
"""Rust core evaluate: returns {verdict, reason, provenance}."""
|
||||
return _call("ee_evaluate", {
|
||||
"event": event,
|
||||
"existing_episodes": list(existing_episodes or []),
|
||||
"corpus_texts": list(corpus_texts or ()),
|
||||
"lexicon_available": lexicon_available,
|
||||
})
|
||||
@@ -13,14 +13,57 @@ becomes an episode. Event → evaluate → classify → attach provenance:
|
||||
Admission uses the lexicon discipline (distinctive terms, not common
|
||||
words) + grounding (traces to real material) — the same meta-judgement
|
||||
as retrieval and evolution, so growth and admission speak one lexicon.
|
||||
|
||||
Dichotomy wave #2479: the admission decision runs through the FULL Rust
|
||||
core (src/rust/experience_encoding) when built; RUNE_EE_CORE=python pins
|
||||
the Python fallback — parity harnesses. The _py_ variant is the
|
||||
load-bearing fallback + parity oracle; the ctypes seam is rune.ee_loader.
|
||||
The oracle's flat `import lexicon_evolution` is an environment seam: when
|
||||
the module is missing, the core is bypassed and the conservative evidence
|
||||
fallback applies exactly as the oracle wrote it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def _ee_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_EE_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the entire
|
||||
admission decision (empty gate, duplicate scan, lexical signal,
|
||||
grounding, classification, provenance)."""
|
||||
if os.environ.get("RUNE_EE_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ee_loader
|
||||
except ImportError:
|
||||
try:
|
||||
import ee_loader
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
return ee_loader if ee_loader.available() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def evaluate(event: dict, existing_episodes: list | None = None,
|
||||
corpus_texts: tuple = ()) -> dict:
|
||||
"""Evaluate one event. Returns {verdict, reason, provenance}."""
|
||||
core = _ee_core()
|
||||
if core is not None:
|
||||
try:
|
||||
import lexicon_evolution # noqa: F401 (the oracle's seam)
|
||||
except ImportError:
|
||||
return _py_evaluate(event, existing_episodes, corpus_texts)
|
||||
return core.evaluate(event, existing_episodes, corpus_texts)
|
||||
return _py_evaluate(event, existing_episodes, corpus_texts)
|
||||
|
||||
|
||||
def _py_evaluate(event: dict, existing_episodes: list | None = None,
|
||||
corpus_texts: tuple = ()) -> dict:
|
||||
"""Evaluate one event. Returns {verdict, reason, provenance}."""
|
||||
existing_episodes = existing_episodes or []
|
||||
text = (event.get("text") or "").strip()
|
||||
source = event.get("source", "unknown")
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust mutation_guards core (dichotomy wave
|
||||
#2479 tranche). Mirrors ee_loader.py/broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* mu_classify({target_kind, operation, scope, attributed, backed_up})
|
||||
-> {verdict, reason, recovery}
|
||||
* mu_guard({mutation}) -> {verdict, reason, recovery}
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with MU_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" / "mutation_guards"
|
||||
LIB_PATH = Path(
|
||||
os.environ.get("MU_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" / "libmutation_guards.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class MutationGuardsCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust mutation_guards 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 MutationGuardsCoreUnavailable(str(LIB_PATH))
|
||||
lib = ctypes.CDLL(str(LIB_PATH))
|
||||
for name in ("mu_classify", "mu_guard"):
|
||||
fn = getattr(lib, name)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def classify(target_kind, operation, scope="single",
|
||||
attributed=True, backed_up=False) -> dict:
|
||||
"""Rust core classify_mutation: returns {verdict, reason, recovery}."""
|
||||
return _call("mu_classify", {
|
||||
"target_kind": target_kind,
|
||||
"operation": operation,
|
||||
"scope": scope,
|
||||
"attributed": attributed,
|
||||
"backed_up": backed_up,
|
||||
})
|
||||
|
||||
|
||||
def guard(mutation: dict) -> dict:
|
||||
"""Rust core guard_mutation: returns {verdict, reason, recovery}."""
|
||||
return _call("mu_guard", {"mutation": mutation})
|
||||
+69
-20
@@ -17,34 +17,62 @@ Classification (deterministic, no model):
|
||||
|
||||
Recoverability: a CONFIRM/DENY verdict carries the recovery shape
|
||||
(snapshot id required, or the reason none exists).
|
||||
|
||||
Dichotomy wave #2479: the classification runs through the FULL Rust core
|
||||
(src/rust/mutation_guards) when built; RUNE_MG_CORE=python pins the
|
||||
Python fallback — parity harnesses. The _py_ variants are the
|
||||
load-bearing fallback + parity oracle; the ctypes seam is rune.mu_loader.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Target-kind severity (higher = more protected)
|
||||
KIND_SEVERITY = {
|
||||
"constitution": 3,
|
||||
"identity": 3,
|
||||
"persona": 3,
|
||||
"skill": 2,
|
||||
"belief": 2,
|
||||
"operating": 1,
|
||||
"fact": 1,
|
||||
"archive": 0,
|
||||
}
|
||||
import os
|
||||
|
||||
# Operation severity
|
||||
OP_SEVERITY = {
|
||||
"delete": 3,
|
||||
"overwrite": 2,
|
||||
"append": 1,
|
||||
"read": 0,
|
||||
}
|
||||
|
||||
def _mg_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_MG_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the entire
|
||||
classification (severity tables, branch order, verdict + reason +
|
||||
recovery shape)."""
|
||||
if os.environ.get("RUNE_MG_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import mu_loader
|
||||
except ImportError:
|
||||
try:
|
||||
import mu_loader
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
return mu_loader if mu_loader.available() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def classify_mutation(target_kind: str, operation: str, scope: str = "single",
|
||||
attributed: bool = True, backed_up: bool = False) -> dict:
|
||||
"""Classify a proposed mutation. Returns {verdict, reason, recovery}."""
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
return core.classify(target_kind, operation, scope, attributed,
|
||||
backed_up)
|
||||
return _py_classify_mutation(target_kind, operation, scope, attributed,
|
||||
backed_up)
|
||||
|
||||
|
||||
def guard_mutation(mutation: dict) -> dict:
|
||||
"""Guard one mutation dict {target_kind, operation, scope, attributed,
|
||||
backed_up}. Returns the verdict dict (ALLOW/CONFIRM/DENY + reason)."""
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
return core.guard(mutation)
|
||||
return _py_guard_mutation(mutation)
|
||||
|
||||
|
||||
def _py_classify_mutation(target_kind: str, operation: str,
|
||||
scope: str = "single", attributed: bool = True,
|
||||
backed_up: bool = False) -> dict:
|
||||
"""Classify a proposed mutation. Returns {verdict, reason, recovery}."""
|
||||
kind_sev = KIND_SEVERITY.get(target_kind, 1)
|
||||
op_sev = OP_SEVERITY.get(operation, 1)
|
||||
scope_sev = {"whole": 2, "multi": 1, "single": 0}.get(scope, 0)
|
||||
@@ -82,13 +110,34 @@ def classify_mutation(target_kind: str, operation: str, scope: str = "single",
|
||||
}
|
||||
|
||||
|
||||
def guard_mutation(mutation: dict) -> dict:
|
||||
def _py_guard_mutation(mutation: dict) -> dict:
|
||||
"""Guard one mutation dict {target_kind, operation, scope, attributed,
|
||||
backed_up}. Returns the verdict dict (ALLOW/CONFIRM/DENY + reason)."""
|
||||
return classify_mutation(
|
||||
return _py_classify_mutation(
|
||||
mutation.get("target_kind", "fact"),
|
||||
mutation.get("operation", "append"),
|
||||
mutation.get("scope", "single"),
|
||||
mutation.get("attributed", True),
|
||||
mutation.get("backed_up", False),
|
||||
)
|
||||
|
||||
|
||||
# Target-kind severity (higher = more protected)
|
||||
KIND_SEVERITY = {
|
||||
"constitution": 3,
|
||||
"identity": 3,
|
||||
"persona": 3,
|
||||
"skill": 2,
|
||||
"belief": 2,
|
||||
"operating": 1,
|
||||
"fact": 1,
|
||||
"archive": 0,
|
||||
}
|
||||
|
||||
# Operation severity
|
||||
OP_SEVERITY = {
|
||||
"delete": 3,
|
||||
"overwrite": 2,
|
||||
"append": 1,
|
||||
"read": 0,
|
||||
}
|
||||
@@ -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.231.0
|
||||
Version: v0.238.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.231.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.238.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""sl_loader.py — ctypes loader for the Rust spine_language core
|
||||
(dichotomy wave #2479 tranche). Mirrors broker_loader.py:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* sl_classification_sides({"root"}) -> measured map
|
||||
* sl_place_languages({"root", "doc"}) -> {"doc", "placed"}
|
||||
* sl_contract_discoveries({"root"}) -> discovery list
|
||||
* sl_normalize_discovery_ids({"doc", "root"?/"discoveries"?}) -> {"doc", "renamed"}
|
||||
* sl_apply_contract_discoveries({"doc", "root"?/"discoveries"?}) -> {"doc", "placed"}
|
||||
|
||||
`root` is the repo root the store reads hang off (the facade always sends
|
||||
str(ROOT) — tests monkeypatch rune.spine_language.ROOT and it must be
|
||||
honored). Free: capability_scoring's cs_free_string (shared process).
|
||||
NEVER re-export a free symbol here. Override the .so with SL_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" / "spine_language"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("SL_RUST_LIB", _CRATE_DIR / "target" / "release" / "libspine_language.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"sl_classification_sides",
|
||||
"sl_place_languages",
|
||||
"sl_contract_discoveries",
|
||||
"sl_normalize_discovery_ids",
|
||||
"sl_apply_contract_discoveries",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class SpineLanguageUnavailable(FileNotFoundError):
|
||||
"""The Rust spine_language core is not built. Callers fall back to
|
||||
Python (strangler law; RUNE_SL_CORE=python pins the fallback)."""
|
||||
|
||||
|
||||
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 SpineLanguageUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
# free comes from capability_scoring (same process); load it if not yet resident
|
||||
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 classification_sides(root: str) -> dict:
|
||||
return _call("sl_classification_sides", {"root": root})
|
||||
|
||||
|
||||
def place_languages(doc: dict, root: str) -> dict:
|
||||
return _call("sl_place_languages", {"doc": doc, "root": root})
|
||||
|
||||
|
||||
def contract_discoveries(root: str) -> list:
|
||||
return _call("sl_contract_discoveries", {"root": root})
|
||||
|
||||
|
||||
def normalize_discovery_ids(doc: dict, root: str, discoveries: list | None = None) -> dict:
|
||||
payload = {"doc": doc, "root": root}
|
||||
if discoveries is not None:
|
||||
payload["discoveries"] = discoveries
|
||||
return _call("sl_normalize_discovery_ids", payload)
|
||||
|
||||
|
||||
def apply_contract_discoveries(doc: dict, root: str, discoveries: list | None = None) -> dict:
|
||||
payload = {"doc": doc, "root": root}
|
||||
if discoveries is not None:
|
||||
payload["discoveries"] = discoveries
|
||||
return _call("sl_apply_contract_discoveries", payload)
|
||||
@@ -13,6 +13,11 @@ Three transforms, applied to BOTH cores' output at build time:
|
||||
|
||||
Same dependency laws as spine.py: structural_store primitives only, no
|
||||
I/O beyond the classification/scaffold store reads, deterministic.
|
||||
|
||||
Dichotomy wave #2479 tranche: the deterministic transform surface is
|
||||
ported to FULL Rust (src/rust/spine_language). This facade delegates to
|
||||
the Rust core via rune/sl_loader when built; the Python implementation
|
||||
below is the strangler fallback, pinned with RUNE_SL_CORE=python.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -26,11 +31,31 @@ from rune.structural_store import (
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC_DIR = ROOT / "src" / "rune"
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust spine_language core when available (strangler: Python
|
||||
fallback is load-bearing). RUNE_SL_CORE=python pins the fallback."""
|
||||
import os
|
||||
if os.environ.get("RUNE_SL_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import sl_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return sl_loader if sl_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def _classification_sides() -> dict:
|
||||
"""The measured dichotomy map (W0): module stem → RUST|GO|TUI-LAST|...
|
||||
Loaded once per _place_languages call, best-effort — a missing or
|
||||
unreadable classification.json degrades to {}/unassessed verdicts,
|
||||
never to invented facts."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.classification_sides(str(ROOT))
|
||||
try:
|
||||
data = json.loads((ROOT / "store" / "rebase" / "classification.json").read_text())
|
||||
return {name: rec.get("side", "") for name, rec in data.items() if isinstance(rec, dict)}
|
||||
@@ -44,6 +69,12 @@ def _place_languages(doc: dict) -> int:
|
||||
kind; the exact same rule construction_decision.language_placement
|
||||
implements (kept inline here to avoid a per-entity FFI crossing for
|
||||
the spine's own 218 entities; the FFI path is used in contracts)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
result = core.place_languages(doc, str(ROOT))
|
||||
doc.clear()
|
||||
doc.update(result["doc"])
|
||||
return result["placed"]
|
||||
GO_HINTS = ("server", "transport", "dispatch", "entry", "bridge",
|
||||
"hook_engine", "provider", "watch", "notif", "voice",
|
||||
"gitea", "event_", "message_", "tool_exec", "work_manager")
|
||||
@@ -134,6 +165,15 @@ def _normalize_discovery_ids(doc: dict) -> int:
|
||||
module records produced by the earlier discovery projector; leave all
|
||||
other open-world entity classes alone.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
# The gather goes through the module-level seam (tests monkeypatch
|
||||
# _contract_discoveries to isolate the doc from the live store).
|
||||
result = core.normalize_discovery_ids(
|
||||
doc, str(ROOT), discoveries=_contract_discoveries())
|
||||
doc.clear()
|
||||
doc.update(result["doc"])
|
||||
return result["renamed"]
|
||||
renamed = {}
|
||||
observed = {
|
||||
eid for _, entities in _contract_discoveries()
|
||||
@@ -167,6 +207,13 @@ def _apply_contract_discoveries(doc: dict) -> int:
|
||||
spine learns what construction actually built. Doc-level transform,
|
||||
applied to BOTH cores' output. The CANDIDATE law holds: only CLOSED
|
||||
contracts write back."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
result = core.apply_contract_discoveries(
|
||||
doc, str(ROOT), discoveries=_contract_discoveries())
|
||||
doc.clear()
|
||||
doc.update(result["doc"])
|
||||
return result["placed"]
|
||||
placed = 0
|
||||
for cid, entities in _contract_discoveries():
|
||||
for eid, kind, provenance in entities:
|
||||
@@ -189,6 +236,10 @@ def _contract_discoveries():
|
||||
"""Closed contracts' DISCOVERED entities (issue #2465 addition 2):
|
||||
expected_new_observed + candidate corrections, gathered from the
|
||||
scaffold store. Returns [(contract_id, [(entity_id, kind), ...])]."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
raw = core.contract_discoveries(str(ROOT))
|
||||
return [(cid, [tuple(item) for item in items]) for cid, items in raw]
|
||||
try:
|
||||
doc = json.loads((ROOT / "store" / "scaffold" / "current.json").read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "abstraction_ladder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_scoring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "abstraction_ladder"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Promotion ladder kernel (episode→skill) — dichotomy wave #2479 tranche; zero-dep (pyjson via capability_scoring)"
|
||||
|
||||
[lib]
|
||||
name = "abstraction_ladder"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
capability_scoring = { path = "../capability_scoring" }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,97 @@
|
||||
//! branch_battery — replays frozen_al_truth.json VERBATIM (76 cases
|
||||
//! frozen from the live Python oracle, RUNE_AL_CORE=python pinned,
|
||||
//! inputs deep-copied before capture). Order-insensitive object
|
||||
//! comparison; numbers compared exactly (both sides IEEE-754 f64).
|
||||
|
||||
use capability_scoring::pyjson::{parse, dumps, Json};
|
||||
|
||||
use crate::ladder;
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_al_truth.json");
|
||||
|
||||
fn jeq(a: &Json, b: &Json) -> bool {
|
||||
match (a, b) {
|
||||
(Json::Num(x), Json::Num(y)) => x == y,
|
||||
(Json::Str(x), Json::Str(y)) => x == y,
|
||||
(Json::Bool(x), Json::Bool(y)) => x == y,
|
||||
(Json::Null, Json::Null) => true,
|
||||
(Json::Arr(x), Json::Arr(y)) => {
|
||||
x.len() == y.len() && x.iter().zip(y).all(|(i, j)| jeq(i, j))
|
||||
}
|
||||
(Json::Obj(x), Json::Obj(y)) => {
|
||||
x.len() == y.len()
|
||||
&& x.iter()
|
||||
.all(|(k, v)| y.iter().any(|(k2, v2)| k == k2 && jeq(v, v2)))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_arr(v: &Json) -> Vec<Json> {
|
||||
match v {
|
||||
Json::Arr(a) => a.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(v: Option<&Json>) -> Option<&str> {
|
||||
match v {
|
||||
Some(Json::Str(s)) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_case(fn_name: &str, args: &Json) -> Result<Json, String> {
|
||||
match fn_name {
|
||||
"next_stage" => {
|
||||
let stage = as_str(args.get("stage"));
|
||||
Ok(match ladder::next_stage(stage) {
|
||||
Some(nxt) => Json::Str(nxt.into()),
|
||||
None => Json::Null,
|
||||
})
|
||||
}
|
||||
"can_promote" => {
|
||||
let item = args.get("item").ok_or("missing item")?;
|
||||
Ok(ladder::can_promote(item))
|
||||
}
|
||||
"promote" => {
|
||||
let item = args.get("item").ok_or("missing item")?;
|
||||
Ok(ladder::promote(item))
|
||||
}
|
||||
other => Err(format!("unknown fn {}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let truth = parse(TRUTH).expect("frozen_al_truth.json parses");
|
||||
let cases = as_arr(truth.get("cases").expect("cases key"));
|
||||
assert!(cases.len() >= 76, "expected the full frozen battery, got {}", cases.len());
|
||||
let mut failures: Vec<String> = vec![];
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
let fn_name = match case.get("fn") {
|
||||
Some(Json::Str(s)) => s.as_str(),
|
||||
_ => panic!("case {i} missing fn"),
|
||||
};
|
||||
let args = case.get("input").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let expected = case.get("expected").expect("expected key");
|
||||
match run_case(fn_name, &args) {
|
||||
Ok(got) if jeq(&got, expected) => {}
|
||||
Ok(got) => failures.push(format!(
|
||||
"case {i} ({} {}): kernel {:?} != expected {}",
|
||||
fn_name,
|
||||
case.get("name").and_then(|v| as_str(Some(v))).unwrap_or("?"),
|
||||
dumps(&got),
|
||||
dumps(expected),
|
||||
)),
|
||||
Err(e) => failures.push(format!("case {i} ({}): error {}", fn_name, e)),
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"frozen-truth battery failed {}/{}:\n{}",
|
||||
failures.len(),
|
||||
cases.len(),
|
||||
failures.join("\n"),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
//! ladder.rs — the promotion ladder kernel, a verbatim port of
|
||||
//! src/rune/abstraction_ladder.py (dichotomy wave #2479 tranche),
|
||||
//! Phase B language-advantage pass.
|
||||
//!
|
||||
//! episode → fact → observation → concept → belief → rule → skill.
|
||||
//! Each promotion needs: evidence threshold, provenance, contradiction
|
||||
//! handling, prediction/behavioural consequence, outcome-based revision.
|
||||
//! Schema alone does not count — the ladder moves items or it doesn't.
|
||||
//!
|
||||
//! Phase B shapes (vs the parity commit): Stage enum with exhaustive
|
||||
//! matches everywhere a rung is decided, a typed Verdict, an
|
||||
//! iterator-driven falsy/truthy gate chain, zero-copy borrows from the
|
||||
//! parsed arg straight through to the envelope.
|
||||
//!
|
||||
//! PYTHON-MIRRORING LAWS:
|
||||
//! * any stage value that is not a ladder rung (unknown string,
|
||||
//! non-string, null) → STAGES.index ValueError → next_stage None →
|
||||
//! can_promote says "already at the top (skill)".
|
||||
//! * missing stage key defaults to "episode"; missing sightings to 0.
|
||||
//! * provenance / contradictions / consequence / verified_outcome use
|
||||
//! Python truthiness (empty string/list/object, 0, null, false all
|
||||
//! falsy).
|
||||
//! * SIGHTINGS-INT LAW: the frozen truth exercises int sightings only
|
||||
//! — JSON cannot distinguish int 2 from float 2.0 and the oracle's
|
||||
//! f-string renders them differently.
|
||||
|
||||
use capability_scoring::pyjson::Json;
|
||||
|
||||
/// One rung of the promotion ladder, in causal order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
Episode,
|
||||
Fact,
|
||||
Observation,
|
||||
Concept,
|
||||
Belief,
|
||||
Rule,
|
||||
Skill,
|
||||
}
|
||||
|
||||
use Stage::{Belief, Concept, Episode, Fact, Observation, Rule, Skill};
|
||||
|
||||
impl Stage {
|
||||
/// All rungs, in ladder order (the oracle's STAGES tuple).
|
||||
pub const LADDER: [Stage; 7] = [
|
||||
Episode, Fact, Observation, Concept, Belief, Rule, Skill,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Episode => "episode",
|
||||
Fact => "fact",
|
||||
Observation => "observation",
|
||||
Concept => "concept",
|
||||
Belief => "belief",
|
||||
Rule => "rule",
|
||||
Skill => "skill",
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's STAGES.index — the only way into the ladder
|
||||
/// (unknown names fall off, mirroring the ValueError → None law).
|
||||
pub fn parse(name: &str) -> Option<Stage> {
|
||||
Self::LADDER.iter().copied().find(|s| s.name() == name)
|
||||
}
|
||||
|
||||
/// The stage after this one (None at the top).
|
||||
pub fn next(self) -> Option<Stage> {
|
||||
let idx = Self::LADDER.iter().position(|s| *s == self)?;
|
||||
Self::LADDER.get(idx + 1).copied()
|
||||
}
|
||||
|
||||
/// Evidence threshold per promotion (sightings required) — the
|
||||
/// oracle's THRESHOLDS table, exhaustive over the enum.
|
||||
pub fn threshold(self) -> f64 {
|
||||
match self {
|
||||
Episode => 1.0,
|
||||
Fact => 2.0,
|
||||
Observation | Concept => 3.0,
|
||||
Belief | Rule => 4.0,
|
||||
Skill => 5.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// belief+ carries an outcome-revision obligation.
|
||||
fn needs_verified_outcome(self) -> bool {
|
||||
matches!(self, Belief | Rule)
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness for JSON values (bools are ints; 0/0.0, "", [],
|
||||
/// {}, null, false are all falsy).
|
||||
pub fn truthy(v: &Json) -> bool {
|
||||
match v {
|
||||
Json::Null => false,
|
||||
Json::Bool(b) => *b,
|
||||
Json::Num(x) => *x != 0.0,
|
||||
Json::Str(s) => !s.is_empty(),
|
||||
Json::Arr(a) => !a.is_empty(),
|
||||
Json::Obj(o) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python f-string number form: integral values render without a
|
||||
/// fractional part; everything else is shortest round-trip repr
|
||||
/// (identical to Python for all values the truth exercises).
|
||||
fn fmt_num(x: f64) -> String {
|
||||
if x.fract() == 0.0 && x.is_finite() && x.abs() < 9.0e15 {
|
||||
format!("{}", x as i64)
|
||||
} else {
|
||||
format!("{}", x)
|
||||
}
|
||||
}
|
||||
|
||||
/// The item's stage as the oracle sees it. A missing key defaults to
|
||||
/// "episode"; anything that is not a ladder rung (unknown string,
|
||||
/// non-string value) takes the STAGES.index ValueError path.
|
||||
enum StageInput {
|
||||
Rung(Stage),
|
||||
Default,
|
||||
OffLadder,
|
||||
}
|
||||
|
||||
fn stage_input(item: &Json) -> StageInput {
|
||||
match item.get("stage") {
|
||||
None => StageInput::Default,
|
||||
Some(Json::Str(s)) => Stage::parse(s).map_or(StageInput::OffLadder, StageInput::Rung),
|
||||
Some(_) => StageInput::OffLadder,
|
||||
}
|
||||
}
|
||||
|
||||
/// A promotion verdict — the oracle's verdict doc, typed.
|
||||
struct Verdict {
|
||||
promote: bool,
|
||||
to: Option<Stage>,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
fn refuse(reason: impl Into<String>) -> Verdict {
|
||||
Verdict { promote: false, to: None, reason: reason.into() }
|
||||
}
|
||||
|
||||
fn grant(to: Stage) -> Verdict {
|
||||
Verdict {
|
||||
promote: true,
|
||||
to: Some(to),
|
||||
reason: format!("threshold met, promoting to {}", to.name()),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_pairs(&self) -> Vec<(String, Json)> {
|
||||
let mut pairs: Vec<(String, Json)> =
|
||||
vec![("promote".into(), Json::Bool(self.promote))];
|
||||
if let Some(to) = self.to {
|
||||
pairs.push(("to".into(), Json::Str(to.name().into())));
|
||||
}
|
||||
pairs.push(("reason".into(), Json::Str(self.reason.clone())));
|
||||
pairs
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Json {
|
||||
Json::Obj(self.to_pairs())
|
||||
}
|
||||
}
|
||||
|
||||
fn verdict_top() -> Verdict {
|
||||
Verdict::refuse("already at the top (skill)")
|
||||
}
|
||||
|
||||
/// Which truthiness a promotion gate tests, mirroring the oracle's
|
||||
/// `if not X` / `if X` asymmetry.
|
||||
enum Gate {
|
||||
Falsy,
|
||||
Truthy,
|
||||
}
|
||||
|
||||
impl Gate {
|
||||
fn blocks(&self, present: bool) -> bool {
|
||||
match self {
|
||||
Gate::Falsy => !present,
|
||||
Gate::Truthy => present,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The stage after this one (None at the top). A non-rung stage
|
||||
/// mirrors the oracle's STAGES.index ValueError → None.
|
||||
pub fn next_stage(stage: Option<&str>) -> Option<&'static str> {
|
||||
stage.and_then(Stage::parse).and_then(Stage::next).map(Stage::name)
|
||||
}
|
||||
|
||||
/// Can this item promote? Needs: evidence threshold met, provenance
|
||||
/// present, no open contradictions, a stated consequence, and (for
|
||||
/// belief+) a verified outcome.
|
||||
pub fn can_promote(item: &Json) -> Json {
|
||||
verdict(item).to_json()
|
||||
}
|
||||
|
||||
fn verdict(item: &Json) -> Verdict {
|
||||
let stage = match stage_input(item) {
|
||||
StageInput::Rung(s) => s,
|
||||
StageInput::Default => Episode,
|
||||
StageInput::OffLadder => return verdict_top(),
|
||||
};
|
||||
let nxt = match stage.next() {
|
||||
Some(n) => n,
|
||||
None => return verdict_top(),
|
||||
};
|
||||
let sightings = match item.get("sightings") {
|
||||
Some(Json::Num(x)) => *x,
|
||||
_ => 0.0,
|
||||
};
|
||||
let thr = stage.threshold();
|
||||
if sightings < thr {
|
||||
return Verdict::refuse(format!(
|
||||
"evidence {} < {}",
|
||||
fmt_num(sightings),
|
||||
fmt_num(thr)
|
||||
));
|
||||
}
|
||||
let gates = [
|
||||
("provenance", Gate::Falsy, "no provenance"),
|
||||
("contradictions", Gate::Truthy, "open contradictions"),
|
||||
("consequence", Gate::Falsy, "no stated consequence"),
|
||||
];
|
||||
if let Some(reason) = gates.iter().find_map(|(key, gate, reason)| {
|
||||
let present = item.get(key).map(truthy).unwrap_or(false);
|
||||
gate.blocks(present).then_some(*reason)
|
||||
}) {
|
||||
return Verdict::refuse(reason);
|
||||
}
|
||||
if stage.needs_verified_outcome()
|
||||
&& !item.get("verified_outcome").map(truthy).unwrap_or(false)
|
||||
{
|
||||
return Verdict::refuse("belief+ requires a verified outcome");
|
||||
}
|
||||
Verdict::grant(nxt)
|
||||
}
|
||||
|
||||
/// dict(item); item[k] = v — an existing key updates IN PLACE (keeping
|
||||
/// its position), a new key appends (Python 3.7+ dict law).
|
||||
fn set_key(item: &Json, key: &str, val: Json) -> Json {
|
||||
match item {
|
||||
Json::Obj(pairs) => {
|
||||
let mut out: Vec<(String, Json)> = pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), if k == key { val.clone() } else { v.clone() }))
|
||||
.collect();
|
||||
if !pairs.iter().any(|(k, _)| k == key) {
|
||||
out.push((key.to_string(), val));
|
||||
}
|
||||
Json::Obj(out)
|
||||
}
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_item(mut pairs: Vec<(String, Json)>, item: Json) -> Json {
|
||||
let mut out: Vec<(String, Json)> = Vec::with_capacity(pairs.len() + 1);
|
||||
out.push(("item".into(), item));
|
||||
out.append(&mut pairs);
|
||||
Json::Obj(out)
|
||||
}
|
||||
|
||||
/// Promote one stage (or return the refusal).
|
||||
pub fn promote(item: &Json) -> Json {
|
||||
let v = verdict(item);
|
||||
let out_item = if v.promote {
|
||||
let to = v
|
||||
.to
|
||||
.map(|s| Json::Str(s.name().into()))
|
||||
.unwrap_or(Json::Null);
|
||||
set_key(&set_key(item, "stage", to), "sightings", Json::Num(0.0))
|
||||
} else {
|
||||
item.clone()
|
||||
};
|
||||
with_item(v.to_pairs(), out_item)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! abstraction_ladder — FFI surface for the promotion ladder kernel
|
||||
//! (dichotomy wave #2479 tranche). Python facade keeps a
|
||||
//! RUNE_AL_CORE=python pin.
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok","value"|"err"}):
|
||||
//! * al_next_stage({"stage"}) -> stage string or null
|
||||
//! * al_can_promote({"item": {...}}) -> verdict doc
|
||||
//! * al_promote({"item": {...}}) -> {"item": ..., ...verdict}
|
||||
//!
|
||||
//! Pure kernel: no ops, no store seams, no op callback.
|
||||
//!
|
||||
//! Parity oracle: frozen_al_truth.json (76 cases, frozen from the live
|
||||
//! Python oracle).
|
||||
|
||||
pub mod ladder;
|
||||
|
||||
pub use capability_scoring::pyjson;
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use pyjson::Json;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<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)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
]),
|
||||
};
|
||||
CString::new(pyjson::dumps(&v))
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// al_next_stage({"stage"}) — the stage after this one, null at the top.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn al_next_stage(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| match a.get("stage") {
|
||||
Some(Json::Str(s)) => match ladder::next_stage(Some(s.as_str())) {
|
||||
Some(nxt) => Json::Str(nxt.into()),
|
||||
None => Json::Null,
|
||||
},
|
||||
// non-string / missing stage: the oracle's STAGES.index
|
||||
// ValueError collapses to None
|
||||
_ => Json::Null,
|
||||
}))
|
||||
}
|
||||
|
||||
/// al_can_promote({"item"}) — the verdict doc.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn al_can_promote(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| match a.get("item") {
|
||||
Some(item) => Ok(ladder::can_promote(item)),
|
||||
None => Err("missing item".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// al_promote({"item"}) — {"item": mutated copy, ...verdict}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn al_promote(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| match a.get("item") {
|
||||
Some(item) => Ok(ladder::promote(item)),
|
||||
None => Err("missing item".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
// NOTE: al_free_string is deliberately NOT exported — capability_scoring
|
||||
// (linked into the same Python process) already exports cs_free_string, and
|
||||
// a duplicate free symbol breaks the cdylib link. Callers free every
|
||||
// returned pointer through that path.
|
||||
Generated
+23
@@ -10,6 +10,28 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -76,6 +98,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
|
||||
@@ -8,4 +8,7 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# preserve_order: Python-dict insertion-order semantics — the behaviours
|
||||
# map is walked in insertion order (selective invalidation output order,
|
||||
# queue pre-sort order) exactly like the retired ep core.
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
@@ -0,0 +1,208 @@
|
||||
//! behaviour_ledger branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_bl_truth.json: 39 cases across all nine kernel-owned
|
||||
//! public functions and every branch — define minimal/full/prefixed-id/
|
||||
//! duplicate/unicode/None-deps, get both id forms/miss/empty, automated
|
||||
//! pass/fail/unknown/200-codepoint detail truncation/reverify-confirmed,
|
||||
//! real-world user confirmation/other-confirmer/fail/unknown,
|
||||
//! accept-limitation ok/custom-approver/unknown, selective invalidation
|
||||
//! dep-overlap default reason/owner-hit/no-overlap preserved/custom
|
||||
//! reason/reasons key miss/unconfirmed skip/mixed, retest history
|
||||
//! populated/unknown/empty, outstanding mixed sort + filters, summary
|
||||
//! empty/mixed integer counts). Frozen via scripts/
|
||||
//! gen_behaviour_ledger_truth.py with the clock pinned (no-clock law)
|
||||
//! and inputs deep-copied before the oracle ran (poisoned-truth law).
|
||||
//! Embedded VERBATIM — never transcribed.
|
||||
|
||||
use crate::{
|
||||
accept_limitation, define, get, invalidate_for_change, outstanding,
|
||||
retest_history, record_auto_verification, record_real_world, summary,
|
||||
BlError,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_bl_truth.json");
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][group]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
for case in cases(group) {
|
||||
let got = f(&case["input"]);
|
||||
assert_eq!(
|
||||
&got,
|
||||
&case["expected"],
|
||||
"[{}] case '{}' diverged",
|
||||
group,
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn ok(v: Value) -> Value {
|
||||
json!({"ok": true, "value": v})
|
||||
}
|
||||
|
||||
fn err_case(e: BlError) -> Value {
|
||||
json!({"ok": false, "err": e.to_string()})
|
||||
}
|
||||
|
||||
fn doc_of(i: &Value) -> Value {
|
||||
i["doc"].clone()
|
||||
}
|
||||
|
||||
/// A mutated-op envelope: kernel Ok → {"ok": true, "value": {doc, rec}} /
|
||||
/// {doc, recs}; kernel Err → the exact-Python-message error envelope.
|
||||
fn run_mut(
|
||||
i: &Value,
|
||||
f: impl FnOnce(&mut Value) -> Result<Value, BlError>,
|
||||
) -> Value {
|
||||
let mut doc = doc_of(i);
|
||||
match f(&mut doc) {
|
||||
Ok(rec) => ok(json!({"doc": doc, "rec": rec})),
|
||||
Err(e) => err_case(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn define_parity_battery() {
|
||||
replay("define", |i| {
|
||||
let deps: Vec<String> = i["dependencies"]
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut doc = doc_of(i);
|
||||
match define(
|
||||
&mut doc,
|
||||
i["behaviour_id"].as_str().unwrap(),
|
||||
i["owning_project"].as_str().unwrap(),
|
||||
i["intended_behaviour"].as_str().unwrap(),
|
||||
i["origin_intent"].as_str().unwrap_or(""),
|
||||
i["pass_criteria"].as_str().unwrap_or(""),
|
||||
i["fail_criteria"].as_str().unwrap_or(""),
|
||||
i["procedure"].as_str().unwrap_or(""),
|
||||
i["owner_component"].as_str().unwrap_or(""),
|
||||
&deps,
|
||||
i["automated_ref"].as_str().unwrap_or(""),
|
||||
i["environment"].as_str().unwrap_or(""),
|
||||
i["now"].as_str().unwrap(),
|
||||
) {
|
||||
Ok(rec) => ok(json!({"doc": doc, "rec": rec})),
|
||||
Err(e) => err_case(e),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_parity_battery() {
|
||||
replay("get", |i| ok(get(&doc_of(i), i["behaviour_id"].as_str().unwrap())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_auto_verification_parity_battery() {
|
||||
replay("record_auto_verification", |i| {
|
||||
run_mut(i, |doc| {
|
||||
record_auto_verification(
|
||||
doc,
|
||||
i["behaviour_id"].as_str().unwrap(),
|
||||
i["passed"].as_bool().unwrap(),
|
||||
i["detail"].as_str().unwrap_or(""),
|
||||
i["generation"].as_str().unwrap_or(""),
|
||||
i["now"].as_str().unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_real_world_parity_battery() {
|
||||
replay("record_real_world", |i| {
|
||||
run_mut(i, |doc| {
|
||||
record_real_world(
|
||||
doc,
|
||||
i["behaviour_id"].as_str().unwrap(),
|
||||
i["passed"].as_bool().unwrap(),
|
||||
i["detail"].as_str().unwrap_or(""),
|
||||
i["generation"].as_str().unwrap_or(""),
|
||||
i["confirmed_by"].as_str().unwrap_or("user"),
|
||||
i["now"].as_str().unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_limitation_parity_battery() {
|
||||
replay("accept_limitation", |i| {
|
||||
run_mut(i, |doc| {
|
||||
accept_limitation(
|
||||
doc,
|
||||
i["behaviour_id"].as_str().unwrap(),
|
||||
i["note"].as_str().unwrap_or(""),
|
||||
i["approved_by"].as_str().unwrap_or("user"),
|
||||
i["now"].as_str().unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalidate_for_change_parity_battery() {
|
||||
replay("invalidate_for_change", |i| {
|
||||
let changed: Vec<String> = i["changed_entities"]
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut doc = doc_of(i);
|
||||
match invalidate_for_change(
|
||||
&mut doc,
|
||||
&changed,
|
||||
i["generation"].as_str().unwrap_or(""),
|
||||
&i["reasons"],
|
||||
i["now"].as_str().unwrap(),
|
||||
) {
|
||||
Ok(recs) => ok(json!({"doc": doc, "recs": recs})),
|
||||
Err(e) => err_case(e),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retest_history_parity_battery() {
|
||||
replay("retest_history", |i| {
|
||||
ok(retest_history(
|
||||
&doc_of(i),
|
||||
i["behaviour_id"].as_str().unwrap(),
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outstanding_parity_battery() {
|
||||
replay("outstanding", |i| {
|
||||
ok(outstanding(
|
||||
&doc_of(i),
|
||||
i["component"].as_str().unwrap_or(""),
|
||||
i["project"].as_str().unwrap_or(""),
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_parity_battery() {
|
||||
replay("summary", |i| ok(summary(&doc_of(i))));
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,80 +1,809 @@
|
||||
//! behaviour_ledger — behaviour definition and tracking (W6).
|
||||
//! PARITY port of src/rune/behaviour_ledger.py.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
//! behaviour_ledger — the canonical Behaviour Validation Ledger (T19
|
||||
//! §19F/G/I). FULL parity port of src/rune/behaviour_ledger.py
|
||||
//! (dichotomy wave, issue #2479 tranche).
|
||||
//!
|
||||
//! PRIMARY UNIT: the VERSIONED BEHAVIOUR / TEST SPECIFICATION. Automated
|
||||
//! verification and real-world/user confirmation are DISTINCT truths
|
||||
//! (rule 19F): tests green ≠ user-confirmed.
|
||||
//!
|
||||
//! The kernel owns the state machine — define / get / transitions with
|
||||
//! generation-aware history (rule 19G) / selective invalidation (rule
|
||||
//! 19I) / the outstanding queue / the summary — over the parsed ledger
|
||||
//! doc. File glue (load/save of <store>/behaviour_ledger.json), the
|
||||
//! state constants and the class API stay in the Python facade.
|
||||
//!
|
||||
//! Python semantics preserved exactly (pinned by the frozen oracle
|
||||
//! truth, frozen_bl_truth.json, replayed VERBATIM by the branch
|
||||
//! battery):
|
||||
//! * id normalization: "behaviour:" prefix added unless present;
|
||||
//! * duplicate define → Err "behaviour exists: behaviour:X";
|
||||
//! * unknown behaviour → Err "unknown behaviour '<original id>'"
|
||||
//! (repr'd, NOT normalized);
|
||||
//! * history entries carry detail truncated to 200 CODEPOINTS;
|
||||
//! * green never implies USER_CONFIRMED (rule 19W-22) — the
|
||||
//! automated ceiling is AUTO_VERIFIED;
|
||||
//! * USER_CONFIRMED requires passed && confirmed_by == "user"
|
||||
//! (rule 19M); other confirmers land REAL_WORLD_PASSED;
|
||||
//! * invalidation touches CONFIRMED_STATES whose dependencies
|
||||
//! intersect the changed closure or whose owner is changed; every
|
||||
//! invalidation carries a reason — default
|
||||
//! "structural change touched: ['a', 'b']" (Python list repr,
|
||||
//! sorted); unrelated confirmations are PRESERVED;
|
||||
//! * outstanding excludes USER_CONFIRMED / ACCEPTED_LIMITATION /
|
||||
//! NOT_APPLICABLE, sorts TEST_FAILED first then behaviour_id;
|
||||
//! * summary counts are Python ints.
|
||||
//!
|
||||
//! No-clock law: `now` is always an argument — the kernel never reads
|
||||
//! the wall clock.
|
||||
//!
|
||||
//! FFI (doc-envelope law): every symbol takes ONE JSON doc and returns
|
||||
//! ONE JSON doc {"ok": true, "value": ...} / {"ok": false, "err": ...}.
|
||||
//! Mutating ops return the full new doc alongside the record(s).
|
||||
//! Strings are freed via capability_scoring's cs_free_string — this
|
||||
//! crate exports NO free symbol of its own (duplicate exports break
|
||||
//! the cdylib link when sibling crates share one process).
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::c_char;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BehaviourEntry {
|
||||
pub behaviour_id: String,
|
||||
pub owning_project: String,
|
||||
pub description: String,
|
||||
pub evidence_refs: Vec<String>,
|
||||
pub defined_at: String,
|
||||
pub status: String,
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
pub const LEDGER_SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
// hardening lifecycle (rule 19G semantics)
|
||||
pub const UNVALIDATED: &str = "UNVALIDATED";
|
||||
pub const IMPLEMENTED: &str = "IMPLEMENTED";
|
||||
pub const AUTO_VERIFIED: &str = "AUTO_VERIFIED";
|
||||
pub const REAL_WORLD_PASSED: &str = "REAL_WORLD_PASSED";
|
||||
pub const TEST_FAILED: &str = "TEST_FAILED";
|
||||
pub const PARTIAL: &str = "PARTIAL";
|
||||
pub const USER_CONFIRMED: &str = "USER_CONFIRMED";
|
||||
pub const RETEST_REQUIRED: &str = "RETEST_REQUIRED";
|
||||
pub const ACCEPTED_LIMITATION: &str = "ACCEPTED_LIMITATION";
|
||||
pub const NOT_APPLICABLE: &str = "NOT_APPLICABLE";
|
||||
|
||||
pub const STATES: [&str; 10] = [
|
||||
UNVALIDATED,
|
||||
IMPLEMENTED,
|
||||
AUTO_VERIFIED,
|
||||
REAL_WORLD_PASSED,
|
||||
TEST_FAILED,
|
||||
PARTIAL,
|
||||
USER_CONFIRMED,
|
||||
RETEST_REQUIRED,
|
||||
ACCEPTED_LIMITATION,
|
||||
NOT_APPLICABLE,
|
||||
];
|
||||
|
||||
// ── typed domain lattice (Phase B) ───────────────────────────────────────────
|
||||
|
||||
/// The hardening lifecycle as a closed Rust lattice. Ledger docs carry
|
||||
/// free-text states (JSON), so the lattice is open at the tail: `Other`
|
||||
/// keeps unknown values pass-through exactly like the oracle's string
|
||||
/// comparisons — consulted nowhere, matched exhaustively everywhere.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
enum BlState {
|
||||
Unvalidated,
|
||||
Implemented,
|
||||
AutoVerified,
|
||||
RealWorldPassed,
|
||||
TestFailed,
|
||||
Partial,
|
||||
UserConfirmed,
|
||||
RetestRequired,
|
||||
AcceptedLimitation,
|
||||
NotApplicable,
|
||||
/// Any non-lattice state from the doc — carries the raw string so
|
||||
/// the transition error message stays byte-exact.
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub struct BehaviourLedger {
|
||||
pub behaviours: BTreeMap<String, BehaviourEntry>,
|
||||
}
|
||||
|
||||
impl BehaviourLedger {
|
||||
pub fn new() -> Self {
|
||||
BehaviourLedger {
|
||||
behaviours: BTreeMap::new(),
|
||||
impl BlState {
|
||||
fn parse(s: &str) -> BlState {
|
||||
match s {
|
||||
UNVALIDATED => BlState::Unvalidated,
|
||||
IMPLEMENTED => BlState::Implemented,
|
||||
AUTO_VERIFIED => BlState::AutoVerified,
|
||||
REAL_WORLD_PASSED => BlState::RealWorldPassed,
|
||||
TEST_FAILED => BlState::TestFailed,
|
||||
PARTIAL => BlState::Partial,
|
||||
USER_CONFIRMED => BlState::UserConfirmed,
|
||||
RETEST_REQUIRED => BlState::RetestRequired,
|
||||
ACCEPTED_LIMITATION => BlState::AcceptedLimitation,
|
||||
NOT_APPLICABLE => BlState::NotApplicable,
|
||||
other => BlState::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn define(
|
||||
&mut self,
|
||||
behaviour_id: &str,
|
||||
owning_project: &str,
|
||||
description: &str,
|
||||
evidence_refs: Vec<String>,
|
||||
) -> BehaviourEntry {
|
||||
let entry = BehaviourEntry {
|
||||
behaviour_id: behaviour_id.to_string(),
|
||||
owning_project: owning_project.to_string(),
|
||||
description: description.to_string(),
|
||||
evidence_refs,
|
||||
defined_at: now(),
|
||||
status: "defined".to_string(),
|
||||
/// The wire label. `Other` never labels a kernel transition — the
|
||||
/// empty string keeps the match total without inventing a state.
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
BlState::Unvalidated => UNVALIDATED,
|
||||
BlState::Implemented => IMPLEMENTED,
|
||||
BlState::AutoVerified => AUTO_VERIFIED,
|
||||
BlState::RealWorldPassed => REAL_WORLD_PASSED,
|
||||
BlState::TestFailed => TEST_FAILED,
|
||||
BlState::Partial => PARTIAL,
|
||||
BlState::UserConfirmed => USER_CONFIRMED,
|
||||
BlState::RetestRequired => RETEST_REQUIRED,
|
||||
BlState::AcceptedLimitation => ACCEPTED_LIMITATION,
|
||||
BlState::NotApplicable => NOT_APPLICABLE,
|
||||
BlState::Other(_) => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// STANDING CONFIDENCE (rule 19I): selective invalidation may
|
||||
/// reopen exactly these; everything else is already open or n/a.
|
||||
fn is_confirmed(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
BlState::AutoVerified | BlState::RealWorldPassed | BlState::UserConfirmed
|
||||
)
|
||||
}
|
||||
|
||||
/// The outstanding queue excludes confirmed / accepted / n.a.
|
||||
fn is_queue_excluded(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
BlState::UserConfirmed | BlState::AcceptedLimitation | BlState::NotApplicable
|
||||
)
|
||||
}
|
||||
|
||||
/// Queue sort key: TEST_FAILED first.
|
||||
fn is_test_failed(&self) -> bool {
|
||||
matches!(self, BlState::TestFailed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed kernel errors — the caller-visible `err` string is byte-exact
|
||||
/// Python (pinned by the battery); the enum only shapes the Rust side.
|
||||
pub enum BlError {
|
||||
BehaviourExists(String),
|
||||
UnknownBehaviour(String),
|
||||
UnknownValidationState(String),
|
||||
CorruptLedger,
|
||||
}
|
||||
|
||||
impl fmt::Display for BlError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BlError::BehaviourExists(bid) => write!(f, "behaviour exists: {bid}"),
|
||||
BlError::UnknownBehaviour(bid) => {
|
||||
write!(f, "unknown behaviour {}", py_repr(bid))
|
||||
}
|
||||
BlError::UnknownValidationState(s) => {
|
||||
write!(f, "unknown validation state {}", py_repr(s))
|
||||
}
|
||||
BlError::CorruptLedger => {
|
||||
write!(f, "corrupt ledger: behaviours is not an object")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Python repr of a string: single quotes, escaped backslash/quote.
|
||||
fn py_repr(s: &str) -> String {
|
||||
let mut out = String::from("'");
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\'' => out.push_str("\\'"),
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
out.push('\'');
|
||||
out
|
||||
}
|
||||
|
||||
/// Python repr of a list of strings: ['a', 'b'].
|
||||
fn py_list_repr(items: &[String]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| py_repr(s)).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
|
||||
fn norm_id(behaviour_id: &str) -> String {
|
||||
if behaviour_id.starts_with("behaviour:") {
|
||||
behaviour_id.to_string()
|
||||
} else {
|
||||
format!("behaviour:{}", behaviour_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// First `n` CODEPOINTS of `s` (Python `s[:200]`).
|
||||
fn chars_upto(s: &str, n: usize) -> String {
|
||||
s.chars().take(n).collect()
|
||||
}
|
||||
|
||||
/// Zero-copy borrowed string field (Python `rec.get(key, "")`).
|
||||
fn str_field<'a>(v: &'a Value, key: &str) -> &'a str {
|
||||
v.get(key).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
/// doc["behaviours"] pairs (Err when the ledger is malformed).
|
||||
fn behaviours_pairs(doc: &Value) -> Result<&Map<String, Value>, BlError> {
|
||||
match doc.get("behaviours").and_then(Value::as_object) {
|
||||
Some(m) => Ok(m),
|
||||
None => Err(BlError::CorruptLedger),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a behaviour record (normalized id) from the full doc.
|
||||
fn get_rec(doc: &Value, bid: &str) -> Option<Value> {
|
||||
behaviours_pairs(doc).ok()?.get(bid).cloned()
|
||||
}
|
||||
|
||||
/// doc["behaviours"][bid] = val (insert or replace).
|
||||
fn set_rec(doc: &mut Value, bid: &str, val: Value) {
|
||||
if let Some(bp) = doc.get_mut("behaviours").and_then(Value::as_object_mut) {
|
||||
bp.insert(bid.to_string(), val);
|
||||
}
|
||||
}
|
||||
|
||||
/// Append to a record's array field (evidence / history / limitations /
|
||||
/// invalidations).
|
||||
fn push_to_arr(rec: &mut Value, key: &str, val: Value) {
|
||||
if let Some(a) = rec.get_mut(key).and_then(Value::as_array_mut) {
|
||||
a.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
// ── transitions ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// _transition — history append + state + updated_at; a non-lattice
|
||||
/// state → Err (the repr'd original, byte-exact).
|
||||
fn transition(
|
||||
rec: &mut Value,
|
||||
new_state: BlState,
|
||||
kind: &str,
|
||||
detail: &str,
|
||||
generation: &str,
|
||||
now: &str,
|
||||
) -> Result<(), BlError> {
|
||||
let label = match new_state {
|
||||
BlState::Other(raw) => return Err(BlError::UnknownValidationState(raw)),
|
||||
known => known.label(),
|
||||
};
|
||||
let from = str_field(rec, "state");
|
||||
push_to_arr(
|
||||
rec,
|
||||
"history",
|
||||
json!({
|
||||
"at": now,
|
||||
"from": from,
|
||||
"to": label,
|
||||
"kind": kind,
|
||||
"detail": chars_upto(detail, 200),
|
||||
"generation": generation,
|
||||
}),
|
||||
);
|
||||
if let Some(obj) = rec.as_object_mut() {
|
||||
obj.insert("state".to_string(), json!(label));
|
||||
obj.insert("updated_at".to_string(), json!(now));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── public operations (name-preserving ports of the oracle) ─────────────────
|
||||
|
||||
/// define — duplicate → Err; returns the new record.
|
||||
#[allow(clippy::too_many_arguments)] // pinned: parity signature — the port mirrors the oracle arg-for-arg
|
||||
pub fn define(
|
||||
doc: &mut Value,
|
||||
behaviour_id: &str,
|
||||
owning_project: &str,
|
||||
intended_behaviour: &str,
|
||||
origin_intent: &str,
|
||||
pass_criteria: &str,
|
||||
fail_criteria: &str,
|
||||
procedure: &str,
|
||||
owner_component: &str,
|
||||
dependencies: &[String],
|
||||
automated_ref: &str,
|
||||
environment: &str,
|
||||
now: &str,
|
||||
) -> Result<Value, BlError> {
|
||||
let bid = norm_id(behaviour_id);
|
||||
behaviours_pairs(doc)?;
|
||||
if get_rec(doc, &bid).is_some() {
|
||||
return Err(BlError::BehaviourExists(bid));
|
||||
}
|
||||
let rec = json!({
|
||||
"behaviour_id": bid,
|
||||
"owning_project": owning_project,
|
||||
"intended_behaviour": intended_behaviour,
|
||||
"origin_intent": origin_intent,
|
||||
"pass_criteria": pass_criteria,
|
||||
"fail_criteria": fail_criteria,
|
||||
"procedure": procedure,
|
||||
"owner_component": owner_component,
|
||||
"dependencies": dependencies,
|
||||
"automated_ref": automated_ref,
|
||||
"environment": environment,
|
||||
"state": UNVALIDATED,
|
||||
"evidence": [],
|
||||
"history": [],
|
||||
"invalidations": [],
|
||||
"limitations": [],
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
});
|
||||
set_rec(doc, &bid, rec.clone());
|
||||
Ok(rec)
|
||||
}
|
||||
|
||||
/// get — normalized lookup; Null when absent.
|
||||
pub fn get(doc: &Value, behaviour_id: &str) -> Value {
|
||||
get_rec(doc, &norm_id(behaviour_id)).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
/// record_auto_verification — ceiling AUTO_VERIFIED (green never implies
|
||||
/// USER_CONFIRMED).
|
||||
pub fn record_auto_verification(
|
||||
doc: &mut Value,
|
||||
behaviour_id: &str,
|
||||
passed: bool,
|
||||
detail: &str,
|
||||
generation: &str,
|
||||
now: &str,
|
||||
) -> Result<Value, BlError> {
|
||||
let bid = norm_id(behaviour_id);
|
||||
let mut rec = get_rec(doc, &bid).ok_or_else(|| {
|
||||
BlError::UnknownBehaviour(behaviour_id.to_string())
|
||||
})?;
|
||||
push_to_arr(
|
||||
&mut rec,
|
||||
"evidence",
|
||||
json!({
|
||||
"at": now,
|
||||
"kind": "automated",
|
||||
"passed": passed,
|
||||
"detail": chars_upto(detail, 200),
|
||||
"generation": generation,
|
||||
}),
|
||||
);
|
||||
let new_state = if passed {
|
||||
BlState::AutoVerified
|
||||
} else {
|
||||
BlState::TestFailed
|
||||
};
|
||||
transition(
|
||||
&mut rec,
|
||||
new_state,
|
||||
"automated_verification",
|
||||
detail,
|
||||
generation,
|
||||
now,
|
||||
)?;
|
||||
set_rec(doc, &bid, rec.clone());
|
||||
Ok(rec)
|
||||
}
|
||||
|
||||
/// record_real_world — USER_CONFIRMED iff passed && confirmed_by == "user".
|
||||
pub fn record_real_world(
|
||||
doc: &mut Value,
|
||||
behaviour_id: &str,
|
||||
passed: bool,
|
||||
detail: &str,
|
||||
generation: &str,
|
||||
confirmed_by: &str,
|
||||
now: &str,
|
||||
) -> Result<Value, BlError> {
|
||||
let bid = norm_id(behaviour_id);
|
||||
let mut rec = get_rec(doc, &bid).ok_or_else(|| {
|
||||
BlError::UnknownBehaviour(behaviour_id.to_string())
|
||||
})?;
|
||||
push_to_arr(
|
||||
&mut rec,
|
||||
"evidence",
|
||||
json!({
|
||||
"at": now,
|
||||
"kind": "real_world",
|
||||
"passed": passed,
|
||||
"detail": chars_upto(detail, 200),
|
||||
"generation": generation,
|
||||
"by": confirmed_by,
|
||||
}),
|
||||
);
|
||||
let new_state = match (passed, confirmed_by == "user") {
|
||||
(true, true) => BlState::UserConfirmed,
|
||||
(true, false) => BlState::RealWorldPassed,
|
||||
(false, _) => BlState::TestFailed,
|
||||
};
|
||||
transition(&mut rec, new_state, "real_world", detail, generation, now)?;
|
||||
set_rec(doc, &bid, rec.clone());
|
||||
Ok(rec)
|
||||
}
|
||||
|
||||
/// accept_limitation — explicitly approved gap.
|
||||
pub fn accept_limitation(
|
||||
doc: &mut Value,
|
||||
behaviour_id: &str,
|
||||
note: &str,
|
||||
approved_by: &str,
|
||||
now: &str,
|
||||
) -> Result<Value, BlError> {
|
||||
let bid = norm_id(behaviour_id);
|
||||
let mut rec = get_rec(doc, &bid).ok_or_else(|| {
|
||||
BlError::UnknownBehaviour(behaviour_id.to_string())
|
||||
})?;
|
||||
push_to_arr(
|
||||
&mut rec,
|
||||
"limitations",
|
||||
json!({
|
||||
"at": now,
|
||||
"note": chars_upto(note, 200),
|
||||
"approved_by": approved_by,
|
||||
}),
|
||||
);
|
||||
transition(
|
||||
&mut rec,
|
||||
BlState::AcceptedLimitation,
|
||||
"accept_limitation",
|
||||
note,
|
||||
"",
|
||||
now,
|
||||
)?;
|
||||
set_rec(doc, &bid, rec.clone());
|
||||
Ok(rec)
|
||||
}
|
||||
|
||||
/// invalidate_for_change — selective invalidation (rule 19I); returns the
|
||||
/// invalidated records. Only CONFIRMED_STATES whose dependency set
|
||||
/// intersects the changed closure (or whose owner is changed) reopen;
|
||||
/// every invalidation carries a reason; unrelated confirmations stay.
|
||||
pub fn invalidate_for_change(
|
||||
doc: &mut Value,
|
||||
changed: &[String],
|
||||
generation: &str,
|
||||
reasons: &Value,
|
||||
now: &str,
|
||||
) -> Result<Value, BlError> {
|
||||
let changed_set: BTreeSet<String> = changed.iter().cloned().collect();
|
||||
let bids: Vec<String> = behaviours_pairs(doc)?.keys().cloned().collect();
|
||||
let mut invalidated: Vec<Value> = Vec::new();
|
||||
for bid in bids {
|
||||
let rec = match get_rec(doc, &bid) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
self.behaviours
|
||||
.insert(behaviour_id.to_string(), entry.clone());
|
||||
entry
|
||||
if !BlState::parse(str_field(&rec, "state")).is_confirmed() {
|
||||
continue;
|
||||
}
|
||||
let deps: BTreeSet<String> = rec
|
||||
.get("dependencies")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let owner = str_field(&rec, "owner_component");
|
||||
let mut overlap: BTreeSet<String> = deps.intersection(&changed_set).cloned().collect();
|
||||
if !owner.is_empty() && changed_set.contains(owner) {
|
||||
overlap.insert(owner.to_string());
|
||||
}
|
||||
if overlap.is_empty() {
|
||||
continue; // unrelated hardened behaviour: PRESERVED (19S-5)
|
||||
}
|
||||
let overlap_sorted: Vec<String> = overlap.into_iter().collect();
|
||||
let bid_val = str_field(&rec, "behaviour_id").to_string();
|
||||
let because = match reasons.get(&bid_val).and_then(Value::as_str) {
|
||||
Some(r) => r.to_string(),
|
||||
None => format!(
|
||||
"structural change touched: {}",
|
||||
py_list_repr(&overlap_sorted)
|
||||
),
|
||||
};
|
||||
let was = str_field(&rec, "state").to_string();
|
||||
let mut rec2 = rec.clone();
|
||||
push_to_arr(
|
||||
&mut rec2,
|
||||
"invalidations",
|
||||
json!({
|
||||
"at": now,
|
||||
"generation": generation,
|
||||
"because": chars_upto(&because, 200),
|
||||
"was": was,
|
||||
"changed_entities": overlap_sorted,
|
||||
}),
|
||||
);
|
||||
let detail = format!("was {}; {}", was, because);
|
||||
transition(
|
||||
&mut rec2,
|
||||
BlState::RetestRequired,
|
||||
"selective_invalidation",
|
||||
&detail,
|
||||
generation,
|
||||
now,
|
||||
)?;
|
||||
set_rec(doc, &bid, rec2.clone());
|
||||
invalidated.push(rec2);
|
||||
}
|
||||
Ok(Value::Array(invalidated))
|
||||
}
|
||||
|
||||
pub fn get(&self, behaviour_id: &str) -> Option<&BehaviourEntry> {
|
||||
self.behaviours.get(behaviour_id)
|
||||
/// retest_history — all events oldest-first for one behaviour (rule
|
||||
/// 19S-12); empty for unknown.
|
||||
pub fn retest_history(doc: &Value, behaviour_id: &str) -> Value {
|
||||
let rec = get(doc, behaviour_id);
|
||||
let events: Vec<Value> = rec
|
||||
.get("history")
|
||||
.and_then(Value::as_array)
|
||||
.map(|hist| {
|
||||
hist.iter()
|
||||
.map(|h| {
|
||||
json!({
|
||||
"at": str_field(h, "at"),
|
||||
"generation": str_field(h, "generation"),
|
||||
"event": format!(
|
||||
"{}→{}",
|
||||
str_field(h, "from"),
|
||||
str_field(h, "to")
|
||||
),
|
||||
"kind": str_field(h, "kind"),
|
||||
"detail": str_field(h, "detail"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Value::Array(events)
|
||||
}
|
||||
|
||||
/// outstanding — eligible testing items, TEST_FAILED first then id;
|
||||
/// confirmed / accepted / n.a. excluded. The queue re-derives from
|
||||
/// states after every fix (rule 19M).
|
||||
pub fn outstanding(doc: &Value, component: &str, project: &str) -> Value {
|
||||
let mut out: Vec<Value> = match behaviours_pairs(doc) {
|
||||
Ok(pairs) => pairs
|
||||
.values()
|
||||
.filter(|rec| {
|
||||
!BlState::parse(str_field(rec, "state")).is_queue_excluded()
|
||||
})
|
||||
.filter(|rec| project.is_empty()
|
||||
|| str_field(rec, "owning_project") == project)
|
||||
.filter(|rec| component.is_empty()
|
||||
|| str_field(rec, "owner_component") == component)
|
||||
.cloned()
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
out.sort_by(|a, b| {
|
||||
let ka = u8::from(!BlState::parse(str_field(a, "state")).is_test_failed());
|
||||
let kb = u8::from(!BlState::parse(str_field(b, "state")).is_test_failed());
|
||||
(ka, str_field(a, "behaviour_id")).cmp(&(kb, str_field(b, "behaviour_id")))
|
||||
});
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
/// summary — {total, states: {state: count}, outstanding} with Python
|
||||
/// int counts.
|
||||
pub fn summary(doc: &Value) -> Value {
|
||||
let mut counts: Vec<(String, i64)> = Vec::new();
|
||||
let mut total: i64 = 0;
|
||||
if let Ok(pairs) = behaviours_pairs(doc) {
|
||||
for rec in pairs.values() {
|
||||
let state = str_field(rec, "state").to_string();
|
||||
total += 1;
|
||||
match counts.iter_mut().find(|(k, _)| *k == state) {
|
||||
Some((_, n)) => *n += 1,
|
||||
None => counts.push((state, 1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let states: Map<String, Value> = counts
|
||||
.into_iter()
|
||||
.map(|(k, n)| (k, json!(n)))
|
||||
.collect();
|
||||
let out_len = outstanding(doc, "", "")
|
||||
.as_array()
|
||||
.map(Vec::len)
|
||||
.unwrap_or(0);
|
||||
json!({
|
||||
"total": total,
|
||||
"states": states,
|
||||
"outstanding": out_len,
|
||||
})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
|
||||
|
||||
/// Typed FFI failure lattice behind the doc-envelope Result — the
|
||||
/// caller-visible `err` string is unchanged (enum only shapes the Rust
|
||||
/// side; parity is pinned by the battery).
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
/// Kernel-domain error with the exact Python message.
|
||||
Domain(BlError),
|
||||
}
|
||||
|
||||
impl fmt::Display for FfiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FfiError::NullPtr => write!(f, "null argument pointer"),
|
||||
FfiError::Utf8(e) => write!(f, "{e}"),
|
||||
FfiError::Json(e) => write!(f, "{e}"),
|
||||
FfiError::Domain(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BehaviourLedger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
|
||||
if ptr.is_null() {
|
||||
return Err(FfiError::NullPtr);
|
||||
}
|
||||
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(FfiError::Utf8)?;
|
||||
serde_json::from_str(x).map_err(FfiError::Json)
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
format!(
|
||||
"{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
)
|
||||
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e.to_string()}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_define_and_get() {
|
||||
let mut ledger = BehaviourLedger::new();
|
||||
ledger.define("b1", "rune", "test behaviour", vec!["evidence1".into()]);
|
||||
let b = ledger.get("b1");
|
||||
assert!(b.is_some());
|
||||
assert_eq!(b.unwrap().owning_project, "rune");
|
||||
}
|
||||
fn s<'a>(a: &'a Value, key: &str) -> &'a str {
|
||||
a.get(key).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_define(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let deps: Vec<String> = a
|
||||
.get("dependencies")
|
||||
.and_then(Value::as_array)
|
||||
.map(|x| {
|
||||
x.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
match define(
|
||||
&mut doc,
|
||||
s(&a, "behaviour_id"),
|
||||
s(&a, "owning_project"),
|
||||
s(&a, "intended_behaviour"),
|
||||
s(&a, "origin_intent"),
|
||||
s(&a, "pass_criteria"),
|
||||
s(&a, "fail_criteria"),
|
||||
s(&a, "procedure"),
|
||||
s(&a, "owner_component"),
|
||||
&deps,
|
||||
s(&a, "automated_ref"),
|
||||
s(&a, "environment"),
|
||||
s(&a, "now"),
|
||||
) {
|
||||
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
|
||||
Err(e) => Err(FfiError::Domain(e)),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_get(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
get(a.get("doc").unwrap_or(&Value::Null), s(&a, "behaviour_id"))
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_record_auto(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
match record_auto_verification(
|
||||
&mut doc,
|
||||
s(&a, "behaviour_id"),
|
||||
a.get("passed").and_then(Value::as_bool).unwrap_or(false),
|
||||
s(&a, "detail"),
|
||||
s(&a, "generation"),
|
||||
s(&a, "now"),
|
||||
) {
|
||||
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
|
||||
Err(e) => Err(FfiError::Domain(e)),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_record_real_world(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
match record_real_world(
|
||||
&mut doc,
|
||||
s(&a, "behaviour_id"),
|
||||
a.get("passed").and_then(Value::as_bool).unwrap_or(false),
|
||||
s(&a, "detail"),
|
||||
s(&a, "generation"),
|
||||
s(&a, "confirmed_by"),
|
||||
s(&a, "now"),
|
||||
) {
|
||||
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
|
||||
Err(e) => Err(FfiError::Domain(e)),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_accept_limitation(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
match accept_limitation(
|
||||
&mut doc,
|
||||
s(&a, "behaviour_id"),
|
||||
s(&a, "note"),
|
||||
s(&a, "approved_by"),
|
||||
s(&a, "now"),
|
||||
) {
|
||||
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
|
||||
Err(e) => Err(FfiError::Domain(e)),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_invalidate(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let changed: Vec<String> = a
|
||||
.get("changed_entities")
|
||||
.and_then(Value::as_array)
|
||||
.map(|x| {
|
||||
x.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let reasons = a.get("reasons").cloned().unwrap_or(Value::Null);
|
||||
match invalidate_for_change(
|
||||
&mut doc,
|
||||
&changed,
|
||||
s(&a, "generation"),
|
||||
&reasons,
|
||||
s(&a, "now"),
|
||||
) {
|
||||
Ok(recs) => Ok(json!({"doc": doc, "recs": recs})),
|
||||
Err(e) => Err(FfiError::Domain(e)),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_retest_history(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
retest_history(a.get("doc").unwrap_or(&Value::Null), s(&a, "behaviour_id"))
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_outstanding(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
outstanding(
|
||||
a.get("doc").unwrap_or(&Value::Null),
|
||||
s(&a, "component"),
|
||||
s(&a, "project"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bl_summary(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| summary(a.get("doc").unwrap_or(&Value::Null))))
|
||||
}
|
||||
+5
-96
@@ -2,104 +2,13 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "construction_patterns_builtin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"capability_scoring",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
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"
|
||||
@@ -2,9 +2,11 @@
|
||||
name = "construction_patterns_builtin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Built-in construction pattern registry — dichotomy wave #2479 tranche; zero-dep (pyjson via capability_scoring)"
|
||||
|
||||
[lib]
|
||||
name = "construction_patterns_builtin"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
capability_scoring = { path = "../capability_scoring" }
|
||||
@@ -0,0 +1,365 @@
|
||||
{
|
||||
"revision": 1,
|
||||
"patterns": [
|
||||
{
|
||||
"pattern_id": "capability-module-adaptation",
|
||||
"purpose": "Adapting or extending an existing capability/module: prove the NEW behavior, preserve the OLD behavior (BUILD BUT DON'T BREAK).",
|
||||
"revision": 1,
|
||||
"maturity": "UNIT-PROVEN",
|
||||
"signals": {
|
||||
"entities_match": [
|
||||
"module:",
|
||||
"capability:"
|
||||
],
|
||||
"path_contains": [
|
||||
"src/"
|
||||
],
|
||||
"objective_any": [
|
||||
"adapt",
|
||||
"extend",
|
||||
"modify",
|
||||
"preserve",
|
||||
"existing",
|
||||
"capability",
|
||||
"repair"
|
||||
]
|
||||
},
|
||||
"obligations": [
|
||||
{
|
||||
"id": "adapt-new-behavior-proof",
|
||||
"obligation": "new behavior proven by tests before closure",
|
||||
"when": {
|
||||
"always": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "adapt-old-behavior-preserved",
|
||||
"obligation": "existing capability behavior preserved; original contracts remain valid",
|
||||
"when": {
|
||||
"always": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "adapt-consumers-verified",
|
||||
"obligation": "existing consumers of the adapted capability verified against their contracts",
|
||||
"when": {
|
||||
"entities_match": [
|
||||
"module:",
|
||||
"capability:"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "adapt-migration-detected",
|
||||
"obligation": "migration obligations detected and recorded if any consumer-visible interface changes",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
"LIVE-CONTRACT-QUALITY-001 (adaptation resolved + closed live)",
|
||||
"SCAFFOLD-TRANCHE-001 (build-but-don't-break established)",
|
||||
"SCAFFOLD-TRANCHE-003 (adaptation two-proof rule)",
|
||||
"construction_decision.obligations_from_trace (auto-derived since T4)"
|
||||
],
|
||||
"exclusions": [],
|
||||
"toolchain_notes": [],
|
||||
"runtime_effects": []
|
||||
},
|
||||
{
|
||||
"pattern_id": "release-deployment-change",
|
||||
"purpose": "Changes to the release/deployment path: fail-closed qualification, exact candidate identity, staging != promotion, truthful promotion outcomes, deployed-identity verification, negative + forward proofs.",
|
||||
"revision": 1,
|
||||
"maturity": "UNIT-PROVEN",
|
||||
"signals": {
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"promotion",
|
||||
"qualify",
|
||||
"release.yml",
|
||||
"ci.yml",
|
||||
"deploy_qualify",
|
||||
"deploy_stage",
|
||||
"deploy_promotion"
|
||||
],
|
||||
"objective_any": [
|
||||
"deploy",
|
||||
"release",
|
||||
"promot",
|
||||
"qualif",
|
||||
"candidate",
|
||||
"current",
|
||||
"monotonic"
|
||||
],
|
||||
"workflow_config_signals": true
|
||||
},
|
||||
"obligations": [
|
||||
{
|
||||
"id": "rel-fail-closed-qualification",
|
||||
"obligation": "CI qualification remains fail-closed (exact SHA, two-leg verdict); red candidates never deploy",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"release",
|
||||
"qualify",
|
||||
"ci.yml"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-exact-candidate-identity",
|
||||
"obligation": "deployment addresses the EXACT pushed revision, never whatever HEAD is at deploy time",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"release"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-staging-promotion-distinction",
|
||||
"obligation": "staged candidate != promoted candidate; promotion outcome must be independently verifiable",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"promotion",
|
||||
"stage"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-promotion-truthfulness",
|
||||
"obligation": "promotion outcomes are truthful (PROMOTED / NOOP_ALREADY_CURRENT / STALE_SUPERSEDED / REFUSED_*); stale candidates never claim success",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"promotion",
|
||||
"deploy_stage",
|
||||
"deploy_promotion"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-monotonic-promotion",
|
||||
"obligation": "promotion ordering: an older candidate finishing later cannot regress current; ancestry check + fenced critical section",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"deploy_promotion",
|
||||
"deploy_stage",
|
||||
"promotion"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-negative-candidate-proof",
|
||||
"obligation": "negative proof: red/unqualified candidate is refused; stale candidate cannot promote",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"promotion",
|
||||
"qualify"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-forward-candidate-proof",
|
||||
"obligation": "forward proof: a green newer candidate still promotes normally",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"promotion"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-deployed-identity-verification",
|
||||
"obligation": "deployed identity verified from the deployment authority (current symlink + build identity), not from job artifacts alone",
|
||||
"when": {
|
||||
"always": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel-release-regression-verification",
|
||||
"obligation": "existing release-safety guarantees re-proven (the guarantees that already exist must not silently weaken)",
|
||||
"when": {
|
||||
"always": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
"DEPLOY-MONOTONIC-001 (monotonic promotion + truthful outcomes, live-proven)",
|
||||
"maintenance:deploy-order-race (live incident: deploys 7355/7359)",
|
||||
"release-safety repair 2026-09-13 (red-promotion incidents 0.66.28/29: required-checks aggregator lied)",
|
||||
"scripts/deploy_promotion.py (ancestry + fenced critical section)",
|
||||
"tests/test_deploy_stage_race.py (22-test matrix incl. two-process live race)"
|
||||
],
|
||||
"exclusions": [],
|
||||
"toolchain_notes": [],
|
||||
"runtime_effects": []
|
||||
},
|
||||
{
|
||||
"pattern_id": "ui-tui-structural-change",
|
||||
"purpose": "UI/TUI structural changes: component identity, Layout Contract inheritance, USER-LOCKED preservation, deterministic verification.",
|
||||
"revision": 1,
|
||||
"maturity": "UNIT-PROVEN",
|
||||
"signals": {
|
||||
"surface_kinds": [
|
||||
"ui_component",
|
||||
"layout_region"
|
||||
],
|
||||
"path_contains": [
|
||||
"tui",
|
||||
"ui",
|
||||
"widget"
|
||||
],
|
||||
"entities_match": [
|
||||
"ui:",
|
||||
"region:",
|
||||
"layout:"
|
||||
]
|
||||
},
|
||||
"obligations": [
|
||||
{
|
||||
"id": "ui-layout-contract-inherited",
|
||||
"obligation": "affected UI components inherit their Layout Contract obligations; layout_contract_check must PASS",
|
||||
"when": {
|
||||
"entities_match": [
|
||||
"ui:",
|
||||
"region:",
|
||||
"layout:"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui-user-locked-preserved",
|
||||
"obligation": "USER-LOCKED placement is user authority: preserved exactly; changes require explicit user authorization",
|
||||
"when": {
|
||||
"entities_match": [
|
||||
"ui:",
|
||||
"region:"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui-unknown-never-pass",
|
||||
"obligation": "layout verification UNKNOWN (no adapter evidence) must never be reported as PASS",
|
||||
"when": {
|
||||
"entities_match": [
|
||||
"ui:",
|
||||
"region:",
|
||||
"layout:"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui-component-identity-stable",
|
||||
"obligation": "ui:<component> identity survives implementation-path changes; authority never keyed by filename alone",
|
||||
"when": {
|
||||
"entities_match": [
|
||||
"ui:",
|
||||
"region:",
|
||||
"layout:"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
"Tranche 3 real TUI proof (gate PASS -> violation FAIL -> restore)",
|
||||
"issue #105 + plans/2026-09-03-tui-footer.md (version display)",
|
||||
"maintenance:version-display-user-lock (USER-LOCK upgrade pending user confirmation \u2014 provenance rule honored, not fabricated)",
|
||||
"tests/test_layout_contract.py + tests/test_layout_contract_real_proof.py"
|
||||
],
|
||||
"exclusions": [],
|
||||
"toolchain_notes": [],
|
||||
"runtime_effects": []
|
||||
},
|
||||
{
|
||||
"pattern_id": "store-schema-evolution",
|
||||
"purpose": "Durable store/schema changes: backward readability, authority preservation, deterministic roundtrip, migration only when necessary.",
|
||||
"revision": 1,
|
||||
"maturity": "CANDIDATE",
|
||||
"signals": {
|
||||
"path_contains": [
|
||||
"store",
|
||||
"schema",
|
||||
"structural_store",
|
||||
"scaffold",
|
||||
"spine"
|
||||
],
|
||||
"entities_match": [
|
||||
"module:structural_store",
|
||||
"module:scaffold",
|
||||
"module:spine"
|
||||
],
|
||||
"objective_any": [
|
||||
"store",
|
||||
"schema",
|
||||
"record",
|
||||
"persist",
|
||||
"field",
|
||||
"migration"
|
||||
]
|
||||
},
|
||||
"obligations": [
|
||||
{
|
||||
"id": "store-backward-readability",
|
||||
"obligation": "old records without new fields read as empty/unknown, never crash; legacy keys preserved",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"store",
|
||||
"structural_store",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"schema"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "store-roundtrip",
|
||||
"obligation": "deterministic save/load roundtrip incl. unknown entity/relation kinds and open-world fields",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"store",
|
||||
"structural_store",
|
||||
"scaffold",
|
||||
"schema"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "store-authority-preservation",
|
||||
"obligation": "structural facts only change through their owning projector/authority; canonical rebuild after registration (no hand-edited structural facts)",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"spine",
|
||||
"scaffold",
|
||||
"store/"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "store-migration-only-if-needed",
|
||||
"obligation": "no mandatory migration unless semantics truly change; ext/open-world fields preferred over schema churn",
|
||||
"when": {
|
||||
"path_contains": [
|
||||
"schema",
|
||||
"store",
|
||||
"structural_store"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
"Tranche 2 CI lesson: hand-edited spine store diverged from canonical rebuild (deploy-identity edges)",
|
||||
"Tranche 2 structural-store extraction (behavior-neutrality proof)",
|
||||
"Tranche 4 contract-quality repair (legacy keys preserved, old contracts readable \u2014 test_h)",
|
||||
"Tranche 5 expected-runtime-effects extension (old contracts close without new fields)"
|
||||
],
|
||||
"exclusions": [],
|
||||
"toolchain_notes": [],
|
||||
"runtime_effects": []
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,126 @@
|
||||
//! branch_battery — replays frozen_cpb_truth.json VERBATIM (17 cases
|
||||
//! frozen from the live Python oracle construction_patterns_builtin.py,
|
||||
//! RUNE_CPB_CORE=python pinned, inputs deep-copied before capture).
|
||||
//! Order-insensitive object comparison; numbers compared exactly (both
|
||||
//! sides IEEE-754 f64); list order significant (registry + provenance
|
||||
//! order is oracle-derived).
|
||||
|
||||
use capability_scoring::pyjson::{parse, Json};
|
||||
|
||||
use crate::registry;
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_cpb_truth.json");
|
||||
|
||||
fn jeq(a: &Json, b: &Json) -> bool {
|
||||
match (a, b) {
|
||||
(Json::Num(x), Json::Num(y)) => x == y,
|
||||
(Json::Str(x), Json::Str(y)) => x == y,
|
||||
(Json::Bool(x), Json::Bool(y)) => x == y,
|
||||
(Json::Null, Json::Null) => true,
|
||||
(Json::Arr(x), Json::Arr(y)) => {
|
||||
x.len() == y.len() && x.iter().zip(y).all(|(i, j)| jeq(i, j))
|
||||
}
|
||||
(Json::Obj(x), Json::Obj(y)) => {
|
||||
x.len() == y.len()
|
||||
&& x.iter()
|
||||
.all(|(k, v)| y.iter().any(|(k2, v2)| k == k2 && jeq(v, v2)))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(v: Option<&Json>) -> String {
|
||||
match v {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// One case's oracle verdict, replayed against the Rust kernel.
|
||||
fn run_case(fn_name: &str, args: &Json) -> Result<Json, String> {
|
||||
match fn_name {
|
||||
"registry_revision" => Ok(Json::Num(registry::registry_revision() as f64)),
|
||||
"patterns" => Ok(Json::Arr(registry::patterns())),
|
||||
"get_pattern" => {
|
||||
let pattern_id = as_str(args.get("pattern_id"));
|
||||
let revision = match args.get("revision") {
|
||||
Some(Json::Num(n)) => Some(*n as i64),
|
||||
_ => None,
|
||||
};
|
||||
registry::get_pattern(&pattern_id, revision)
|
||||
}
|
||||
other => Err(format!("unknown truth fn {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_case(fn_name: &str, name: &str, args: &Json, want: &Json) -> Result<(), String> {
|
||||
// The oracle's raising branch is recorded as {"raises","message"}.
|
||||
if let Some(message) = want.get("message").filter(|_| want.get("raises").is_some()) {
|
||||
match run_case(fn_name, args) {
|
||||
Err(err) => {
|
||||
let want_msg = as_str(Some(message));
|
||||
if err != want_msg {
|
||||
return Err(format!(
|
||||
"{fn_name}/{name}: err mismatch\n want {want_msg}\n got {err}"
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok(got) => {
|
||||
return Err(format!(
|
||||
"{fn_name}/{name}: oracle raises, kernel returned {}",
|
||||
capability_scoring::pyjson::dumps(&got)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
match run_case(fn_name, args) {
|
||||
Ok(got) => {
|
||||
if jeq(&got, want) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"{fn_name}/{name}: value mismatch\n want {}\n got {}",
|
||||
capability_scoring::pyjson::dumps(want),
|
||||
capability_scoring::pyjson::dumps(&got)
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{fn_name}/{name}: kernel error {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let truth = parse(TRUTH).expect("frozen_cpb_truth.json parses");
|
||||
let cases = match truth.get("cases") {
|
||||
Some(Json::Arr(c)) => c.clone(),
|
||||
_ => panic!("truth has no cases"),
|
||||
};
|
||||
assert!(!cases.is_empty(), "frozen truth is empty");
|
||||
let mut failures = Vec::new();
|
||||
for case in &cases {
|
||||
let fn_name = as_str(case.get("fn"));
|
||||
let name = as_str(case.get("name"));
|
||||
let args = case.get("input").cloned().unwrap_or(Json::Null);
|
||||
let want = case.get("want").cloned().unwrap_or(Json::Null);
|
||||
if let Err(err) = check_case(&fn_name, &name, &args, &want) {
|
||||
failures.push(err);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"branch battery diverged from frozen truth:\n{}",
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truth_case_count_is_stable() {
|
||||
let truth = parse(TRUTH).expect("frozen_cpb_truth.json parses");
|
||||
let cases = match truth.get("cases") {
|
||||
Some(Json::Arr(c)) => c.len(),
|
||||
_ => 0,
|
||||
};
|
||||
assert_eq!(cases, 19, "frozen case count drifted — regenerate truth");
|
||||
}
|
||||
@@ -1,19 +1,96 @@
|
||||
//! construction_patterns_builtin — built-in construction patterns (W6).
|
||||
pub fn builtin_patterns() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"patterns": [
|
||||
{"pattern_id": "web-api", "description": "HTTP API with REST endpoints"},
|
||||
{"pattern_id": "cli-tool", "description": "Command-line tool with argument parsing"},
|
||||
]
|
||||
})
|
||||
}
|
||||
//! construction_patterns_builtin — the built-in pattern registry kernel
|
||||
//! (dichotomy wave #2479 tranche). Serves the oracle's pattern data
|
||||
//! (frozen verbatim from the live Python registry — provenance order is
|
||||
//! the oracle's __init__-sorted order) through a doc-envelope FFI. The
|
||||
//! Python facade keeps a RUNE_CPB_CORE=python pin.
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok","value"|"err"}):
|
||||
//! * cpb_registry_revision({}) -> number
|
||||
//! * cpb_patterns({}) -> [pattern doc, ...] registry order
|
||||
//! * cpb_get_pattern({"pattern_id", "revision"}) -> pattern doc | null
|
||||
//! (a revision that no longer matches is an err carrying the oracle's
|
||||
//! exact ValueError message)
|
||||
//!
|
||||
//! Parity oracle: frozen_cpb_truth.json (17 cases frozen from the live
|
||||
//! Python oracle) replayed verbatim by the in-crate branch battery.
|
||||
|
||||
pub mod registry;
|
||||
|
||||
pub use capability_scoring::pyjson;
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
mod branch_battery;
|
||||
|
||||
#[test]
|
||||
fn builtin_patterns_exist() {
|
||||
assert!(true);
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use pyjson::Json;
|
||||
|
||||
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(result: Result<Json, String>) -> *mut c_char {
|
||||
let body = match result {
|
||||
Ok(value) => Json::Obj(vec![
|
||||
("ok".to_string(), Json::Bool(true)),
|
||||
("value".to_string(), value),
|
||||
]),
|
||||
Err(err) => Json::Obj(vec![
|
||||
("ok".to_string(), Json::Bool(false)),
|
||||
("err".to_string(), Json::Str(err)),
|
||||
]),
|
||||
};
|
||||
let out = pyjson::dumps(&body);
|
||||
CString::new(out)
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn arg_json(text: &str) -> Result<Json, String> {
|
||||
pyjson::parse(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
/// cpb_registry_revision({}) -> number
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cpb_registry_revision(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.map(|_| Json::Num(registry::registry_revision() as f64));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// cpb_patterns({}) -> [pattern doc, ...] in registry order
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cpb_patterns(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.map(|_| Json::Arr(registry::patterns()));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// cpb_get_pattern({"pattern_id": "...", "revision": n|null}) -> doc | null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cpb_get_pattern(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| {
|
||||
let pattern_id = match a.get("pattern_id") {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => return Err("arg.pattern_id required".to_string()),
|
||||
};
|
||||
let revision = match a.get("revision") {
|
||||
Some(Json::Num(n)) => Some(*n as i64),
|
||||
_ => None,
|
||||
};
|
||||
registry::get_pattern(&pattern_id, revision)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! registry — the built-in construction pattern registry. The pattern docs
|
||||
//! are FROZEN from the live Python oracle (frozen_cpb_patterns.json,
|
||||
//! generated by scripts/gen_construction_patterns_builtin_truth.py with
|
||||
//! RUNE_CPB_CORE=python pinned — never hand-transcribed): each doc is the
|
||||
//! oracle's ConstructionPattern.to_dict(), including its __init__-sorted
|
||||
//! provenance/exclusions order.
|
||||
//!
|
||||
//! Phase B: the frozen docs parse once into typed [`BuiltinPattern`]
|
||||
//! values; the FFI re-emits the oracle-shaped doc from the typed value.
|
||||
//! Miss-before-revision-check oracle ordering is explicit in the typed
|
||||
//! lookup.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use capability_scoring::pyjson::{self, Json};
|
||||
|
||||
const PATTERNS_SRC: &str = include_str!("../frozen_cpb_patterns.json");
|
||||
|
||||
/// One builtin pattern, typed from the oracle's to_dict() doc. Free-form
|
||||
/// sub-structures (signals, obligations) stay `Json`: the schema is the
|
||||
/// oracle's, open-world by design.
|
||||
#[derive(Clone, Debug)]
|
||||
struct BuiltinPattern {
|
||||
pattern_id: String,
|
||||
revision: i64,
|
||||
doc: Json,
|
||||
}
|
||||
|
||||
impl BuiltinPattern {
|
||||
fn from_doc(doc: Json) -> Option<Self> {
|
||||
let pattern_id = match doc.get("pattern_id") {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => return None,
|
||||
};
|
||||
let revision = match doc.get("revision") {
|
||||
Some(Json::Num(n)) => *n as i64,
|
||||
_ => return None,
|
||||
};
|
||||
Some(Self { pattern_id, revision, doc })
|
||||
}
|
||||
}
|
||||
|
||||
fn registry() -> &'static Vec<BuiltinPattern> {
|
||||
static REGISTRY: OnceLock<Vec<BuiltinPattern>> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| {
|
||||
pyjson::parse(PATTERNS_SRC)
|
||||
.ok()
|
||||
.and_then(|root| match root.get("patterns") {
|
||||
Some(Json::Arr(items)) => Some(
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|d| BuiltinPattern::from_doc(d.clone()))
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
/// PATTERN_REGISTRY_REVISION from the oracle.
|
||||
pub fn registry_revision() -> i64 {
|
||||
static REVISION: OnceLock<i64> = OnceLock::new();
|
||||
*REVISION.get_or_init(|| {
|
||||
pyjson::parse(PATTERNS_SRC)
|
||||
.ok()
|
||||
.and_then(|root| match root.get("revision") {
|
||||
Some(Json::Num(n)) => Some(*n as i64),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
/// Every builtin pattern doc, in registry order (BUILTIN_PATTERNS order).
|
||||
pub fn patterns() -> Vec<Json> {
|
||||
registry().iter().map(|p| p.doc.clone()).collect()
|
||||
}
|
||||
|
||||
/// Oracle get_pattern semantics: unknown/empty id -> Null BEFORE any
|
||||
/// revision check; a revision that no longer matches raises (err carries
|
||||
/// the oracle's exact ValueError message).
|
||||
pub fn get_pattern(pattern_id: &str, revision: Option<i64>) -> Result<Json, String> {
|
||||
let pattern = match registry().iter().find(|p| p.pattern_id == pattern_id) {
|
||||
Some(p) => p,
|
||||
None => return Ok(Json::Null),
|
||||
};
|
||||
if let Some(rev) = revision {
|
||||
if pattern.revision != rev {
|
||||
return Err(format!(
|
||||
"pattern {pattern_id} revision {rev} not in registry \
|
||||
(current: {}) \u{2014} the consuming contract records which \
|
||||
revision it used",
|
||||
pattern.revision
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(pattern.doc.clone())
|
||||
}
|
||||
Generated
+23
@@ -54,6 +54,12 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -64,6 +70,22 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -135,6 +157,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
|
||||
@@ -8,4 +8,4 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["preserve_order", "float_roundtrip"] }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -14,7 +14,49 @@ 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"];
|
||||
|
||||
/// The five guarded blocks — a closed lattice, not stringly-typed routing.
|
||||
/// The constitution's limit is exhaustive-match derived; every other block
|
||||
/// falls to the plain drift limit (the oracle's ternary).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum BlockLabel {
|
||||
Constitution,
|
||||
Persona,
|
||||
Human,
|
||||
Operating,
|
||||
Project,
|
||||
}
|
||||
|
||||
impl BlockLabel {
|
||||
pub const ALL: [BlockLabel; 5] = [
|
||||
Self::Constitution,
|
||||
Self::Persona,
|
||||
Self::Human,
|
||||
Self::Operating,
|
||||
Self::Project,
|
||||
];
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Constitution => "constitution",
|
||||
Self::Persona => "persona",
|
||||
Self::Human => "human",
|
||||
Self::Operating => "operating",
|
||||
Self::Project => "project",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn of_name(name: &str) -> Option<Self> {
|
||||
Self::ALL.iter().copied().find(|b| b.as_str() == name)
|
||||
}
|
||||
|
||||
pub fn limit(self) -> f64 {
|
||||
match self {
|
||||
Self::Constitution => CONSTITUTION_LIMIT,
|
||||
_ => DRIFT_LIMIT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// _sha port: hex sha256 of UTF-8 text.
|
||||
pub fn sha(text: &str) -> String {
|
||||
@@ -28,28 +70,28 @@ pub fn diff_blocks(reference: &str, current: &str) -> String {
|
||||
if reference.is_empty() {
|
||||
return "no prior content (first drift from empty baseline)".to_string();
|
||||
}
|
||||
fn sig(l: &str) -> Option<&str> {
|
||||
let s = l.trim();
|
||||
(!s.is_empty()).then_some(s)
|
||||
}
|
||||
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();
|
||||
ref_lines.iter().filter_map(|l| sig(l)).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; }
|
||||
}
|
||||
}
|
||||
cur_lines.iter().filter_map(|l| sig(l)).collect();
|
||||
let added: Vec<&str> = cur_lines
|
||||
.iter()
|
||||
.filter_map(|l| sig(l))
|
||||
.filter(|s| !ref_set.contains(*s))
|
||||
.take(6)
|
||||
.collect();
|
||||
let removed: Vec<&str> = ref_lines
|
||||
.iter()
|
||||
.filter_map(|l| sig(l))
|
||||
.filter(|s| !cur_set.contains(*s))
|
||||
.take(3)
|
||||
.collect();
|
||||
let mut parts: Vec<String> = vec![];
|
||||
if !added.is_empty() {
|
||||
parts.push(format!(
|
||||
@@ -85,8 +127,9 @@ pub fn check_block(
|
||||
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"
|
||||
let is_constitution = matches!(BlockLabel::of_name(label), Some(BlockLabel::Constitution));
|
||||
let limit = if is_constitution { CONSTITUTION_LIMIT } else { DRIFT_LIMIT };
|
||||
let content_changed = is_constitution
|
||||
&& anchor_size.is_some()
|
||||
&& base_sha.is_some()
|
||||
&& cur_sha.is_some()
|
||||
@@ -100,7 +143,8 @@ pub fn check_block(
|
||||
|
||||
/// 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" {
|
||||
let is_constitution = matches!(BlockLabel::of_name(label), Some(BlockLabel::Constitution));
|
||||
if is_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"})
|
||||
@@ -111,6 +155,143 @@ pub fn auto_fix_decision(label: &str, anchored: bool, has_last_good: bool) -> Va
|
||||
}
|
||||
}
|
||||
|
||||
/// Python round(x, 3): correct rounding of the binary value with ties-to-even
|
||||
/// (the same contract Rust's {:.3} formatting implements).
|
||||
fn py_round3(x: f64) -> f64 {
|
||||
format!("{:.3}", x).parse().unwrap_or(x)
|
||||
}
|
||||
|
||||
/// ledger_path port: the ledger lives beside the index file, so overriding
|
||||
/// INDEX_FILE moves the ledger too.
|
||||
pub fn ledger_path_for(index_file: &str) -> String {
|
||||
match index_file.rfind('/') {
|
||||
Some(i) => format!("{}drift_ledger.json", &index_file[..=i]),
|
||||
None => "drift_ledger.json".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// snapshot() entry-assembly port: {"when", "blocks": {label: {sha, size,
|
||||
/// content}}}. "size" is the oracle's len(raw) — a CHARACTER count, not bytes.
|
||||
/// Unknown labels in the payload are ignored (the oracle iterates BLOCKS).
|
||||
pub fn snapshot_entry(blocks: &Value, when: &str) -> Value {
|
||||
let mut b = serde_json::Map::new();
|
||||
for label in BlockLabel::ALL {
|
||||
let raw = blocks.get(label.as_str()).and_then(|v| v.as_str()).unwrap_or("");
|
||||
b.insert(
|
||||
label.as_str().to_string(),
|
||||
json!({
|
||||
"sha": sha(raw),
|
||||
"size": raw.chars().count(),
|
||||
"content": raw,
|
||||
}),
|
||||
);
|
||||
}
|
||||
json!({"when": when, "blocks": Value::Object(b)})
|
||||
}
|
||||
|
||||
/// anchor() record-assembly port: the authorised-state backup for re-anchoring.
|
||||
pub fn anchor_record(content: &str, reason: &str, when: &str) -> Value {
|
||||
json!({
|
||||
"sha": sha(content),
|
||||
"size": content.chars().count(),
|
||||
"content": content,
|
||||
"reason": reason,
|
||||
"when": when,
|
||||
})
|
||||
}
|
||||
|
||||
/// check() alert-loop port: given the ledger doc and the current block
|
||||
/// contents {label: text}, produce the alert list exactly as the oracle's
|
||||
/// loop does — anchored blocks are measured against their authorised state,
|
||||
/// unanchored ones against the last snapshot (max(prev_size, 1) floor),
|
||||
/// the constitution additionally trips on ANY content change from its
|
||||
/// anchored sha, pct/limit round to 3 decimals, and each alert carries the
|
||||
/// root-cause diff. Side effects (prints, surface_alert, auto-fix, journal)
|
||||
/// stay in the facade.
|
||||
pub fn check_alerts(ledger: &Value, blocks: &Value) -> Value {
|
||||
let prev = ledger
|
||||
.get("last_snapshot")
|
||||
.and_then(|s| s.get("blocks"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let anchors = ledger.get("anchors").cloned().unwrap_or_else(|| json!({}));
|
||||
let alerts: Vec<Value> = BlockLabel::ALL
|
||||
.iter()
|
||||
.filter_map(|label| {
|
||||
let cur = blocks.get(label.as_str()).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let cur_size = cur.chars().count() as i64;
|
||||
let anchor_meta = anchors.get(label.as_str());
|
||||
let (base, base_sha, delta, ref_content) = match anchor_meta {
|
||||
Some(a) => {
|
||||
let b = a.get("size").and_then(|v| v.as_i64()).unwrap_or(cur_size);
|
||||
let sh = a.get("sha").and_then(|v| v.as_str());
|
||||
let rc = a.get("content").and_then(|v| v.as_str());
|
||||
(b, sh, cur_size - b, rc)
|
||||
}
|
||||
None => {
|
||||
let pm = prev.get(label.as_str());
|
||||
let ps = pm
|
||||
.and_then(|m| m.get("size"))
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(cur_size);
|
||||
let rc = pm.and_then(|m| m.get("content")).and_then(|v| v.as_str());
|
||||
(ps.max(1), None, cur_size - ps, rc)
|
||||
}
|
||||
};
|
||||
let pct = (delta.abs() as f64) / (base.max(1) as f64);
|
||||
let limit = label.limit();
|
||||
let content_changed = matches!(label, BlockLabel::Constitution)
|
||||
&& anchor_meta.is_some()
|
||||
&& base_sha.is_some()
|
||||
&& sha(cur) != base_sha.unwrap();
|
||||
(content_changed || pct > limit).then(|| {
|
||||
json!({
|
||||
"label": label.as_str(),
|
||||
"delta": delta,
|
||||
"pct": py_round3(pct),
|
||||
"limit": py_round3(limit),
|
||||
"base": base,
|
||||
"changed": diff_blocks(ref_content.unwrap_or(""), cur),
|
||||
"content_changed": content_changed,
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Value::Array(alerts)
|
||||
}
|
||||
|
||||
/// fix() decision port: the oracle's retroactive-fix gate minus the restore IO
|
||||
/// (the facade performs restore_topic + journal when {fixed: true}).
|
||||
/// cur_sha is the sha of the block's CURRENT store content (read_block is a
|
||||
/// facade seam; the sha travels in the doc envelope).
|
||||
pub fn fix_decision(ledger: &Value, label: &str, cur_sha: Option<&str>) -> Value {
|
||||
let block_meta = ledger
|
||||
.get("last_snapshot")
|
||||
.and_then(|s| s.get("blocks"))
|
||||
.and_then(|b| b.get(label));
|
||||
let has_content = block_meta
|
||||
.map(|m| m.get("content").is_some())
|
||||
.unwrap_or(false);
|
||||
if block_meta.is_none() || !has_content {
|
||||
return json!({"fixed": false, "reason": format!("no last-good content for {}", label)});
|
||||
}
|
||||
if ledger
|
||||
.get("anchors")
|
||||
.map(|a| a.get(label).is_some())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return json!({"fixed": false, "reason": format!("{} is anchored (authorised); use anchor to re-baseline instead of fix", label)});
|
||||
}
|
||||
let good_sha = block_meta
|
||||
.and_then(|m| m.get("sha"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if cur_sha == Some(good_sha) {
|
||||
return json!({"fixed": false, "reason": format!("{} already matches last-good", label)});
|
||||
}
|
||||
json!({"fixed": true, "reason": format!("{} restored to last-good state", label)})
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
@@ -181,5 +362,54 @@ pub extern "C" fn dl_auto_fix(arg: *const c_char) -> *mut c_char {
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_ledger_path({"index_file"}) -> path string
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_ledger_path(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| Value::String(ledger_path_for(s(a.get("index_file")))));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_snapshot_entry({"blocks": {label: content}, "when"}) -> snapshot entry
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_snapshot_entry(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let blocks = a.get("blocks").cloned().unwrap_or(Value::Null);
|
||||
snapshot_entry(&blocks, s(a.get("when")))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_anchor_record({"content", "reason", "when"}) -> anchor record
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_anchor_record(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
anchor_record(s(a.get("content")), s(a.get("reason")), s(a.get("when")))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_check_alerts({"ledger", "blocks": {label: content}}) -> [alert, ...]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_check_alerts(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let ledger = a.get("ledger").cloned().unwrap_or(Value::Null);
|
||||
let blocks = a.get("blocks").cloned().unwrap_or(Value::Null);
|
||||
check_alerts(&ledger, &blocks)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_fix_decision({"ledger", "label", "cur_sha"?}) -> {fixed, reason}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_fix_decision(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let ledger = a.get("ledger").cloned().unwrap_or(Value::Null);
|
||||
let label = s(a.get("label")).to_string();
|
||||
let cur_sha = a.get("cur_sha").and_then(|v| v.as_str());
|
||||
fix_decision(&ledger, &label, cur_sha)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export here (capability_scoring's cs_free_string
|
||||
// serves the whole process — duplicate symbols break the cdylib link).
|
||||
Generated
+113
@@ -0,0 +1,113 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "experience_encoding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lexicon_evolution",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "lexicon_evolution"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "experience_encoding"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "canonical experience encoding and episodic admission (dichotomy wave #2479) — FULL Rust port of src/rune/experience_encoding.py; zero-dep beyond serde_json + the lexicon_evolution sibling"
|
||||
|
||||
[lib]
|
||||
name = "experience_encoding"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
lexicon_evolution = { path = "../lexicon_evolution" }
|
||||
@@ -0,0 +1,49 @@
|
||||
//! experience_encoding branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_ee_truth.json: 29 cases across the module's single public
|
||||
//! function and every branch — the empty/short gate with its char-count
|
||||
//! boundary (14 vs 15), null/missing/whitespace text coercion, provenance
|
||||
//! dict.get quirk (present-but-null source/session_id pins as null; the
|
||||
//! default applies only to a MISSING key), the whitespace/case-insensitive
|
||||
//! duplicate scan and its punctuation-survives non-duplicate, the lexicon
|
||||
//! seam (ImportError fallback → conservative evidence, ordering vs the
|
||||
//! empty gate and the duplicate check), framing-only trivia, the
|
||||
//! grounded → EPISODE/EVIDENCE split with single/multi/top3 sorted share
|
||||
//! reasons, novel-axis grounding, corpus fallback to episode texts and the
|
||||
//! corpus override that kills the share but not the grounding).
|
||||
//! Frozen via scripts/gen_experience_encoding_truth.py with
|
||||
//! RUNE_EE_CORE=python pinned (poisoned-truth law). Embedded VERBATIM —
|
||||
//! never transcribed. Each case replays the oracle's full return shape
|
||||
//! (verdict + reason + provenance) and compares whole-doc.
|
||||
|
||||
use crate::evaluate;
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_ee_truth.json");
|
||||
|
||||
#[test]
|
||||
fn evaluate_parity_battery() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for case in truth["cases"]["evaluate"].as_array().unwrap() {
|
||||
let input = &case["input"];
|
||||
let event = input["event"].clone();
|
||||
let episodes: Vec<Value> = input["existing_episodes"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let corpus: Vec<Value> = input["corpus_texts"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let lexicon = input
|
||||
.get("lexicon_available")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
let got = evaluate(&event, &episodes, &corpus, lexicon);
|
||||
assert_eq!(
|
||||
&got,
|
||||
&case["expected"],
|
||||
"case '{}' diverged",
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,895 @@
|
||||
{
|
||||
"frozen": "2026-10-12T10:38:28.375209+00:00",
|
||||
"oracle": "src/rune/experience_encoding.py",
|
||||
"pin": "RUNE_EE_CORE=python",
|
||||
"note": "Frozen via scripts/gen_experience_encoding_truth.py against the live Python oracle. Battery embeds this VERBATIM. The `lexicon_available` flag pins the oracle's flat-import environment seam (ImportError fallback branch).",
|
||||
"cases": {
|
||||
"evaluate": [
|
||||
{
|
||||
"name": "ev-missing-text-key-empty",
|
||||
"input": {
|
||||
"event": {
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-empty-text",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-null-text-coerced",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": null,
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-whitespace-only-text",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": " \n\t ",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-short-14-chars",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "xxxxxxxxxxxxxx",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-boundary-15-chars-evidence",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "xxxxxxxxxxxxxxx",
|
||||
"source": "curator",
|
||||
"session_id": "s1"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "curator",
|
||||
"session": "s1"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "curator",
|
||||
"session": "s1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-null-source-session-provenance",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "hi",
|
||||
"source": null,
|
||||
"session_id": null
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": null,
|
||||
"session": null
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": null,
|
||||
"session": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-empty-string-source-session",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "xxxxxxxxxxxxxxxxxxxx",
|
||||
"source": "",
|
||||
"session_id": ""
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-extra-keys-ignored",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "xxxxxxxxxxxxxxxx",
|
||||
"kind": "note",
|
||||
"weight": 3
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "unknown",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "unknown",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-duplicate-normalized-whitespace-case",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "Nick prefers DARK coffee",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "nick prefers DARK coffee"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-duplicate-exact-string",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she drinks oatmilk daily",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "she drinks oatmilk daily"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-duplicate-episode-uppercase",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "nick prefers dark coffee",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "NICK PREFERS DARK COFFEE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-duplicate-episode-padded",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "nick prefers dark coffee",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": " nick prefers dark coffee "
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "duplicate of an existing episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-punctuation-survives-not-duplicate",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "NICK PREFERS dark coffee!",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "nick prefers dark coffee"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['coffee', 'dark', 'nick']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['coffee', 'dark', 'nick']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-null-episode-texts-ungrounded",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "quantum flux patterns emerge here",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": null
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-lexicon-blocked-conservative-evidence",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she drinks oatmilk every single morning",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "lexicon unavailable \u2014 conservative evidence admission",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "lexicon unavailable \u2014 conservative evidence admission",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-lexicon-blocked-short-still-ignored",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "hi",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "empty or trivial (<15 chars)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-lexicon-blocked-duplicate-path-conservative",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "nick prefers dark coffee",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": false,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "nick prefers dark coffee"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "lexicon unavailable \u2014 conservative evidence admission",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "lexicon unavailable \u2014 conservative evidence admission",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-framing-only-no-lexical-signal",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "work thing way always",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "no lexical signal (framing-only)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "IGNORE",
|
||||
"reason": "no lexical signal (framing-only)",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-shared-single-term",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she never jokes about oatmilk at all",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "i love oatmilk a lot"
|
||||
}
|
||||
],
|
||||
"corpus_texts": [
|
||||
"oatmilk is life",
|
||||
"tea later day"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['oatmilk']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['oatmilk']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-shared-multiple-sorted",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "oatmilk and celesta every morning",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "oatmilk and celesta"
|
||||
}
|
||||
],
|
||||
"corpus_texts": [
|
||||
"oatmilk bar",
|
||||
"celesta hall"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['celesta', 'oatmilk']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['celesta', 'oatmilk']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-shared-sorted-top3",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "oatmilk celesta zephyr quartz ritual",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "oatmilk celesta zephyr quartz"
|
||||
}
|
||||
],
|
||||
"corpus_texts": [
|
||||
"oatmilk celesta zephyr quartz notes"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['celesta', 'oatmilk', 'quartz']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['celesta', 'oatmilk', 'quartz']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-novel-axis-grounded",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "celesta practice daily ritual",
|
||||
"source": "session"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "i love oatmilk"
|
||||
}
|
||||
],
|
||||
"corpus_texts": [
|
||||
"she plays the celesta nightly"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "novel axis grounded in lived material",
|
||||
"provenance": {
|
||||
"source": "session",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "novel axis grounded in lived material",
|
||||
"provenance": {
|
||||
"source": "session",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-evidence-fact-shaped-ungrounded",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "quantum flux patterns emerge",
|
||||
"source": "curator"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "i love oatmilk"
|
||||
}
|
||||
],
|
||||
"corpus_texts": [
|
||||
"oatmilk is life"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "curator",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "curator",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-evidence-no-episodes-no-corpus",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "quantum flux patterns emerge",
|
||||
"source": "curator"
|
||||
},
|
||||
"lexicon_available": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "curator",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EVIDENCE",
|
||||
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
|
||||
"provenance": {
|
||||
"source": "curator",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-stem-grounded-default-corpus",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she migrated north yesterday",
|
||||
"source": "session"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "the migration of songbirds"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['migrated']",
|
||||
"provenance": {
|
||||
"source": "session",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['migrated']",
|
||||
"provenance": {
|
||||
"source": "session",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-corpus-override-share-killed-novel-grounded",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she never jokes about oatmilk ok",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "i love oatmilk"
|
||||
}
|
||||
],
|
||||
"corpus_texts": [
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk",
|
||||
"oatmilk"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "novel axis grounded in lived material",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "novel axis grounded in lived material",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-no-episodes-corpus-grounded",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she drinks oatmilk daily",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"corpus_texts": [
|
||||
"oatmilk is life"
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "novel axis grounded in lived material",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "novel axis grounded in lived material",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ev-episode-default-corpus-is-episodes",
|
||||
"input": {
|
||||
"event": {
|
||||
"text": "she drinks oatmilk daily",
|
||||
"source": "user"
|
||||
},
|
||||
"lexicon_available": true,
|
||||
"existing_episodes": [
|
||||
{
|
||||
"text": "oatmilk rituals fill the morning"
|
||||
}
|
||||
]
|
||||
},
|
||||
"return": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['oatmilk']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "EPISODE",
|
||||
"reason": "shares distinctive term(s): ['oatmilk']",
|
||||
"provenance": {
|
||||
"source": "user",
|
||||
"session": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
//! experience_encoding — #153: canonical experience encoding and episodic
|
||||
//! admission (the write-side authority).
|
||||
//!
|
||||
//! FULL name-preserving port of src/rune/experience_encoding.py (dichotomy
|
||||
//! wave, #2479 tranche). ONE rule for what becomes persistent lived
|
||||
//! experience. Not every event becomes an episode. Event → evaluate →
|
||||
//! classify → attach provenance:
|
||||
//!
|
||||
//! ```text
|
||||
//! IGNORE — noise (empty, duplicate of an existing episode, trivia
|
||||
//! with no distinctive signal)
|
||||
//! EVIDENCE — a fact worth the store (grounded, attributed, distinctive)
|
||||
//! EPISODE — a lived experience (a behaviour refinement, an identity
|
||||
//! value, a correction — something that changes future action)
|
||||
//! ```
|
||||
//!
|
||||
//! Admission uses the lexicon discipline (distinctive terms, not common
|
||||
//! words) + grounding (traces to real material) — the same meta-judgement
|
||||
//! as retrieval and evolution, so growth and admission speak one lexicon.
|
||||
//! The lexical decisions cross to the lexicon_evolution sibling crate
|
||||
//! IN-PROCESS (the exact mirror of the oracle's `import lexicon_evolution`
|
||||
//! call — library dependency, no FFI hop).
|
||||
//!
|
||||
//! FFI (doc-envelope law): one symbol, takes one JSON doc and returns
|
||||
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
|
||||
//! * ee_evaluate({event, existing_episodes, corpus_texts,
|
||||
//! lexicon_available}) → {"verdict", "reason", "provenance"}
|
||||
//!
|
||||
//! Lexicon-seam law: the oracle's flat `import lexicon_evolution` is an
|
||||
//! ENVIRONMENT seam — when the module is missing, every event that passes
|
||||
//! the empty/short gate is admitted conservatively as evidence. The
|
||||
//! payload's `lexicon_available` flag pins that seam so the frozen truth
|
||||
//! (and this battery) can replay the ImportError branch; the empty gate
|
||||
//! runs BEFORE the seam (the oracle's short-circuit ordering).
|
||||
//!
|
||||
//! No ee_free_string export — sibling crates sharing the Python process
|
||||
//! already export identical free symbols; callers free through
|
||||
//! capability_scoring's cs_free_string.
|
||||
//!
|
||||
//! Python semantics preserved exactly:
|
||||
//! * (text or "") coercion — null/missing text is the empty string,
|
||||
//! then .strip(); the gate is len(text) < 15 over CHARACTERS
|
||||
//! * provenance source/session: dict.get defaults apply only when the
|
||||
//! KEY is missing — present-but-null pins as null (quirk-for-quirk)
|
||||
//! * duplicate test = " ".join(text.lower().split()) equality (the
|
||||
//! episode side coerces missing/null text to "")
|
||||
//! * the duplicate and lexical-signal checks run INSIDE the lexicon
|
||||
//! availability seam; blocked lexicon beats a would-be duplicate
|
||||
//! * corpus = list(corpus_texts) or the episodes' texts (empty and
|
||||
//! absent corpus_texts both fall back)
|
||||
//! * EPISODE share reason is the oracle's f-string:
|
||||
//! sorted(shared)[:3] rendered as a Python list literal
|
||||
//!
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the verdict lattice
|
||||
//! is an `Admission` enum with an exhaustive `as_str` match, the reason
|
||||
//! strings are a `Reason` enum with Display (the EPISODE share reason
|
||||
//! materializes its sorted-top3 list lazily, one `Decision` enum driving
|
||||
//! both projections), the duplicate scan compares whitespace-token
|
||||
//! iterators over the lowered texts WITHOUT materializing either
|
||||
//! normalized join, the episode text extraction is a zero-copy
|
||||
//! filter_map over borrowed episode values, and the oracle's gate flow
|
||||
//! is a Result-free early-return pipeline whose every arm is matched
|
||||
//! exhaustively. Battery green throughout (unchanged from the parity
|
||||
//! commit — the whole-doc replay pins the refactor).
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use lexicon_evolution::{content_words, grounded, shared_distinctive};
|
||||
|
||||
// ── verdict lattice (enums, not string soup) ────────────────────────────────
|
||||
|
||||
/// The admission verdict — the oracle's three-way classification.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Admission {
|
||||
Ignore,
|
||||
Evidence,
|
||||
Episode,
|
||||
}
|
||||
|
||||
impl Admission {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Admission::Ignore => "IGNORE",
|
||||
Admission::Evidence => "EVIDENCE",
|
||||
Admission::Episode => "EPISODE",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The admission reason — the oracle's reason strings, byte-for-byte.
|
||||
enum Reason {
|
||||
/// "empty or trivial (<15 chars)"
|
||||
Trivial,
|
||||
/// "duplicate of an existing episode"
|
||||
Duplicate,
|
||||
/// "no lexical signal (framing-only)"
|
||||
NoLexicalSignal,
|
||||
/// "shares distinctive term(s): [...]" — sorted, top 3
|
||||
Shares(BTreeSet<String>),
|
||||
/// "novel axis grounded in lived material"
|
||||
NovelGrounded,
|
||||
/// "fact-shaped, ungrounded — stored as evidence, not episode"
|
||||
Ungrounded,
|
||||
/// "lexicon unavailable — conservative evidence admission"
|
||||
LexiconUnavailable,
|
||||
}
|
||||
|
||||
impl fmt::Display for Reason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Reason::Trivial => write!(f, "empty or trivial (<15 chars)"),
|
||||
Reason::Duplicate => write!(f, "duplicate of an existing episode"),
|
||||
Reason::NoLexicalSignal => write!(f, "no lexical signal (framing-only)"),
|
||||
Reason::Shares(shared) => {
|
||||
let top: Vec<String> = shared
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|s| format!("'{}'", s))
|
||||
.collect();
|
||||
write!(f, "shares distinctive term(s): [{}]", top.join(", "))
|
||||
}
|
||||
Reason::NovelGrounded => write!(f, "novel axis grounded in lived material"),
|
||||
Reason::Ungrounded => {
|
||||
write!(f, "fact-shaped, ungrounded — stored as evidence, not episode")
|
||||
}
|
||||
Reason::LexiconUnavailable => {
|
||||
write!(f, "lexicon unavailable — conservative evidence admission")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One admission decision: verdict + reason, matched exhaustively.
|
||||
enum Decision {
|
||||
Ignore(Reason),
|
||||
Evidence(Reason),
|
||||
Episode(Reason),
|
||||
}
|
||||
|
||||
impl Decision {
|
||||
fn verdict(&self) -> Admission {
|
||||
match self {
|
||||
Decision::Ignore(_) => Admission::Ignore,
|
||||
Decision::Evidence(_) => Admission::Evidence,
|
||||
Decision::Episode(_) => Admission::Episode,
|
||||
}
|
||||
}
|
||||
|
||||
fn reason(&self) -> &Reason {
|
||||
match self {
|
||||
Decision::Ignore(r) | Decision::Evidence(r) | Decision::Episode(r) => r,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── event extraction ─────────────────────────────────────────────────────────
|
||||
|
||||
/// (event.get("text") or "").strip() — null/missing text is the empty
|
||||
/// string; other JSON types are outside the oracle's domain (the oracle
|
||||
/// would crash on a truthy non-string) and coerce to empty here.
|
||||
fn event_text(event: &Value) -> &str {
|
||||
match event.get("text") {
|
||||
Some(Value::String(s)) => s.trim(),
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// dict.get(key, default) — the default applies ONLY when the key is
|
||||
/// missing; a present-but-null value pins as null (oracle quirk).
|
||||
fn event_field(event: &Value, key: &str, default: &str) -> Value {
|
||||
event
|
||||
.get(key)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(default.to_string()))
|
||||
}
|
||||
|
||||
/// The oracle's `e.get("text", "")` over the episode list: missing text is
|
||||
/// "", present-null stays null at the JSON layer (both are empty/filtered
|
||||
/// downstream — null → "" keeps the lexicon sibling's str domain).
|
||||
fn episode_texts(episodes: &[Value]) -> Vec<String> {
|
||||
episodes
|
||||
.iter()
|
||||
.filter_map(|ep| ep.get("text"))
|
||||
.map(|t| t.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The oracle's duplicate normalizer, comparison form: whitespace-token
|
||||
/// iterators over the lowered texts, equal WITHOUT materializing either
|
||||
/// " ".join(...) — zero normalized-join allocation.
|
||||
fn normalized_eq(a: &str, b: &str) -> bool {
|
||||
let low_a = a.to_lowercase();
|
||||
let low_b = b.to_lowercase();
|
||||
low_a.split_whitespace().eq(low_b.split_whitespace())
|
||||
}
|
||||
|
||||
// ── the decision pipeline ────────────────────────────────────────────────────
|
||||
|
||||
/// `evaluate` — one event in, one admission decision out. `episodes` and
|
||||
/// `corpus` are the JSON arrays (empty = the oracle's falsy fallback);
|
||||
/// `lexicon_available` pins the oracle's flat-import environment seam.
|
||||
fn decide(
|
||||
event: &Value,
|
||||
episodes: &[Value],
|
||||
corpus_texts: &[Value],
|
||||
lexicon_available: bool,
|
||||
) -> Decision {
|
||||
let text = event_text(event);
|
||||
|
||||
// 1. empty is noise (BEFORE the lexicon seam)
|
||||
if text.chars().count() < 15 {
|
||||
return Decision::Ignore(Reason::Trivial);
|
||||
}
|
||||
|
||||
// 2-4. everything else lives inside the lexicon seam
|
||||
if !lexicon_available {
|
||||
return Decision::Evidence(Reason::LexiconUnavailable);
|
||||
}
|
||||
|
||||
// 2. duplicate of an existing episode is noise (token-iterator compare)
|
||||
if episodes
|
||||
.iter()
|
||||
.any(|ep| {
|
||||
let ep_text = ep.get("text").and_then(Value::as_str).unwrap_or("");
|
||||
normalized_eq(text, ep_text)
|
||||
})
|
||||
{
|
||||
return Decision::Ignore(Reason::Duplicate);
|
||||
}
|
||||
|
||||
// 3. no lexical signal is noise (framing-only trivia)
|
||||
let terms = content_words(Some(text));
|
||||
if terms.is_empty() {
|
||||
return Decision::Ignore(Reason::NoLexicalSignal);
|
||||
}
|
||||
|
||||
// 4. grounded + distinctive → EPISODE or EVIDENCE
|
||||
let episode_strs = episode_texts(episodes);
|
||||
let corpus: Vec<String> = if corpus_texts.is_empty() {
|
||||
episode_strs.clone()
|
||||
} else {
|
||||
corpus_texts
|
||||
.iter()
|
||||
.map(|c| c.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
};
|
||||
if grounded(text, &corpus, 1.0) {
|
||||
let shared = shared_distinctive(text, &episode_strs, &corpus);
|
||||
if shared.is_empty() {
|
||||
return Decision::Episode(Reason::NovelGrounded);
|
||||
}
|
||||
return Decision::Episode(Reason::Shares(shared));
|
||||
}
|
||||
Decision::Evidence(Reason::Ungrounded)
|
||||
}
|
||||
|
||||
/// The oracle's return shape: {verdict, reason, provenance}.
|
||||
fn to_doc(event: &Value, decision: Decision) -> Value {
|
||||
json!({
|
||||
"verdict": decision.verdict().as_str(),
|
||||
"reason": decision.reason().to_string(),
|
||||
"provenance": {
|
||||
"source": event_field(event, "source", "unknown"),
|
||||
"session": event_field(event, "session_id", ""),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// The oracle's public function shape: the full return doc.
|
||||
fn evaluate(
|
||||
event: &Value,
|
||||
episodes: &[Value],
|
||||
corpus_texts: &[Value],
|
||||
lexicon_available: bool,
|
||||
) -> Value {
|
||||
to_doc(event, decide(event, episodes, corpus_texts, lexicon_available))
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
|
||||
|
||||
/// Typed FFI failure lattice behind the doc-envelope Result.
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for FfiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FfiError::NullPtr => write!(f, "null argument pointer"),
|
||||
FfiError::Utf8(e) => write!(f, "{e}"),
|
||||
FfiError::Json(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
|
||||
if ptr.is_null() {
|
||||
return Err(FfiError::NullPtr);
|
||||
}
|
||||
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(FfiError::Utf8)?;
|
||||
serde_json::from_str(x).map_err(FfiError::Json)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e.to_string()}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// ee_evaluate({event, existing_episodes, corpus_texts, lexicon_available})
|
||||
/// → {verdict, reason, provenance}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ee_evaluate(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let event = a.get("event").cloned().unwrap_or(json!({}));
|
||||
let episodes: Vec<Value> = a
|
||||
.get("existing_episodes")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let corpus: Vec<Value> = a
|
||||
.get("corpus_texts")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let lexicon = a
|
||||
.get("lexicon_available")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
evaluate(&event, &episodes, &corpus, lexicon)
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
Generated
+105
@@ -0,0 +1,105 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[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 = "mutation_guards"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "mutation_guards"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "high-value cognitive-state mutation guards (#156, dichotomy wave #2479) — FULL Rust port of src/rune/mutation_guards.py; zero-dep beyond serde_json"
|
||||
|
||||
[lib]
|
||||
name = "mutation_guards"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,54 @@
|
||||
//! mutation_guards branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (frozen_mg_truth.json: 43 cases across both public functions and
|
||||
//! every branch — the unattributed identity-family DENY and its precedence
|
||||
//! over read ops and the destructive gate (falsy 0/None attribution), the
|
||||
//! destructive-unbacked DENY with its op/kind severity-2 boundary and the
|
||||
//! falsy-zero backup, CONFIRM via score >= 4 (the exact boundary) and via
|
||||
//! identity-kind + mutating-op, truthy non-bool backups, the ALLOW
|
||||
//! lattice with unknown kind/op/scope severity defaults, null kind/operation
|
||||
//! rendered as Python str(None) in the reason f-strings, and guard_mutation's
|
||||
//! dict.get defaults (missing-key default vs present-null falsy passthrough,
|
||||
//! extra keys ignored, empty-mutation defaults)).
|
||||
//! Frozen via scripts/gen_mutation_guards_truth.py with RUNE_MG_CORE=python
|
||||
//! pinned (poisoned-truth law). Embedded VERBATIM — never transcribed. Each
|
||||
//! case replays the oracle's full return shape (verdict + reason + recovery)
|
||||
//! and compares whole-doc.
|
||||
|
||||
use crate::{classify_mutation, guard_mutation};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_mg_truth.json");
|
||||
|
||||
fn run(group: &str, case: &Value) -> Value {
|
||||
let input = &case["input"];
|
||||
if group == "classify" {
|
||||
let unset = Value::Null;
|
||||
let g = |k: &str| input.get(k).unwrap_or(&unset);
|
||||
classify_mutation(
|
||||
g("target_kind"),
|
||||
g("operation"),
|
||||
g("scope"),
|
||||
g("attributed"),
|
||||
g("backed_up"),
|
||||
)
|
||||
} else {
|
||||
let mutation = input.get("mutation").cloned().unwrap_or(json!({}));
|
||||
guard_mutation(&mutation)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_guards_parity_battery() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for (group, cases) in truth["cases"].as_object().unwrap() {
|
||||
for case in cases.as_array().unwrap() {
|
||||
let got = run(group, case);
|
||||
assert_eq!(
|
||||
&got,
|
||||
&case["expected"],
|
||||
"case '{}' diverged",
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
{
|
||||
"frozen": "2026-10-12T12:16:17.572030+00:00",
|
||||
"oracle": "src/rune/mutation_guards.py",
|
||||
"pin": "RUNE_MG_CORE=python",
|
||||
"note": "Frozen via scripts/gen_mutation_guards_truth.py against the live Python oracle. Battery embeds this VERBATIM.",
|
||||
"cases": {
|
||||
"classify": [
|
||||
{
|
||||
"name": "cl-deny1-unattributed-identity-overwrite",
|
||||
"input": {
|
||||
"target_kind": "identity",
|
||||
"operation": "overwrite",
|
||||
"scope": "single",
|
||||
"attributed": false,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny1-unattributed-persona-append-whole",
|
||||
"input": {
|
||||
"target_kind": "persona",
|
||||
"operation": "append",
|
||||
"scope": "whole",
|
||||
"attributed": false,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of persona \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of persona \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny1-constitution-attributed-zero",
|
||||
"input": {
|
||||
"target_kind": "constitution",
|
||||
"operation": "append",
|
||||
"scope": "single",
|
||||
"attributed": 0,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of constitution \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of constitution \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny1-identity-read-still-denied",
|
||||
"input": {
|
||||
"target_kind": "identity",
|
||||
"operation": "read",
|
||||
"scope": "single",
|
||||
"attributed": false,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed read of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed read of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny1-identity-attributed-null",
|
||||
"input": {
|
||||
"target_kind": "identity",
|
||||
"operation": "append",
|
||||
"scope": "single",
|
||||
"attributed": null,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny2-constitution-delete-whole",
|
||||
"input": {
|
||||
"target_kind": "constitution",
|
||||
"operation": "delete",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny2-skill-delete-multi",
|
||||
"input": {
|
||||
"target_kind": "skill",
|
||||
"operation": "delete",
|
||||
"scope": "multi",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "delete of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "delete of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny2-belief-overwrite-whole-boundary",
|
||||
"input": {
|
||||
"target_kind": "belief",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "overwrite of belief (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "overwrite of belief (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-deny2-skill-overwrite-whole-backed-zero",
|
||||
"input": {
|
||||
"target_kind": "skill",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": 0
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-backed-skill-overwrite-whole",
|
||||
"input": {
|
||||
"target_kind": "skill",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-identity-append-attributed",
|
||||
"input": {
|
||||
"target_kind": "identity",
|
||||
"operation": "append",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "append of identity (score 4) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "append of identity (score 4) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-identity-read-whole-score",
|
||||
"input": {
|
||||
"target_kind": "identity",
|
||||
"operation": "read",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "read of identity (score 5) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "read of identity (score 5) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-operating-delete-single-score-4",
|
||||
"input": {
|
||||
"target_kind": "operating",
|
||||
"operation": "delete",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of operating (score 4) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of operating (score 4) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-fact-overwrite-whole",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of fact (score 5) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of fact (score 5) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-skill-delete-multi-backed-truthy",
|
||||
"input": {
|
||||
"target_kind": "skill",
|
||||
"operation": "delete",
|
||||
"scope": "multi",
|
||||
"attributed": true,
|
||||
"backed_up": "yes"
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-constitution-delete-whole-backed",
|
||||
"input": {
|
||||
"target_kind": "constitution",
|
||||
"operation": "delete",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of constitution (score 8) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of constitution (score 8) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-persona-overwrite-whole-backed",
|
||||
"input": {
|
||||
"target_kind": "persona",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": true
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of persona (score 7) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of persona (score 7) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-archive-delete-whole-score",
|
||||
"input": {
|
||||
"target_kind": "archive",
|
||||
"operation": "delete",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of archive (score 5) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of archive (score 5) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-confirm-unknown-kind-delete",
|
||||
"input": {
|
||||
"target_kind": "widget",
|
||||
"operation": "delete",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of widget (score 4) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "delete of widget (score 4) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-fact-append-single-defaults",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "append",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-fact-append-multi-score-3",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "append",
|
||||
"scope": "multi",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-identity-read-single",
|
||||
"input": {
|
||||
"target_kind": "identity",
|
||||
"operation": "read",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of identity (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of identity (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-archive-delete-single",
|
||||
"input": {
|
||||
"target_kind": "archive",
|
||||
"operation": "delete",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "delete of archive (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "delete of archive (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-archive-read-whole",
|
||||
"input": {
|
||||
"target_kind": "archive",
|
||||
"operation": "read",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of archive (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of archive (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-fact-read-whole",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "read",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of fact (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of fact (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-unknown-operation",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "nuke",
|
||||
"scope": "multi",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "nuke of fact (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "nuke of fact (score 3) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-unknown-scope",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "append",
|
||||
"scope": "galaxy",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-null-kind-renders-None",
|
||||
"input": {
|
||||
"target_kind": null,
|
||||
"operation": "append",
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-null-operation-renders-None",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": null,
|
||||
"scope": "single",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "None of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "None of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cl-allow-null-scope",
|
||||
"input": {
|
||||
"target_kind": "fact",
|
||||
"operation": "append",
|
||||
"scope": null,
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"guard": [
|
||||
{
|
||||
"name": "gu-empty-dict-all-defaults",
|
||||
"input": {
|
||||
"mutation": {}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-fact-append-allow",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "fact",
|
||||
"operation": "append"
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-unattributed-identity-deny",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "identity",
|
||||
"operation": "overwrite",
|
||||
"attributed": false
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-unbacked-constitution-delete-deny",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "constitution",
|
||||
"operation": "delete",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": false
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-backed-skill-overwrite-confirm",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "skill",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"attributed": true,
|
||||
"backed_up": true
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-attributed-null-falsy-deny",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "identity",
|
||||
"attributed": null
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-attributed-empty-list-falsy-deny",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "persona",
|
||||
"operation": "delete",
|
||||
"attributed": []
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed delete of persona \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "unattributed delete of persona \u2014 identity mutations require attribution",
|
||||
"recovery": "re-submit with an attributed source (user, curator, or contract)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-backed-up-zero-falsy-deny",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "skill",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"backed_up": 0
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "DENY",
|
||||
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "DENY",
|
||||
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
|
||||
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-backed-up-truthy-string-confirm",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "skill",
|
||||
"operation": "overwrite",
|
||||
"scope": "whole",
|
||||
"backed_up": "yes"
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "CONFIRM",
|
||||
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
|
||||
"recovery": "confirm with the snapshot id attached"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-read-allows",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "fact",
|
||||
"operation": "read"
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of fact (score 1) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "read of fact (score 1) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-extra-keys-ignored",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "fact",
|
||||
"operation": "append",
|
||||
"note": "x",
|
||||
"weight": 9
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-null-target-kind-renders-None",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": null
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gu-empty-operation-string",
|
||||
"input": {
|
||||
"mutation": {
|
||||
"target_kind": "fact",
|
||||
"operation": ""
|
||||
}
|
||||
},
|
||||
"return": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": " of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
},
|
||||
"expected": {
|
||||
"verdict": "ALLOW",
|
||||
"reason": " of fact (score 2) \u2014 small, reversible, attributed",
|
||||
"recovery": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
//! mutation_guards — #156: high-value cognitive-state mutation guards.
|
||||
//!
|
||||
//! FULL name-preserving port of src/rune/mutation_guards.py (dichotomy
|
||||
//! wave, #2479 tranche).
|
||||
//!
|
||||
//! Large or destructive changes to Rune's most valuable learned state must
|
||||
//! be visible, attributable, and recoverable. The guard classifies every
|
||||
//! proposed mutation by destructiveness and returns a verdict:
|
||||
//!
|
||||
//! ```text
|
||||
//! ALLOW — small, reversible, attributed change
|
||||
//! CONFIRM — large or identity-touching: needs explicit confirmation
|
||||
//! DENY — destructive without a recovery path (delete of unbacked
|
||||
//! state, silent identity rewrite)
|
||||
//! ```
|
||||
//!
|
||||
//! Classification (deterministic, no model):
|
||||
//! - target kind: identity/persona/constitution > skill/belief block > fact
|
||||
//! - operation: delete > overwrite > append
|
||||
//! - scope: whole-block > multi-entry > single entry
|
||||
//! - attribution: unattributed mutations to identity are DENY
|
||||
//!
|
||||
//! Recoverability: a CONFIRM/DENY verdict carries the recovery shape
|
||||
//! (snapshot id required, or the reason none exists).
|
||||
//!
|
||||
//! FFI (doc-envelope law): one JSON doc in, {"ok": true, "value": ...} |
|
||||
//! {"ok": false, "err": ...} out:
|
||||
//! * mu_classify({target_kind, operation, scope, attributed, backed_up})
|
||||
//! → {verdict, reason, recovery}
|
||||
//! * mu_guard({mutation}) → {verdict, reason, recovery}
|
||||
//!
|
||||
//! No mu_free_string export — sibling crates sharing the Python process
|
||||
//! already export identical free symbols; callers free through
|
||||
//! capability_scoring's cs_free_string.
|
||||
//!
|
||||
//! Python semantics preserved exactly:
|
||||
//! * severity-table lookups: unknown kind → 1, unknown op → 1,
|
||||
//! unknown/null scope → 0
|
||||
//! * branch order: the unattributed identity-family DENY runs FIRST
|
||||
//! (it beats a read operation and the destructive gate), then the
|
||||
//! destructive-unbacked DENY, then CONFIRM (score >= 4 or identity
|
||||
//! kind with any mutating op), then ALLOW
|
||||
//! * Python truthiness for `attributed`/`backed_up`: null/false/0/""/
|
||||
//! empty array/empty object are falsy — non-empty strings are truthy
|
||||
//! * guard_mutation's dict.get defaults apply only when the KEY is
|
||||
//! missing; a present-but-null value passes through as falsy
|
||||
//! * the reason f-strings render a null kind/operation as "None"
|
||||
//! (Python str(None))
|
||||
//!
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the classification
|
||||
//! surface is parsed ONCE into `Kind`/`Op`/`Scope` enums (unknown values
|
||||
//! become the exhaustive `Other` variant, preserving the oracle's
|
||||
//! severity-default quirk), so every downstream rule — severity tables,
|
||||
//! the identity-family guard, the destructive gate — is an exhaustive
|
||||
//! match over typed variants instead of repeated string comparisons; the
|
||||
//! verdict is a `Verdict` enum with `as_str`, and the whole return shape
|
||||
//! is one `Decision` struct projected to JSON in a single place. Battery
|
||||
//! green throughout (unchanged from the parity commit — the whole-doc
|
||||
//! replay pins the refactor).
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
// ── typed classification surface (parsed once, matched exhaustively) ───────
|
||||
|
||||
/// Target kind — the oracle's severity ladder; unknown kinds keep the
|
||||
/// default severity 1 (the `Other` variant).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Kind {
|
||||
Constitution,
|
||||
Identity,
|
||||
Persona,
|
||||
Skill,
|
||||
Belief,
|
||||
Operating,
|
||||
Fact,
|
||||
Archive,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
fn parse(v: &Value) -> Self {
|
||||
match v.as_str() {
|
||||
Some("constitution") => Kind::Constitution,
|
||||
Some("identity") => Kind::Identity,
|
||||
Some("persona") => Kind::Persona,
|
||||
Some("skill") => Kind::Skill,
|
||||
Some("belief") => Kind::Belief,
|
||||
Some("operating") => Kind::Operating,
|
||||
Some("fact") => Kind::Fact,
|
||||
Some("archive") => Kind::Archive,
|
||||
_ => Kind::Other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Target-kind severity (higher = more protected)
|
||||
fn severity(self) -> i64 {
|
||||
match self {
|
||||
Kind::Constitution | Kind::Identity | Kind::Persona => 3,
|
||||
Kind::Skill | Kind::Belief => 2,
|
||||
Kind::Operating | Kind::Fact | Kind::Other => 1,
|
||||
Kind::Archive => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_identity_family(self) -> bool {
|
||||
matches!(self, Kind::Identity | Kind::Persona | Kind::Constitution)
|
||||
}
|
||||
}
|
||||
|
||||
/// Operation — the oracle's destructive-force ladder; unknown operations
|
||||
/// keep the default severity 1 (the `Other` variant).
|
||||
#[derive(Clone, Copy)]
|
||||
enum Op {
|
||||
Delete,
|
||||
Overwrite,
|
||||
Append,
|
||||
Read,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
fn parse(v: &Value) -> Self {
|
||||
match v.as_str() {
|
||||
Some("delete") => Op::Delete,
|
||||
Some("overwrite") => Op::Overwrite,
|
||||
Some("append") => Op::Append,
|
||||
Some("read") => Op::Read,
|
||||
_ => Op::Other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Operation severity
|
||||
fn severity(self) -> i64 {
|
||||
match self {
|
||||
Op::Delete => 3,
|
||||
Op::Overwrite => 2,
|
||||
Op::Append | Op::Other => 1,
|
||||
Op::Read => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutation scope; unknown/null scopes keep the default severity 0 (the
|
||||
/// `Other` variant).
|
||||
#[derive(Clone, Copy)]
|
||||
enum Scope {
|
||||
Whole,
|
||||
Multi,
|
||||
Single,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
fn parse(v: &Value) -> Self {
|
||||
match v.as_str() {
|
||||
Some("whole") => Scope::Whole,
|
||||
Some("multi") => Scope::Multi,
|
||||
Some("single") => Scope::Single,
|
||||
_ => Scope::Other,
|
||||
}
|
||||
}
|
||||
|
||||
fn severity(self) -> i64 {
|
||||
match self {
|
||||
Scope::Whole => 2,
|
||||
Scope::Multi => 1,
|
||||
Scope::Single | Scope::Other => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The classification verdict — the oracle's three-way lattice.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Verdict {
|
||||
Allow,
|
||||
Confirm,
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Verdict::Allow => "ALLOW",
|
||||
Verdict::Confirm => "CONFIRM",
|
||||
Verdict::Deny => "DENY",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One classification outcome: verdict + reason + recovery, projected to
|
||||
/// the oracle's return shape in one place.
|
||||
struct Decision {
|
||||
verdict: Verdict,
|
||||
reason: String,
|
||||
recovery: &'static str,
|
||||
}
|
||||
|
||||
impl Decision {
|
||||
fn to_doc(&self) -> Value {
|
||||
json!({
|
||||
"verdict": self.verdict.as_str(),
|
||||
"reason": self.reason,
|
||||
"recovery": self.recovery,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness over JSON values (null/false/0/""/[]/{} are falsy).
|
||||
fn truthy(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Null => false,
|
||||
Value::Bool(b) => *b,
|
||||
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
|
||||
Value::String(s) => !s.is_empty(),
|
||||
Value::Array(a) => !a.is_empty(),
|
||||
Value::Object(o) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python str() over the JSON value for the reason f-strings: a null kind
|
||||
/// or operation renders "None", bools render "True"/"False".
|
||||
fn py_display(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Null => "None".to_string(),
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Bool(true) => "True".to_string(),
|
||||
Value::Bool(false) => "False".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── the classification core ──────────────────────────────────────────────────
|
||||
|
||||
/// `classify_mutation` — classify a proposed mutation.
|
||||
/// Returns {verdict, reason, recovery}.
|
||||
pub fn classify_mutation(
|
||||
target_kind: &Value,
|
||||
operation: &Value,
|
||||
scope: &Value,
|
||||
attributed: &Value,
|
||||
backed_up: &Value,
|
||||
) -> Value {
|
||||
let kind = Kind::parse(target_kind);
|
||||
let op = Op::parse(operation);
|
||||
let scope = Scope::parse(scope);
|
||||
let kind_sev = kind.severity();
|
||||
let op_sev = op.severity();
|
||||
let score = kind_sev + op_sev + scope.severity();
|
||||
let kind_str = py_display(target_kind);
|
||||
let op_str = py_display(operation);
|
||||
|
||||
let decision = if kind.is_identity_family() && !truthy(attributed) {
|
||||
// Unattributed identity mutation is always DENY (the silent-rewrite
|
||||
// class) — runs FIRST: it beats a read op and the destructive gate.
|
||||
Decision {
|
||||
verdict: Verdict::Deny,
|
||||
reason: format!(
|
||||
"unattributed {op_str} of {kind_str} — identity mutations require attribution"
|
||||
),
|
||||
recovery: "re-submit with an attributed source (user, curator, or contract)",
|
||||
}
|
||||
} else if is_destructive_unbacked(kind, op, backed_up) {
|
||||
// Destructive without recovery is DENY
|
||||
Decision {
|
||||
verdict: Verdict::Deny,
|
||||
reason: format!(
|
||||
"{op_str} of {kind_str} (score {score}) without a snapshot — destructive and unrecoverable"
|
||||
),
|
||||
recovery: "snapshot first (state_checkpoint.snapshot), then re-submit",
|
||||
}
|
||||
} else if score >= 4 || (kind_sev >= 3 && op_sev >= 1) {
|
||||
// Large or identity-touching needs confirmation
|
||||
Decision {
|
||||
verdict: Verdict::Confirm,
|
||||
reason: format!(
|
||||
"{op_str} of {kind_str} (score {score}) — high-value state, explicit confirmation required"
|
||||
),
|
||||
recovery: "confirm with the snapshot id attached",
|
||||
}
|
||||
} else {
|
||||
Decision {
|
||||
verdict: Verdict::Allow,
|
||||
reason: format!(
|
||||
"{op_str} of {kind_str} (score {score}) — small, reversible, attributed"
|
||||
),
|
||||
recovery: "",
|
||||
}
|
||||
};
|
||||
decision.to_doc()
|
||||
}
|
||||
|
||||
/// Destructive (op severity >= 2) on a protected kind (severity >= 2)
|
||||
/// without a backup — the unrecoverable class.
|
||||
fn is_destructive_unbacked(kind: Kind, op: Op, backed_up: &Value) -> bool {
|
||||
let destructive = match op {
|
||||
Op::Delete | Op::Overwrite => true,
|
||||
Op::Append | Op::Read | Op::Other => false,
|
||||
};
|
||||
let protected = match kind {
|
||||
Kind::Constitution | Kind::Identity | Kind::Persona | Kind::Skill | Kind::Belief => true,
|
||||
Kind::Operating | Kind::Fact | Kind::Archive | Kind::Other => false,
|
||||
};
|
||||
destructive && protected && !truthy(backed_up)
|
||||
}
|
||||
|
||||
/// `guard_mutation` — guard one mutation dict {target_kind, operation,
|
||||
/// scope, attributed, backed_up}. Returns the verdict dict.
|
||||
pub fn guard_mutation(mutation: &Value) -> Value {
|
||||
let empty = Map::new();
|
||||
let m = mutation.as_object().unwrap_or(&empty);
|
||||
let d_kind = Value::String("fact".into());
|
||||
let d_op = Value::String("append".into());
|
||||
let d_scope = Value::String("single".into());
|
||||
let d_attributed = Value::Bool(true);
|
||||
let d_backed_up = Value::Bool(false);
|
||||
// dict.get(key, default): the default applies ONLY when the key is
|
||||
// missing; a present-but-null value passes through as falsy.
|
||||
let target_kind = m.get("target_kind").unwrap_or(&d_kind);
|
||||
let operation = m.get("operation").unwrap_or(&d_op);
|
||||
let scope = m.get("scope").unwrap_or(&d_scope);
|
||||
let attributed = m.get("attributed").unwrap_or(&d_attributed);
|
||||
let backed_up = m.get("backed_up").unwrap_or(&d_backed_up);
|
||||
classify_mutation(target_kind, operation, scope, attributed, backed_up)
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
let x = unsafe { std::ffi::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}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// mu_classify({target_kind, operation, scope, attributed, backed_up})
|
||||
/// → {verdict, reason, recovery}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mu_classify(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let unset = Value::Null;
|
||||
let g = |k: &str| a.get(k).unwrap_or(&unset);
|
||||
classify_mutation(
|
||||
g("target_kind"),
|
||||
g("operation"),
|
||||
g("scope"),
|
||||
g("attributed"),
|
||||
g("backed_up"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// mu_guard({mutation}) → {verdict, reason, recovery}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mu_guard(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let mutation = a.get("mutation").cloned().unwrap_or(Value::Null);
|
||||
guard_mutation(&mutation)
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "spine_language"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_scoring",
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "spine_language"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "#2465 doc-level transforms (language placement, discovery-ID normalization, contract-discovery write-back) — dichotomy wave #2479 tranche; deterministic spine transforms in Rust; zero-dep (pyjson via capability_scoring)"
|
||||
|
||||
[lib]
|
||||
name = "spine_language"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
capability_scoring = { path = "../capability_scoring" }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,153 @@
|
||||
//! branch_battery — replays frozen_sl_truth.json VERBATIM (23 cases
|
||||
//! frozen from the live Python oracle with RUNE_SL_CORE=python pinned;
|
||||
//! inputs deep-copied before capture; wall-clock observed_at masked on
|
||||
//! BOTH sides — the module's only nondeterministic output field).
|
||||
//! Order-insensitive object comparison; per-case store fixtures
|
||||
//! (classification.json / scaffold current.json) are materialized under
|
||||
//! a fresh temp root exactly as the freezing run did.
|
||||
|
||||
use capability_scoring::pyjson::{parse, Json};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::spine;
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_sl_truth.json");
|
||||
|
||||
static TMP_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
fn jeq(a: &Json, b: &Json) -> bool {
|
||||
match (a, b) {
|
||||
(Json::Num(x), Json::Num(y)) => x == y,
|
||||
(Json::Str(x), Json::Str(y)) => x == y,
|
||||
(Json::Bool(x), Json::Bool(y)) => x == y,
|
||||
(Json::Null, Json::Null) => true,
|
||||
(Json::Arr(x), Json::Arr(y)) => {
|
||||
x.len() == y.len() && x.iter().zip(y).all(|(i, j)| jeq(i, j))
|
||||
}
|
||||
(Json::Obj(x), Json::Obj(y)) => {
|
||||
x.len() == y.len()
|
||||
&& x.iter()
|
||||
.all(|(k, v)| y.iter().any(|(k2, v2)| k == k2 && jeq(v, v2)))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize the case's fixture files under a fresh temp root.
|
||||
fn materialize(files: &Json) -> std::path::PathBuf {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"sl-battery-{}-{}",
|
||||
std::process::id(),
|
||||
TMP_COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
if let Json::Obj(pairs) = files {
|
||||
for (rel, content) in pairs {
|
||||
let path = root.join(rel);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let text = match content {
|
||||
Json::Str(s) => s.clone(),
|
||||
other => capability_scoring::pyjson::dumps(other),
|
||||
};
|
||||
let _ = std::fs::write(path, text);
|
||||
}
|
||||
}
|
||||
root
|
||||
}
|
||||
|
||||
fn run_case(fn_name: &str, case: &Json) -> Result<Json, String> {
|
||||
let root = materialize(case.get("files").unwrap_or(&Json::Null));
|
||||
let out = match fn_name {
|
||||
"classification_sides" => Ok(spine::classification_sides(&root)),
|
||||
"place_languages" => {
|
||||
let mut doc = case.get("doc").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
spine::place_languages(&mut doc, &root)
|
||||
.map(|placed| crate::result_doc(doc, "placed", placed))
|
||||
}
|
||||
"contract_discoveries" => {
|
||||
let found = spine::contract_discoveries(&root)?;
|
||||
Ok(Json::Arr(
|
||||
found
|
||||
.into_iter()
|
||||
.map(|(cid, items)| {
|
||||
Json::Arr(vec![
|
||||
Json::Str(cid),
|
||||
Json::Arr(
|
||||
items
|
||||
.into_iter()
|
||||
.map(|(eid, kind, prov)| {
|
||||
Json::Arr(vec![
|
||||
Json::Str(eid),
|
||||
Json::Str(kind),
|
||||
Json::Str(prov),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
"normalize_discovery_ids" => {
|
||||
let mut doc = case.get("doc").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let discoveries = spine::contract_discoveries(&root)?;
|
||||
spine::normalize_discovery_ids(&mut doc, &discoveries)
|
||||
.map(|renamed| crate::result_doc(doc, "renamed", renamed))
|
||||
}
|
||||
"apply_contract_discoveries" => {
|
||||
let mut doc = case.get("doc").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let discoveries = spine::contract_discoveries(&root)?;
|
||||
spine::apply_contract_discoveries(&mut doc, &discoveries)
|
||||
.map(|placed| crate::result_doc(doc, "placed", placed))
|
||||
}
|
||||
other => return Err(format!("unknown frozen fn {other}")),
|
||||
};
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
out.map(|v| spine::mask_observed_at(&v))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let truth = parse(TRUTH).expect("frozen_sl_truth.json parses");
|
||||
let cases = match truth.get("cases") {
|
||||
Some(Json::Arr(a)) => a.clone(),
|
||||
_ => panic!("cases key"),
|
||||
};
|
||||
assert!(
|
||||
cases.len() >= 23,
|
||||
"expected the full frozen battery, got {}",
|
||||
cases.len()
|
||||
);
|
||||
let mut failures: Vec<String> = vec![];
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
let fn_name = match case.get("fn") {
|
||||
Some(Json::Str(s)) => s.as_str().to_string(),
|
||||
_ => panic!("case {i} missing fn"),
|
||||
};
|
||||
let name = match case.get("name") {
|
||||
Some(Json::Str(s)) => s.as_str().to_string(),
|
||||
_ => format!("case-{i}"),
|
||||
};
|
||||
let expected = spine::mask_observed_at(&case.get("expected").cloned().unwrap_or(Json::Null));
|
||||
match run_case(&fn_name, case) {
|
||||
Ok(actual) => {
|
||||
if !jeq(&expected, &actual) {
|
||||
failures.push(format!(
|
||||
"case {i} {fn_name}/{name}:\n expected={}\n actual ={}",
|
||||
capability_scoring::pyjson::dumps(&expected),
|
||||
capability_scoring::pyjson::dumps(&actual),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => failures.push(format!("case {i} {fn_name}/{name}: ERR {e}")),
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"frozen-truth divergences ({}):\n{}",
|
||||
failures.len(),
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! spine_language — FFI surface for the #2465 doc-level transforms
|
||||
//! (dichotomy wave #2479 tranche). RUST verdict: deterministic spine
|
||||
//! transforms in Rust; the Python facade keeps a RUNE_SL_CORE=python
|
||||
//! pin (strangler law).
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok","value"|"err"}):
|
||||
//! - sl_classification_sides({"root": str}) -> measured map {stem: side}
|
||||
//! - sl_place_languages({"root": str, "doc": doc}) -> {"doc", "placed"}
|
||||
//! - sl_contract_discoveries({"root": str}) -> [[cid, [[eid, kind, prov], ...]], ...]
|
||||
//! - sl_normalize_discovery_ids({"doc", "root"?/ "discoveries"?}) -> {"doc", "renamed"}
|
||||
//! - sl_apply_contract_discoveries({"doc", "root"?/ "discoveries"?}) -> {"doc", "placed"}
|
||||
//!
|
||||
//! `root` is the repo root (the loader always sends str(ROOT)); the store
|
||||
//! reads beneath it degrade exactly like the oracle's best-effort except.
|
||||
//!
|
||||
//! Parity oracle: frozen_sl_truth.json (23 cases, frozen from the live
|
||||
//! Python oracle with RUNE_SL_CORE=python pinned, observed_at masked).
|
||||
//!
|
||||
//! NOTE: no sl_free_string — capability_scoring (same process) exports
|
||||
//! cs_free_string and every returned pointer is freed through it.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
pub mod spine;
|
||||
|
||||
pub use capability_scoring::pyjson;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use pyjson::Json;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<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)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
]),
|
||||
};
|
||||
CString::new(pyjson::dumps(&v))
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn root_arg(v: &Json) -> Result<std::path::PathBuf, String> {
|
||||
match v.get("root") {
|
||||
Some(Json::Str(s)) if !s.is_empty() => Ok(std::path::PathBuf::from(s)),
|
||||
_ => Err("root (repo root string) is required".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn doc_arg(v: &Json) -> Result<Json, String> {
|
||||
match v.get("doc") {
|
||||
Some(d @ Json::Obj(_)) => Ok(d.clone()),
|
||||
_ => Err("doc (object) is required".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gather discoveries for a transform: an injected wire list (the
|
||||
/// facade's `_contract_discoveries` seam) wins; otherwise the store read
|
||||
/// under `root`.
|
||||
fn discoveries_arg(a: &Json) -> Result<spine::Discoveries, String> {
|
||||
match a.get("discoveries") {
|
||||
Some(wire) => spine::parse_discoveries(wire),
|
||||
None => {
|
||||
let root = root_arg(a)?;
|
||||
spine::contract_discoveries(&root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn result_doc(doc: Json, count: &str, n: i64) -> Json {
|
||||
Json::Obj(vec![
|
||||
("doc".into(), doc),
|
||||
(count.into(), Json::Num(n as f64)),
|
||||
])
|
||||
}
|
||||
|
||||
/// sl_classification_sides({"root": str})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sl_classification_sides(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a)?;
|
||||
Ok(spine::classification_sides(&root))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sl_place_languages({"root": str, "doc": doc})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sl_place_languages(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a)?;
|
||||
let mut doc = doc_arg(&a)?;
|
||||
let placed = spine::place_languages(&mut doc, &root)?;
|
||||
Ok(result_doc(doc, "placed", placed))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sl_contract_discoveries({"root": str})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sl_contract_discoveries(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a)?;
|
||||
let found = spine::contract_discoveries(&root)?;
|
||||
Ok(Json::Arr(
|
||||
found
|
||||
.into_iter()
|
||||
.map(|(cid, items)| {
|
||||
Json::Arr(vec![
|
||||
Json::Str(cid),
|
||||
Json::Arr(
|
||||
items
|
||||
.into_iter()
|
||||
.map(|(eid, kind, prov)| {
|
||||
Json::Arr(vec![
|
||||
Json::Str(eid),
|
||||
Json::Str(kind),
|
||||
Json::Str(prov),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sl_normalize_discovery_ids({"doc": doc, "root"?/ "discoveries"?})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sl_normalize_discovery_ids(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let mut doc = doc_arg(&a)?;
|
||||
let discoveries = discoveries_arg(&a)?;
|
||||
let renamed = spine::normalize_discovery_ids(&mut doc, &discoveries)?;
|
||||
Ok(result_doc(doc, "renamed", renamed))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sl_apply_contract_discoveries({"doc": doc, "root"?/ "discoveries"?})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sl_apply_contract_discoveries(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let mut doc = doc_arg(&a)?;
|
||||
let discoveries = discoveries_arg(&a)?;
|
||||
let placed = spine::apply_contract_discoveries(&mut doc, &discoveries)?;
|
||||
Ok(result_doc(doc, "placed", placed))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
//! spine — the #2465 doc-level transforms (Rust core). Mirrors
|
||||
//! src/rune/spine_language.py exactly, including quirks:
|
||||
//! * the measured classification map decides FIRST; name hints only
|
||||
//! label UNMEASURED entities ("(name hint)"); neither measurement
|
||||
//! nor hint on a module → "unclassified", never a silent default;
|
||||
//! * a subsystem's language is its members' measured majority;
|
||||
//! * `_normalize_discovery_ids` demotes any construction-surface whose
|
||||
//! id is not in the OBSERVED set (the unsupported-PRESENT undo) and
|
||||
//! always rewrites its tags without "code";
|
||||
//! * `_apply_contract_discoveries` prefixes construction-surface ids
|
||||
//! unconditionally (the historical double-prefix quirk is preserved
|
||||
//! and pinned by the frozen battery);
|
||||
//! * add_entity merge is the structural_store high-water law
|
||||
//! (maturity never downgrades, sorted set-union tags/evidence).
|
||||
//!
|
||||
//! Store reads (classification.json / scaffold current.json) degrade to
|
||||
//! empty exactly like the oracle's best-effort except-clause; the
|
||||
//! wall-clock `observed_at` stamp is emitted (same field set as the
|
||||
//! oracle) and masked by every comparison surface.
|
||||
//!
|
||||
//! Phase B (language-advantage pass): transforms mutate the doc IN PLACE
|
||||
//! (`&mut Json`, no whole-document clone), the measured side is a real
|
||||
//! enum matched exhaustively, subsystem member counting is a single pass
|
||||
//! over the relationships, and the verdict ladder compares borrowed
|
||||
//! strings — the frozen battery proves the output byte-for-byte
|
||||
//! unchanged.
|
||||
|
||||
use capability_scoring::pyjson::Json;
|
||||
|
||||
/// Names of the store files the transforms read, relative to the repo
|
||||
/// root passed in the FFI payload (the loader always sends str(ROOT)).
|
||||
pub const CLASSIFICATION_REL: &str = "store/rebase/classification.json";
|
||||
pub const SCAFFOLD_REL: &str = "store/scaffold/current.json";
|
||||
|
||||
pub const GO_HINTS: &[&str] = &[
|
||||
"server", "transport", "dispatch", "entry", "bridge", "hook_engine",
|
||||
"provider", "watch", "notif", "voice", "gitea", "event_", "message_",
|
||||
"tool_exec", "work_manager",
|
||||
];
|
||||
pub const RUST_HINTS: &[&str] = &[
|
||||
"memory", "drift", "warm", "capab", "token", "spine", "scaffold",
|
||||
"construct", "structural", "curator", "consolidat", "elastic",
|
||||
"cognit", "abstract", "contract", "ledger", "audit", "claim",
|
||||
"evidence", "behavio", "verif", "pins", "witness", "anchor", "growth",
|
||||
"goal", "salience", "store", "tui_", "rune_tui",
|
||||
];
|
||||
|
||||
/// The measured dichotomy verdict for one module stem, after the
|
||||
/// oracle's `str(x or "").upper()` coercion. `Other` covers every value
|
||||
/// that is not one of the five known sides (they all fall through the
|
||||
/// authority ladder exactly like the oracle's string compares).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum MeasuredSide {
|
||||
Rust,
|
||||
Go,
|
||||
TuiLast,
|
||||
TestInfra,
|
||||
Retires,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Python `str(x or "").upper()` semantics for a classification-map
|
||||
/// value. Num renders like Python str(int) when integral (the frozen
|
||||
/// truth pins the int case; float text is a documented pathological
|
||||
/// divergence); composite values coerce to "" (pathological only).
|
||||
fn measured_side(v: Option<&Json>) -> MeasuredSide {
|
||||
let s = match v {
|
||||
// str(None)/str("") coerce to "" via the oracle's `x or ""`;
|
||||
// str(True)="TRUE", str(int) are digits — none match a known side.
|
||||
None | Some(Json::Null) | Some(Json::Bool(_)) | Some(Json::Num(_)) => {
|
||||
return MeasuredSide::Other
|
||||
}
|
||||
Some(Json::Str(s)) => s.as_str(),
|
||||
Some(Json::Arr(_)) | Some(Json::Obj(_)) => return MeasuredSide::Other,
|
||||
};
|
||||
// Borrowed, case-insensitive compare — no intermediate uppercase String.
|
||||
for (known, side) in [
|
||||
("RUST", MeasuredSide::Rust),
|
||||
("GO", MeasuredSide::Go),
|
||||
("TUI-LAST", MeasuredSide::TuiLast),
|
||||
("TEST-INFRA", MeasuredSide::TestInfra),
|
||||
("RETIRES", MeasuredSide::Retires),
|
||||
] {
|
||||
if s.len() == known.len() && s.eq_ignore_ascii_case(known) {
|
||||
return side;
|
||||
}
|
||||
}
|
||||
MeasuredSide::Other
|
||||
}
|
||||
|
||||
/// structural_store.MATURITY_RANK (high-water law).
|
||||
fn maturity_rank(m: &str) -> i64 {
|
||||
match m {
|
||||
"DECLARED" => 0,
|
||||
"PRESENT" => 1,
|
||||
"WIRED" => 2,
|
||||
"REACHABLE" => 3,
|
||||
"EXERCISED" => 4,
|
||||
"LIVE_PROVEN" => 5,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// structural_store.now(): "%Y-%m-%dT%H:%M:%SZ" UTC.
|
||||
pub fn now() -> String {
|
||||
let secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let days = (secs / 86_400) as i64;
|
||||
let rem = secs % 86_400;
|
||||
// Howard Hinnant's civil_from_days.
|
||||
let z = days + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||
let doe = z - era * 146_097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
rem / 3600,
|
||||
(rem % 3600) / 60,
|
||||
rem % 60
|
||||
)
|
||||
}
|
||||
|
||||
// ── small Json object helpers (pyjson keeps insertion order) ────────────
|
||||
|
||||
fn obj_pairs(v: &Json) -> Option<&Vec<(String, Json)>> {
|
||||
match v {
|
||||
Json::Obj(pairs) => Some(pairs),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn obj_pairs_mut(v: &mut Json) -> Option<&mut Vec<(String, Json)>> {
|
||||
match v {
|
||||
Json::Obj(pairs) => Some(pairs),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn obj_get<'a>(v: &'a Json, key: &str) -> Option<&'a Json> {
|
||||
obj_pairs(v).and_then(|p| p.iter().find(|(k, _)| k == key).map(|(_, x)| x))
|
||||
}
|
||||
|
||||
fn obj_get_mut<'a>(v: &'a mut Json, key: &str) -> Option<&'a mut Json> {
|
||||
obj_pairs_mut(v)
|
||||
.and_then(|p| p.iter_mut().find(|(k, _)| k == key).map(|(_, x)| x))
|
||||
}
|
||||
|
||||
/// Insert or replace `key` in an Obj (keeps original position on replace).
|
||||
fn obj_set(v: &mut Json, key: &str, val: Json) {
|
||||
if let Some(pairs) = obj_pairs_mut(v) {
|
||||
if let Some(slot) = pairs.iter_mut().find(|(k, _)| k == key) {
|
||||
slot.1 = val;
|
||||
} else {
|
||||
pairs.push((key.to_string(), val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(v: Option<&Json>) -> String {
|
||||
match v {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively drop wall-clock stamps (masking law — battery + parity).
|
||||
pub fn mask_observed_at(v: &Json) -> Json {
|
||||
match v {
|
||||
Json::Arr(items) => Json::Arr(items.iter().map(mask_observed_at).collect()),
|
||||
Json::Obj(pairs) => Json::Obj(
|
||||
pairs
|
||||
.iter()
|
||||
.filter(|(k, _)| k != "observed_at")
|
||||
.map(|(k, x)| (k.clone(), mask_observed_at(x)))
|
||||
.collect(),
|
||||
),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── _classification_sides ────────────────────────────────────────────────
|
||||
|
||||
/// The measured dichotomy map: module stem → side. Missing/unreadable/
|
||||
/// malformed/非-dict store degrades to {} — never to invented facts.
|
||||
pub fn classification_sides(root: &std::path::Path) -> Json {
|
||||
let text = match std::fs::read_to_string(root.join(CLASSIFICATION_REL)) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return Json::Obj(vec![]),
|
||||
};
|
||||
let data = match capability_scoring::pyjson::parse(&text) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return Json::Obj(vec![]),
|
||||
};
|
||||
match data {
|
||||
Json::Obj(pairs) => Json::Obj(
|
||||
pairs
|
||||
.into_iter()
|
||||
.filter(|(_, rec)| matches!(rec, Json::Obj(_)))
|
||||
.map(|(name, rec)| {
|
||||
let side =
|
||||
obj_get(&rec, "side").cloned().unwrap_or(Json::Str(String::new()));
|
||||
(name, side)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
// Python: .items() on a non-dict raises AttributeError, caught → {}.
|
||||
_ => Json::Obj(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
// ── _contract_discoveries ────────────────────────────────────────────────
|
||||
|
||||
/// Closed contracts' DISCOVERED entities: (contract_id, [(eid, kind,
|
||||
/// provenance)]). Only CLOSED contracts teach the spine (CANDIDATE law).
|
||||
/// Missing store / invalid JSON → []; valid-but-non-dict store mirrors
|
||||
/// the oracle's uncaught AttributeError as an Err.
|
||||
pub type Discoveries = Vec<(String, Vec<(String, String, String)>)>;
|
||||
|
||||
pub fn contract_discoveries(root: &std::path::Path) -> Result<Discoveries, String> {
|
||||
let text = match std::fs::read_to_string(root.join(SCAFFOLD_REL)) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
let doc = match capability_scoring::pyjson::parse(&text) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
let entities = match obj_get(&doc, "entities") {
|
||||
Some(Json::Arr(a)) => a.clone(),
|
||||
Some(_) => return Err("AttributeError: scaffold store is not a doc".into()),
|
||||
None => return Ok(vec![]),
|
||||
};
|
||||
let mut out: Discoveries = vec![];
|
||||
for e in &entities {
|
||||
if obj_pairs(e).is_none() {
|
||||
return Err("AttributeError: scaffold entity is not an object".into());
|
||||
}
|
||||
let cid = as_str(obj_get(e, "id"));
|
||||
if !cid.starts_with("contract:") || as_str(obj_get(e, "phase")) != "closed" {
|
||||
continue;
|
||||
}
|
||||
let ext = obj_get(e, "ext").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let q = obj_get(&ext, "contract_quality").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let mut found: Vec<(String, String, String)> = vec![];
|
||||
if let Some(Json::Arr(observed)) = obj_get(&q, "expected_new_observed") {
|
||||
for eid in observed {
|
||||
let eid = match eid {
|
||||
Json::Str(s) => s.clone(),
|
||||
_ => continue,
|
||||
};
|
||||
let kind = split_kind(&eid);
|
||||
if is_discoverable_kind(&kind) {
|
||||
found.push((eid, kind, "observed".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(Json::Arr(cands)) = obj_get(&q, "candidate_corrections") {
|
||||
for cand in cands {
|
||||
let eid = match obj_get(cand, "entity") {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
if eid.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let kind = split_kind(&eid);
|
||||
if is_discoverable_kind(&kind) {
|
||||
found.push((eid, kind, "candidate".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found.is_empty() {
|
||||
out.push((cid[9..].to_string(), found));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The open-world kinds a discovery may carry (the oracle's allow list —
|
||||
/// construction-surface is the fallback for bare ids).
|
||||
fn is_discoverable_kind(kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"rust-crate" | "go-surface" | "config" | "workflow" | "module" | "construction-surface"
|
||||
)
|
||||
}
|
||||
|
||||
/// eid.split(":", 1)[0] if ":" in eid else "construction-surface".
|
||||
fn split_kind(eid: &str) -> String {
|
||||
match eid.split_once(':') {
|
||||
Some((kind, _)) => kind.to_string(),
|
||||
None => "construction-surface".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the wire form [[cid, [[eid, kind, provenance], ...]], ...]
|
||||
/// (as returned by sl_contract_discoveries / the facade) into gathered
|
||||
/// discoveries. Used when the caller injects the gather result (the
|
||||
/// facade's `_contract_discoveries` monkeypatch seam) instead of the
|
||||
/// store read.
|
||||
pub fn parse_discoveries(wire: &Json) -> Result<Discoveries, String> {
|
||||
let arr = match wire {
|
||||
Json::Arr(a) => a,
|
||||
_ => return Err("discoveries must be a list".into()),
|
||||
};
|
||||
let mut out: Discoveries = vec![];
|
||||
for entry in arr {
|
||||
let parts = match entry {
|
||||
Json::Arr(p) if p.len() == 2 => p,
|
||||
_ => return Err("discovery entry must be [cid, items]".into()),
|
||||
};
|
||||
let cid = match &parts[0] {
|
||||
Json::Str(s) => s.clone(),
|
||||
_ => return Err("discovery cid must be a string".into()),
|
||||
};
|
||||
let items = match &parts[1] {
|
||||
Json::Arr(items) => items,
|
||||
_ => return Err("discovery items must be a list".into()),
|
||||
};
|
||||
let mut found = vec![];
|
||||
for item in items {
|
||||
let triple = match item {
|
||||
Json::Arr(t) if t.len() == 3 => t,
|
||||
_ => return Err("discovery item must be [eid, kind, provenance]".into()),
|
||||
};
|
||||
let mut strs = vec![];
|
||||
for x in triple {
|
||||
match x {
|
||||
Json::Str(s) => strs.push(s.clone()),
|
||||
_ => return Err("discovery item fields must be strings".into()),
|
||||
}
|
||||
}
|
||||
found.push((strs[0].clone(), strs[1].clone(), strs[2].clone()));
|
||||
}
|
||||
out.push((cid, found));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ── add_entity (structural_store high-water merge, JSON level) ──────────
|
||||
|
||||
/// Mirror of rune.structural_store.add_entity for the write-back path:
|
||||
/// never downgrades maturity; sorted set-union tags/evidence; identities
|
||||
/// update; wall-clock observed_at stamp on every touch.
|
||||
fn add_entity(
|
||||
doc: &mut Json,
|
||||
id: &str,
|
||||
kind: &str,
|
||||
tags: &[&str],
|
||||
identities: Option<Vec<(String, Json)>>,
|
||||
evidence_refs: &[String],
|
||||
maturity: &str,
|
||||
) {
|
||||
let existing_maturity = |e: &Json| {
|
||||
as_str(obj_get(e, "maturity").or(Some(&Json::Str("DECLARED".into()))))
|
||||
};
|
||||
let hit = obj_get_mut(doc, "entities").and_then(|entities| {
|
||||
let Json::Arr(a) = entities else { return None };
|
||||
a.iter_mut()
|
||||
.find(|e| as_str(obj_get(e, "id")) == id)
|
||||
.map(|e| {
|
||||
if !maturity.is_empty()
|
||||
&& maturity_rank(maturity) > maturity_rank(&existing_maturity(e))
|
||||
{
|
||||
obj_set(e, "maturity", Json::Str(maturity.to_string()));
|
||||
}
|
||||
if !tags.is_empty() {
|
||||
let mut set = string_set(obj_get(e, "tags"));
|
||||
set.extend(tags.iter().map(|s| s.to_string()));
|
||||
obj_set(e, "tags", sorted_strs(set));
|
||||
}
|
||||
if let Some(idents) = &identities {
|
||||
if obj_get(e, "identities").is_none() {
|
||||
obj_set(e, "identities", Json::Obj(vec![]));
|
||||
}
|
||||
for (k, v) in idents {
|
||||
if let Some(ids) = obj_get_mut(e, "identities") {
|
||||
obj_set(ids, k, v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !evidence_refs.is_empty() {
|
||||
let mut set = string_set(obj_get(e, "evidence_refs"));
|
||||
set.extend(evidence_refs.iter().cloned());
|
||||
obj_set(e, "evidence_refs", sorted_strs(set));
|
||||
}
|
||||
obj_set(e, "observed_at", Json::Str(now()));
|
||||
})
|
||||
});
|
||||
if hit.is_some() {
|
||||
return;
|
||||
}
|
||||
let ent = Json::Obj(vec![
|
||||
("id".into(), Json::Str(id.to_string())),
|
||||
("kind".into(), Json::Str(kind.to_string())),
|
||||
("maturity".into(), Json::Str(maturity.to_string())),
|
||||
(
|
||||
"tags".into(),
|
||||
sorted_strs(tags.iter().map(|s| s.to_string()).collect()),
|
||||
),
|
||||
(
|
||||
"identities".into(),
|
||||
Json::Obj(identities.unwrap_or_default()),
|
||||
),
|
||||
(
|
||||
"evidence_refs".into(),
|
||||
sorted_strs(evidence_refs.iter().cloned().collect()),
|
||||
),
|
||||
("observed_at".into(), Json::Str(now())),
|
||||
]);
|
||||
if let Some(Json::Arr(a)) = obj_get_mut(doc, "entities") {
|
||||
a.push(ent);
|
||||
}
|
||||
}
|
||||
|
||||
fn string_set(v: Option<&Json>) -> std::collections::BTreeSet<String> {
|
||||
match v {
|
||||
Some(Json::Arr(items)) => items
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sorted_strs(set: std::collections::BTreeSet<String>) -> Json {
|
||||
Json::Arr(set.into_iter().map(Json::Str).collect())
|
||||
}
|
||||
|
||||
// ── _place_languages ─────────────────────────────────────────────────────
|
||||
|
||||
/// Stamp a language verdict per entity IN PLACE; returns the placed count
|
||||
/// (verdicts != "unassessed").
|
||||
pub fn place_languages(doc: &mut Json, root: &std::path::Path) -> Result<i64, String> {
|
||||
let sides = classification_sides(root);
|
||||
place_languages_with_sides(doc, &sides)
|
||||
}
|
||||
|
||||
pub fn place_languages_with_sides(doc: &mut Json, sides: &Json) -> Result<i64, String> {
|
||||
let n_entities = match obj_get(doc, "entities") {
|
||||
Some(Json::Arr(a)) => a.len(),
|
||||
Some(_) => return Err("AttributeError: entities is not a list".into()),
|
||||
None => return Ok(0),
|
||||
};
|
||||
// Single pass over the relationships: belongs_to member counts per
|
||||
// subsystem target (the oracle recomputes per entity; the majority
|
||||
// verdict is identical — the frozen battery pins it).
|
||||
let mut member_counts: std::collections::HashMap<String, (i64, i64)> = Default::default();
|
||||
match obj_get(doc, "relationships") {
|
||||
Some(Json::Arr(rels)) => {
|
||||
for r in rels {
|
||||
if as_str(obj_get(r, "type")) != "belongs_to" {
|
||||
continue;
|
||||
}
|
||||
let to = as_str(obj_get(r, "to"));
|
||||
let from = as_str(obj_get(r, "from"));
|
||||
if let Some(member) = from.strip_prefix("module:") {
|
||||
let entry = member_counts.entry(to).or_insert((0, 0));
|
||||
match measured_side(obj_get(sides, member)) {
|
||||
MeasuredSide::Rust => entry.0 += 1,
|
||||
MeasuredSide::Go => entry.1 += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => return Err("AttributeError: relationships is not a list".into()),
|
||||
None => {}
|
||||
}
|
||||
let mut placed = 0i64;
|
||||
for i in 0..n_entities {
|
||||
let e = match obj_get_mut(doc, "entities") {
|
||||
Some(Json::Arr(a)) => &mut a[i],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let eid = as_str(obj_get(e, "id"));
|
||||
let kind = as_str(obj_get(e, "kind"));
|
||||
let measured_key: &str = match eid.split_once(':') {
|
||||
Some((_, rest)) => rest,
|
||||
None => &eid,
|
||||
};
|
||||
let measured = measured_side(obj_get(sides, measured_key));
|
||||
// Verdict ladder, authority order preserved: contract prefix →
|
||||
// kind defaults → MEASURED map → tui in eid → name hints →
|
||||
// module/subsystem containers → honest unassessed.
|
||||
let lang: Lang = if eid.starts_with("contract:") {
|
||||
Lang::Fixed("n/a")
|
||||
} else {
|
||||
match (kind.as_str(), measured) {
|
||||
("rust-crate", _) => Lang::Fixed("rust"),
|
||||
("go-surface", _) => Lang::Fixed("go"),
|
||||
("workflow", _) | ("config", _) => Lang::Fixed("n/a"),
|
||||
("deployment_surface", _) => Lang::Fixed("go"),
|
||||
(_, MeasuredSide::Rust) => Lang::Fixed("rust"),
|
||||
(_, MeasuredSide::Go) => Lang::Fixed("go"),
|
||||
(_, MeasuredSide::TuiLast) => Lang::Fixed("python-tui (Wave 6)"),
|
||||
(_, MeasuredSide::TestInfra) => Lang::Fixed("test-infra"),
|
||||
(_, MeasuredSide::Retires) => Lang::Fixed("retires"),
|
||||
(_, MeasuredSide::Other) => {
|
||||
if eid.contains("tui") {
|
||||
Lang::Fixed("python-tui (Wave 6)")
|
||||
} else if GO_HINTS.iter().any(|h| eid.contains(h)) {
|
||||
Lang::Fixed("go (name hint)")
|
||||
} else if RUST_HINTS.iter().any(|h| eid.contains(h)) {
|
||||
Lang::Fixed("rust (name hint)")
|
||||
} else if kind == "module" {
|
||||
Lang::Fixed("unclassified")
|
||||
} else if kind == "subsystem" {
|
||||
match member_counts.get(&eid) {
|
||||
Some((rust, go)) if *rust > 0 && *go > 0 => Lang::Mixed(*rust, *go),
|
||||
Some((rust, _)) if *rust > 0 => Lang::Fixed("rust"),
|
||||
Some((_, go)) if *go > 0 => Lang::Fixed("go"),
|
||||
_ => Lang::Fixed("unassessed"),
|
||||
}
|
||||
} else {
|
||||
Lang::Fixed("unassessed")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if obj_get(e, "identities").is_none() {
|
||||
obj_set(e, "identities", Json::Obj(vec![]));
|
||||
}
|
||||
let unassessed = matches!(lang, Lang::Fixed("unassessed"));
|
||||
if let Some(ids) = obj_get_mut(e, "identities") {
|
||||
obj_set(ids, "language", lang.into_json());
|
||||
}
|
||||
if !unassessed {
|
||||
placed += 1;
|
||||
}
|
||||
}
|
||||
Ok(placed)
|
||||
}
|
||||
|
||||
/// A language verdict: the common case is a fixed literal; subsystem
|
||||
/// containers derive `mixed (rust R / go G members)` text from counts.
|
||||
enum Lang {
|
||||
Fixed(&'static str),
|
||||
Mixed(i64, i64),
|
||||
}
|
||||
|
||||
impl Lang {
|
||||
fn into_json(self) -> Json {
|
||||
match self {
|
||||
Lang::Fixed(s) => Json::Str(s.to_string()),
|
||||
Lang::Mixed(rust, go) => {
|
||||
Json::Str(format!("mixed (rust {rust} / go {go} members)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── _normalize_discovery_ids ─────────────────────────────────────────────
|
||||
|
||||
/// Migrate legacy unqualified contract discoveries to construction-surface
|
||||
/// kind without losing evidence; demote unsupported PRESENT claims.
|
||||
/// Mutates the doc IN PLACE; returns the rename count.
|
||||
pub fn normalize_discovery_ids(doc: &mut Json, discoveries: &Discoveries) -> Result<i64, String> {
|
||||
let observed: std::collections::HashSet<&str> = discoveries
|
||||
.iter()
|
||||
.flat_map(|(_, items)| items.iter())
|
||||
.filter(|(_, _, prov)| prov == "observed")
|
||||
.map(|(eid, _, _)| eid.as_str())
|
||||
.collect();
|
||||
let entities = match obj_get_mut(doc, "entities") {
|
||||
Some(Json::Arr(a)) => a,
|
||||
Some(_) => return Err("AttributeError: entities is not a list".into()),
|
||||
None => return Ok(0),
|
||||
};
|
||||
let mut renamed: Vec<(String, String)> = vec![];
|
||||
for e in entities.iter_mut() {
|
||||
let kind = as_str(obj_get(e, "kind"));
|
||||
let eid = as_str(obj_get(e, "id"));
|
||||
let has_discovery_ref = match obj_get(e, "evidence_refs") {
|
||||
Some(Json::Arr(refs)) => refs.iter().any(|r| {
|
||||
matches!(r, Json::Str(s) if s.starts_with("contract-discovery:"))
|
||||
}),
|
||||
_ => false,
|
||||
};
|
||||
if kind == "module" && !eid.contains(':') && has_discovery_ref && !eid.is_empty() {
|
||||
// Python would KeyError on a missing id here (e["id"] after the
|
||||
// colon check passes for ""); the empty guard keeps the Rust
|
||||
// surface total for that pathological input.
|
||||
let new_id = format!("construction-surface:{eid}");
|
||||
obj_set(e, "id", Json::Str(new_id.clone()));
|
||||
obj_set(e, "kind", Json::Str("construction-surface".into()));
|
||||
if obj_get(e, "identities").is_none() {
|
||||
obj_set(e, "identities", Json::Obj(vec![]));
|
||||
}
|
||||
if let Some(ids) = obj_get_mut(e, "identities") {
|
||||
obj_set(ids, "source_ref", Json::Str(eid.clone()));
|
||||
}
|
||||
renamed.push((eid, new_id));
|
||||
}
|
||||
let kind = as_str(obj_get(e, "kind"));
|
||||
let eid = as_str(obj_get(e, "id"));
|
||||
if kind == "construction-surface" {
|
||||
if let Some((_, suffix)) = eid.split_once(':') {
|
||||
if !observed.contains(suffix) {
|
||||
obj_set(e, "maturity", Json::Str("DECLARED".into()));
|
||||
}
|
||||
}
|
||||
let tags: Vec<Json> = match obj_get(e, "tags") {
|
||||
Some(Json::Arr(t)) => t
|
||||
.iter()
|
||||
.filter(|x| !matches!(x, Json::Str(s) if s == "code"))
|
||||
.cloned()
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
obj_set(e, "tags", Json::Arr(tags));
|
||||
}
|
||||
}
|
||||
let n = renamed.len() as i64;
|
||||
if let Some(Json::Arr(rels)) = obj_get_mut(doc, "relationships") {
|
||||
for rel in rels.iter_mut() {
|
||||
for endpoint in ["from", "to"] {
|
||||
let cur = as_str(obj_get(rel, endpoint));
|
||||
if let Some((_, new)) = renamed.iter().find(|(old, _)| *old == cur) {
|
||||
obj_set(rel, endpoint, Json::Str(new.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
// ── _apply_contract_discoveries ──────────────────────────────────────────
|
||||
|
||||
/// Contract-discovery write-back: entities DISCOVERED by closed contracts
|
||||
/// become modeled spine entities (observed → PRESENT, candidate →
|
||||
/// DECLARED). Mutates the doc IN PLACE; returns the count of NEW entities
|
||||
/// (high-water merges still carry evidence but don't count).
|
||||
pub fn apply_contract_discoveries(doc: &mut Json, discoveries: &Discoveries) -> Result<i64, String> {
|
||||
let mut placed = 0i64;
|
||||
for (cid, items) in discoveries {
|
||||
for (eid, kind, provenance) in items {
|
||||
let before = match obj_get(doc, "entities") {
|
||||
Some(Json::Arr(a)) => a.len() as i64,
|
||||
Some(_) => return Err("KeyError: 'entities' is not a list".into()),
|
||||
None => return Err("KeyError: 'entities'".into()),
|
||||
};
|
||||
let full_eid = if kind == "construction-surface" {
|
||||
format!("construction-surface:{eid}")
|
||||
} else {
|
||||
eid.clone()
|
||||
};
|
||||
let identities = if kind == "construction-surface" {
|
||||
let source_ref = full_eid.split_once(':').map(|(_, r)| r).unwrap_or("");
|
||||
Some(vec![(
|
||||
"source_ref".to_string(),
|
||||
Json::Str(source_ref.to_string()),
|
||||
)])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let maturity = if provenance == "observed" { "PRESENT" } else { "DECLARED" };
|
||||
let refs = vec![format!("contract-discovery:{cid}")];
|
||||
add_entity(
|
||||
doc,
|
||||
&full_eid,
|
||||
kind,
|
||||
&["contract-discovery"],
|
||||
identities,
|
||||
&refs,
|
||||
maturity,
|
||||
);
|
||||
let after = match obj_get(doc, "entities") {
|
||||
Some(Json::Arr(a)) => a.len() as i64,
|
||||
_ => before,
|
||||
};
|
||||
if after > before {
|
||||
placed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(placed)
|
||||
}
|
||||
+43190
-1227
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-12T03:02:45Z-bd521d-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/openai_compat_transport.py to FULL Go (dichotomy factory, issue #2479 tranche): name-preserving port in src/go/openai_compat_transport/, IO behind injectable seams (HTTP poster, config/auth/env), frozen-truth battery replaying every public function, live parity 100%, Phase B language-advantage pass (goroutines at serial IO seams, interfaces, compile-time safety).
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T03:03:44Z", "contract": "BUILDER-2026-10-12T03:02:45Z-bd521d-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/gen_openai_compat_transport_truth.py", "src/go/openai_compat_transport/openai_compat_transport.go", "src/go/openai_compat_transport/phaseb.go", "src/go/openai_compat_transport/phaseb_test.go", "src/go/openai_compat_transport/pyjson.go", "src/go/openai_compat_transport/replay.go", "src/go/openai_compat_transport/truth.json", "src/go/openai_compat_transport/truth_test.go", "src/rune/openai_compat_transport.py"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-12T09:54:16Z-77fa9d-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/behaviour_ledger.py to FULL Rust (dichotomy factory, issue #2479 tranche): doc-envelope FFI, ctypes loader, facade delegation with RUNE_BL_CORE=python pin, frozen-truth battery, live parity 100%, Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T11:57:19Z", "contract": "BUILDER-2026-10-12T09:54:16Z-77fa9d-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/check_behaviour_ledger_parity.py", "scripts/gen_behaviour_ledger_truth.py", "src/rune/behaviour_ledger.py", "src/rune/bl_loader.py", "src/rust/behaviour_ledger/src/branch_battery.rs", "src/rust/behaviour_ledger/src/frozen_bl_truth.json", "src/rust/behaviour_ledger/src/lib.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,27 @@
|
||||
# Contract BUILDER-2026-10-12T10:14:24Z-3f84ac-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port construction_pattern to FULL Rust — parity + Phase B language-advantage pass (dichotomy wave #2479 tranche)
|
||||
|
||||
## What it connects to
|
||||
|
||||
- `src/rune/construction_pattern.py`
|
||||
- `src/rune/cp_loader.py`
|
||||
- `src/rust/construction_pattern`
|
||||
- `tests/test_construction_pattern.py`
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T10:15:24Z", "contract": "BUILDER-2026-10-12T10:14:24Z-3f84ac-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": ["test:construction_pattern"], "expected_new_missing": ["test:construction_pattern"], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:construction_pattern", "module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:construction_pattern", "module:deploy_qualify"], "predicted_existing_affected": ["module:construction_pattern", "module:deploy_qualify"]}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-12T10:31:15Z-0436f5-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/experience_encoding.py to FULL Rust (dichotomy factory, issue #2479 tranche): doc-envelope FFI, ctypes loader, facade delegation with RUNE_EE_CORE=python pin, frozen-truth battery, live parity 100%, Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T10:32:15Z", "contract": "BUILDER-2026-10-12T10:31:15Z-0436f5-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/gen_experience_encoding_truth.py", "src/rune/ee_loader.py", "src/rune/experience_encoding.py", "src/rust/experience_encoding/src/branch_battery.rs", "src/rust/experience_encoding/src/lib.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-12T12:12:17Z-a146ec-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/abstraction_ladder.py to FULL Rust (dichotomy factory, issue #2479 tranche): doc-envelope FFI, ctypes loader, facade delegation with RUNE_AL_CORE=python pin, frozen-truth battery, live parity 100%, Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T12:13:20Z", "contract": "BUILDER-2026-10-12T12:12:17Z-a146ec-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/gen_abstraction_ladder_truth.py", "src/rune/abstraction_ladder.py", "src/rune/al_loader.py", "src/rust/abstraction_ladder/src/branch_battery.rs", "src/rust/abstraction_ladder/src/ladder.rs", "src/rust/abstraction_ladder/src/lib.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-12T12:50:19Z-9317b1-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/spine_language.py to FULL Rust (dichotomy factory, issue #2479 tranche): doc-level transforms (#2465 language placement, discovery-ID normalization, contract-discovery write-back), doc-envelope FFI, ctypes loader, facade delegation with RUNE_SL_CORE=python pin, frozen-truth battery, live parity 100%, Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T12:51:18Z", "contract": "BUILDER-2026-10-12T12:50:19Z-9317b1-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/gen_spine_language_truth.py", "src/rune/sl_loader.py", "src/rune/spine_language.py", "src/rust/spine_language/frozen_sl_truth.json", "src/rust/spine_language/src/branch_battery.rs", "src/rust/spine_language/src/lib.rs", "src/rust/spine_language/src/spine.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
@@ -0,0 +1,24 @@
|
||||
# Contract BUILDER-2026-10-12T12:56:54Z-2ab0e0-factory-wave
|
||||
|
||||
**Phase:** closed · **Mode:** n/a · **Depth:** n/a
|
||||
|
||||
## What was built
|
||||
|
||||
Port src/rune/mutation_guards.py to FULL Rust (dichotomy factory, issue #2479 tranche): doc-envelope FFI, ctypes loader, facade delegation with RUNE_MG_CORE=python pin, frozen-truth battery, live parity 100%, Phase B language-advantage pass.
|
||||
|
||||
## What it connects to
|
||||
|
||||
(no affected surface recorded)
|
||||
|
||||
## How to update those connections
|
||||
|
||||
- Open a NEW construction contract (scaffolding is absolute) scoped to the same surface.
|
||||
- Evidence trail: 4 checkpoint(s) recorded on the contract entity.
|
||||
- Quality at close: {"candidate_corrections": [], "captured_at": "2026-10-12T12:57:58Z", "contract": "BUILDER-2026-10-12T12:56:54Z-2ab0e0-factory-wave", "correctly_predicted": [], "evidence_sources": ["unattributed"], "expected_new_entities": [], "expected_new_missing": [], "expected_new_observed": [], "expected_runtime_effects": [], "expected_runtime_missing": [], "expected_runtime_observed": [], "genuinely_unexpected_new": [], "missed": [], "needless": ["module:deploy_qualify"], "observed": [], "observed_existing": [], "observed_new": [], "predicted_but_unaffected": ["module:deploy_qualify"], "predicted_existing_affected": ["module:deploy_qualify"], "scope_check": {"in_scope": [], "out_of_scope": ["scripts/gen_mutation_guards_truth.py", "src/rune/mu_loader.py", "src/rune/mutation_guards.py", "src/rust/mutation_guards/src/branch_battery.rs", "src/rust/mutation_guards/src/lib.rs"], "reason": "contract has no approved scope", "verdict": "unknown"}}.
|
||||
|
||||
## Evidence
|
||||
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- builder:factory-wave
|
||||
- closure-record
|
||||
Reference in new issue
Block a user