Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d0560ed5 | ||
|
|
4f0556fe16 | ||
|
|
f61a4a3fa3 | ||
|
|
89b691b210 | ||
|
|
03ef8132a2 | ||
|
|
e29cc5ee51 | ||
|
|
8ed117abe2 | ||
|
|
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 | ||
|
|
5501163ec5 | ||
|
|
8f4b4b4414 | ||
|
|
4eed73b551 | ||
|
|
5ac385832f | ||
|
|
3c57ae0326 | ||
|
|
2e3b7e76ec | ||
|
|
ee411f8e6e | ||
|
|
9e6bc4b3c7 | ||
|
|
31e87e612b | ||
|
|
dc20b9b295 | ||
|
|
797bb63f3c | ||
|
|
04583cd26a | ||
|
|
1494c1a5db | ||
|
|
bcc839e78b | ||
|
|
d980d486a7 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.228.0**
|
||||
**Current release: v0.239.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.228.0"
|
||||
version = "0.239.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)
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave construction contract for
|
||||
construction_patterns_builtin. 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/construction_patterns_builtin.py to FULL Rust (dichotomy "
|
||||
"factory, issue #2479 tranche): frozen-oracle registry data, doc-envelope "
|
||||
"FFI, ctypes loader (free via capability_scoring cs_free_string), facade "
|
||||
"delegation with RUNE_CPB_CORE=python pin, frozen-truth branch battery, "
|
||||
"live parity 100%, Phase B typed-registry pass. Plus audit fix: curator "
|
||||
"-> curator-core alias (curator was FULL-Go since a95c6ce1 but the audit "
|
||||
"miscounted it NOT TOUCHED).",
|
||||
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": "construction_patterns_builtin parity port — final SHAs after "
|
||||
"rebase: parity 4649df1c, phase B b50e8553. Battery 19 cases "
|
||||
"green, live parity 16/16, clippy 0.",
|
||||
}], scope_files=[
|
||||
"src/rune/construction_patterns_builtin.py",
|
||||
"src/rune/cpb_loader.py",
|
||||
"src/rust/construction_patterns_builtin/src/lib.rs",
|
||||
"src/rust/construction_patterns_builtin/src/registry.rs",
|
||||
"src/rust/construction_patterns_builtin/src/branch_battery.rs",
|
||||
"src/rust/construction_patterns_builtin/frozen_cpb_patterns.json",
|
||||
"src/rust/construction_patterns_builtin/frozen_cpb_truth.json",
|
||||
"scripts/gen_construction_patterns_builtin_truth.py",
|
||||
"scripts/check_cpb_parity.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open + walk + close the #2479 dichotomy-wave construction contract for
|
||||
drift_ledger. Full lifecycle in ONE process (shared-store clobber hazard,
|
||||
see notes-construction-intent-batch.md)."""
|
||||
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(
|
||||
"Complete the drift_ledger dichotomy port to FULL Rust (#2479 tranche): "
|
||||
"check/alert-loop, snapshot/anchor assembly, auto-fix routing and fix "
|
||||
"gate as doc-envelope FFI; dl_loader ctypes loader mirroring "
|
||||
"broker_loader.py (free via capability_scoring cs_free_string); facade "
|
||||
"delegation with RUNE_DL_CORE=python pin; frozen-oracle battery (87 "
|
||||
"cases, every public function/branch); live parity 100%; Phase B "
|
||||
"language-advantage pass; dichotomy-audit instrument fixes (curator "
|
||||
"alias + hyphen-twin mapping).",
|
||||
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": "drift_ledger dichotomy completion — parity c0ec3135 "
|
||||
"feat(drift_ledger): port remaining cores to FULL Rust — "
|
||||
"dichotomy wave; Phase B 62e47b38 refactor(drift_ledger): "
|
||||
"Phase B language-advantage pass — dichotomy wave (both pushed "
|
||||
"gitea/master). Battery 8 tests replay the 87-case frozen "
|
||||
"oracle truth VERBATIM; live parity 87/87 byte-identical "
|
||||
"(RUNE_DL_CORE=auto vs =python); clippy -D warnings clean; "
|
||||
"Python tests green (22 passed on drift-touching suites; 6 "
|
||||
"pre-existing unrelated failures on the clean tree). Audit: "
|
||||
"drift-ledger 14/14 FULL, drift_ledger 20/22 FULL, curator "
|
||||
"40/43 FULL after the instrument fixes.",
|
||||
}], scope_files=[
|
||||
"src/rune/drift_ledger.py",
|
||||
"src/rune/dl_loader.py",
|
||||
"src/rust/drift_ledger/src/lib.rs",
|
||||
"src/rust/drift_ledger/src/branch_battery.rs",
|
||||
"scripts/gen_drift_ledger_truth.py",
|
||||
"scripts/dichotomy_audit.py",
|
||||
"docs/dichotomy-rebase-honest-audit.md",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave forge_client contract with SHA evidence.
|
||||
Run on top of CURRENT master (a worktree store close never reaches master
|
||||
by itself — build_basis/message_queue precedent)."""
|
||||
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
|
||||
|
||||
CID = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
PARITY_SHA = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
PHASEB_SHA = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||
if not (CID and PARITY_SHA and PHASEB_SHA):
|
||||
sys.exit("usage: close_contract_forge_client.py <contract_id> <parity_sha> <phaseb_sha>")
|
||||
|
||||
iface = ConstructionInterface(builder="factory-wave")
|
||||
|
||||
c = iface.close(CID, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": (
|
||||
f"forge_client GO dichotomy port — parity commit {PARITY_SHA}, "
|
||||
f"Phase B commit {PHASEB_SHA}; parity 49/49 frozen rows vs live "
|
||||
"oracle (scripts/gen_forge_client_truth.py), go test -race green, "
|
||||
"gofmt/vet clean, Python oracle tests 18 passed."
|
||||
),
|
||||
}], scope_files=[
|
||||
"src/rune/forge_client.py",
|
||||
"src/go/forge_client/forge_client.go",
|
||||
"src/go/forge_client/phaseb.go",
|
||||
"src/go/forge_client/replay.go",
|
||||
"src/go/forge_client/main.go",
|
||||
"src/go/forge_client/truth_test.go",
|
||||
"src/go/forge_client/phaseb_test.go",
|
||||
"src/go/forge_client/truth.json",
|
||||
"src/go/forge_client/go.mod",
|
||||
"scripts/gen_forge_client_truth.py",
|
||||
"scripts/open_contract_forge_client.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", CID, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -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")
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Truth generator for the forge_client dichotomy port (#2479 tranche, GO).
|
||||
|
||||
Runs the LIVE Python oracle (src/rune/forge_client.py) with the httpx
|
||||
transport fully scripted, exercises every public function and every branch,
|
||||
and freezes inputs + expected outputs to src/go/forge_client/truth.json
|
||||
(consumed VERBATIM by the Go battery).
|
||||
|
||||
No clock, no store, no env leakage: every case is hermetic — the transport
|
||||
is a scripted fake (never a real socket), sleep is a recording fake, and
|
||||
os.environ is snapshotted/restored around every selection case.
|
||||
|
||||
Scripted entry shapes:
|
||||
{"status": N, "payload": {...}} → FakeResponse(N, payload)
|
||||
{"exc": "connect" | "connect_timeout" | "read_timeout"} → scripted
|
||||
httpx exception (connect-phase vs non-connect transport, the
|
||||
NOISE-vs-SIGNAL axis of _request).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
OUT = REPO / "src" / "go" / "forge_client" / "truth.json"
|
||||
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
import rune.forge_client as fc # noqa: E402
|
||||
|
||||
EXC = {
|
||||
"connect": httpx.ConnectError,
|
||||
"connect_timeout": httpx.ConnectTimeout,
|
||||
"read_timeout": httpx.ReadTimeout,
|
||||
}
|
||||
|
||||
rows = []
|
||||
coverage = set()
|
||||
|
||||
|
||||
def add(name, op, inp, want, covered):
|
||||
rows.append({"name": name, "op": op, "input": inp, "want": want})
|
||||
coverage.update(covered)
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status=200, payload=None):
|
||||
self.status_code = status
|
||||
self._payload = payload if payload is not None else {}
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise httpx.HTTPStatusError(
|
||||
f"error {self.status_code}",
|
||||
request=httpx.Request("GET", "https://x"),
|
||||
response=httpx.Response(self.status_code),
|
||||
)
|
||||
|
||||
|
||||
def make_fake(method, script, rec):
|
||||
pending = list(script)
|
||||
|
||||
def fake(url, **kw):
|
||||
rec["requests"].append(
|
||||
{
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": kw.get("headers"),
|
||||
"json": kw.get("json"),
|
||||
"timeout": kw.get("timeout"),
|
||||
}
|
||||
)
|
||||
if not pending:
|
||||
raise AssertionError(f"script exhausted at request {len(rec['requests'])}")
|
||||
entry = pending.pop(0)
|
||||
if "exc" in entry:
|
||||
raise EXC[entry["exc"]]("scripted")
|
||||
return FakeResponse(entry.get("status", 200), entry.get("payload"))
|
||||
|
||||
return fake
|
||||
|
||||
|
||||
def blank_rec():
|
||||
return {"requests": [], "sleeps": [], "error": None}
|
||||
|
||||
|
||||
def finish_rec(rec, result):
|
||||
return {
|
||||
"result": result,
|
||||
"requests": rec["requests"],
|
||||
"sleeps": rec["sleeps"],
|
||||
"error": rec["error"],
|
||||
}
|
||||
|
||||
|
||||
def call_case(name, method_name, script, kwargs, *, url="https://forge.example.com",
|
||||
token="tok", owner="o", repo="r", covered=()):
|
||||
rec = blank_rec()
|
||||
adapter = fc.GiteaForgejoAdapter(url, token, owner=owner, repo=repo)
|
||||
with patch("rune.forge_client.httpx.get", make_fake("GET", script, rec)), patch(
|
||||
"rune.forge_client.httpx.post", make_fake("POST", script, rec)
|
||||
), patch("rune.forge_client.time.sleep", rec["sleeps"].append):
|
||||
try:
|
||||
result = getattr(adapter, method_name)(**kwargs)
|
||||
except Exception as e: # noqa: BLE001 — the kind IS the information
|
||||
rec["error"] = f"httpx.{type(e).__name__}"
|
||||
result = None
|
||||
add(
|
||||
name,
|
||||
"call",
|
||||
{
|
||||
"method": method_name,
|
||||
"url": url,
|
||||
"token": token,
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"script": script,
|
||||
"kwargs": kwargs,
|
||||
},
|
||||
finish_rec(rec, result),
|
||||
("GiteaForgejoAdapter." + method_name,) + covered,
|
||||
)
|
||||
|
||||
|
||||
def request_case(name, method, script, *, headers=None, json_body=None, timeout=30,
|
||||
retries=2, backoff=0.5, covered=()):
|
||||
rec = blank_rec()
|
||||
with patch("rune.forge_client.httpx.get", make_fake("GET", script, rec)), patch(
|
||||
"rune.forge_client.httpx.post", make_fake("POST", script, rec)
|
||||
):
|
||||
try:
|
||||
result = fc._request(
|
||||
method,
|
||||
"https://x/api",
|
||||
headers=headers,
|
||||
json=json_body,
|
||||
timeout=timeout,
|
||||
retries=retries,
|
||||
backoff=backoff,
|
||||
sleep=rec["sleeps"].append,
|
||||
).json()
|
||||
except Exception as e: # noqa: BLE001
|
||||
rec["error"] = f"httpx.{type(e).__name__}"
|
||||
result = None
|
||||
add(
|
||||
name,
|
||||
"request",
|
||||
{
|
||||
"method": method,
|
||||
"url": "https://x/api",
|
||||
"headers": headers,
|
||||
"json": json_body,
|
||||
"timeout": timeout,
|
||||
"retries": retries,
|
||||
"backoff": backoff,
|
||||
"script": script,
|
||||
},
|
||||
finish_rec(rec, result),
|
||||
("_request",) + covered,
|
||||
)
|
||||
|
||||
|
||||
def select_case(name, config, env, covered=()):
|
||||
saved = {k: os.environ.get(k) for k in
|
||||
("FORGE_URL", "FORGE_TOKEN", "FORGE_OWNER", "FORGE_REPO")}
|
||||
try:
|
||||
for k in ("FORGE_URL", "FORGE_TOKEN", "FORGE_OWNER", "FORGE_REPO"):
|
||||
os.environ.pop(k, None)
|
||||
os.environ.update({k: v for k, v in env.items() if v is not None})
|
||||
client = fc.create_forge_client(config)
|
||||
if isinstance(client, fc.NullAdapter):
|
||||
want = {"adapter": "NullAdapter", "base_url": None, "token": None}
|
||||
else:
|
||||
want = {
|
||||
"adapter": "GiteaForgejoAdapter",
|
||||
"base_url": client._base_url,
|
||||
"token": client._token,
|
||||
}
|
||||
finally:
|
||||
for k, v in saved.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
add(name, "select", {"config": config, "env": env}, want,
|
||||
("create_forge_client",) + covered)
|
||||
|
||||
|
||||
def null_case(name, method_name, args):
|
||||
client = fc.NullAdapter()
|
||||
result = getattr(client, method_name)(*args)
|
||||
add(name, "null", {"method": method_name, "args": args}, result,
|
||||
("NullAdapter." + method_name,))
|
||||
|
||||
|
||||
# ── create_forge_client: every selection branch ─────────────────────────────
|
||||
select_case("select:none", None, {})
|
||||
select_case("select:env-configured", {}, {"FORGE_URL": "https://forge.example.com",
|
||||
"FORGE_TOKEN": "test_token"})
|
||||
select_case("select:config-keys",
|
||||
{"forge.url": "https://cfg.example.com", "forge.token": "cfg_tok",
|
||||
"forge.owner": "cfg_owner", "forge.repo": "cfg_repo"}, {})
|
||||
select_case("select:config-beats-env",
|
||||
{"forge.url": "https://cfg.example.com", "forge.token": "cfg_tok"},
|
||||
{"FORGE_URL": "https://env.example.com", "FORGE_TOKEN": "env_tok"})
|
||||
select_case("select:owner-repo-defaults", {},
|
||||
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token"})
|
||||
select_case("select:owner-repo-env", {},
|
||||
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token",
|
||||
"FORGE_OWNER": "env_owner", "FORGE_REPO": "env_repo"})
|
||||
select_case("select:trailing-slash-strip", {},
|
||||
{"FORGE_URL": "https://forge.example.com///", "FORGE_TOKEN": "test_token"})
|
||||
select_case("select:empty-cfg-url-falls-to-env",
|
||||
{"forge.url": "", "forge.token": ""},
|
||||
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token"})
|
||||
select_case("select:cfg-url-missing-token", {"forge.url": "https://cfg.example.com"}, {})
|
||||
select_case("select:cfg-token-missing-url", {"forge.token": "cfg_tok"}, {})
|
||||
select_case("select:owner-empty-env", {},
|
||||
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token",
|
||||
"FORGE_OWNER": ""})
|
||||
|
||||
# ── NullAdapter: every no-op shape ──────────────────────────────────────────
|
||||
null_case("null:create-issue", "create_issue", ["t", "b"])
|
||||
null_case("null:add-comment", "add_comment", [3, "c"])
|
||||
null_case("null:get-issue-state", "get_issue_state", [7])
|
||||
null_case("null:list-open-issues", "list_open_issues", [])
|
||||
null_case("null:search-issues", "search_issues", ["q"])
|
||||
|
||||
# ── GiteaForgejoAdapter: every method + happy paths ─────────────────────────
|
||||
call_case("call:create-issue", "create_issue",
|
||||
[{"status": 200, "payload": {"number": 42, "state": "open"}}],
|
||||
{"title": "Test", "body": "Body"})
|
||||
call_case("call:create-issue-201", "create_issue",
|
||||
[{"status": 201, "payload": {"number": 1, "title": "t"}}],
|
||||
{"title": "t", "body": "b"})
|
||||
call_case("call:create-issue-4xx", "create_issue",
|
||||
[{"status": 422, "payload": {}}], {"title": "dup", "body": "b"})
|
||||
call_case("call:add-comment", "add_comment",
|
||||
[{"status": 200, "payload": {"id": 9, "body": "c"}}],
|
||||
{"issue_number": 5, "body": "c"})
|
||||
call_case("call:get-issue-state", "get_issue_state",
|
||||
[{"status": 200, "payload": {"number": 9, "state": "open"}}],
|
||||
{"issue_number": 9})
|
||||
call_case("call:list-open-issues", "list_open_issues",
|
||||
[{"status": 200, "payload": [{"number": 1}, {"number": 2}]}], {})
|
||||
call_case("call:search-single-page", "search_issues",
|
||||
[{"status": 200, "payload": [{"number": 3, "title": "[auto-failure:cron:Connection]"}]}],
|
||||
{"query": "[auto-failure:cron:Connection]"}, covered=("GiteaForgejoAdapter.search_issues.pagination",))
|
||||
call_case("call:search-paginates", "search_issues",
|
||||
[{"status": 200, "payload": [{"number": n, "title": f"issue {n}"} for n in range(50)]},
|
||||
{"status": 200, "payload": [{"number": 100, "title": "[auto-failure:cron:Connection] cron"}]}],
|
||||
{"query": "[auto-failure:cron:Connection]"},
|
||||
covered=("GiteaForgejoAdapter.search_issues.pagination",))
|
||||
call_case("call:search-filters-title-body", "search_issues",
|
||||
[{"status": 200, "payload": [
|
||||
{"number": 1, "title": "[auto-failure:cron:Connection] cron", "body": "x"},
|
||||
{"number": 2, "title": "plain title", "body": "quoted [auto-failure:cron:Connection] in body"},
|
||||
{"number": 3, "title": "mention of connection only", "body": "unrelated"}]}],
|
||||
{"query": "[auto-failure:cron:Connection]"},
|
||||
covered=("GiteaForgejoAdapter.search_issues.client-filter",))
|
||||
call_case("call:search-no-hits", "search_issues",
|
||||
[{"status": 200, "payload": [{"number": 1, "title": "unrelated"}]}],
|
||||
{"query": "[auto-failure:cron:Connection]"})
|
||||
call_case("call:search-exact-50-then-empty", "search_issues",
|
||||
[{"status": 200, "payload": [{"number": n, "title": "noise"} for n in range(50)]},
|
||||
{"status": 200, "payload": []}],
|
||||
{"query": "[auto-failure:cron:Connection]"},
|
||||
covered=("GiteaForgejoAdapter.search_issues.pagination",))
|
||||
call_case("call:create-issue-retry-connect", "create_issue",
|
||||
[{"exc": "connect"}, {"status": 200, "payload": {"number": 7}}],
|
||||
{"title": "t", "body": "b"}, covered=("_request",))
|
||||
call_case("call:get-issue-state-retry-5xx", "get_issue_state",
|
||||
[{"status": 502}, {"status": 200, "payload": {"number": 9, "state": "open"}}],
|
||||
{"issue_number": 9}, covered=("_request",))
|
||||
call_case("call:get-issue-state-4xx", "get_issue_state",
|
||||
[{"status": 404, "payload": {}}], {"issue_number": 404}, covered=("_request",))
|
||||
|
||||
# ── _request: every retry branch (noise vs signal) ──────────────────────────
|
||||
request_case("request:get-ok", "GET", [{"status": 200, "payload": {"ok": True}}])
|
||||
request_case("request:get-retries-connect", "GET",
|
||||
[{"exc": "connect"}, {"exc": "connect"},
|
||||
{"status": 200, "payload": {"ok": True}}])
|
||||
request_case("request:get-connect-exhaust", "GET",
|
||||
[{"exc": "connect"}, {"exc": "connect"}, {"exc": "connect"}])
|
||||
request_case("request:get-4xx-no-retry", "GET", [{"status": 422}])
|
||||
request_case("request:get-5xx-retry-then-ok", "GET",
|
||||
[{"status": 502}, {"status": 503}, {"status": 200, "payload": {"n": 3}}])
|
||||
request_case("request:get-5xx-exhaust", "GET", [{"status": 502}, {"status": 503}, {"status": 504}])
|
||||
request_case("request:get-read-timeout-retries", "GET",
|
||||
[{"exc": "read_timeout"}, {"status": 200, "payload": {"ok": True}}])
|
||||
request_case("request:get-read-timeout-exhaust", "GET",
|
||||
[{"exc": "read_timeout"}, {"exc": "read_timeout"}, {"exc": "read_timeout"}])
|
||||
request_case("request:post-ok", "POST", [{"status": 200, "payload": {"a": 1}}],
|
||||
json_body={"a": 1})
|
||||
request_case("request:post-connect-retry", "POST",
|
||||
[{"exc": "connect_timeout"}, {"status": 201, "payload": {"number": 7}}],
|
||||
json_body={"a": 1})
|
||||
request_case("request:post-connect-exhaust", "POST",
|
||||
[{"exc": "connect"}, {"exc": "connect"}, {"exc": "connect"}],
|
||||
json_body={"a": 1})
|
||||
request_case("request:post-read-timeout-no-retry", "POST",
|
||||
[{"exc": "read_timeout"}], json_body={"a": 1})
|
||||
request_case("request:post-5xx-no-retry", "POST", [{"status": 502}], json_body={"a": 1})
|
||||
request_case("request:post-4xx-no-retry", "POST", [{"status": 422}], json_body={"a": 1})
|
||||
request_case("request:backoff-exponential", "GET",
|
||||
[{"exc": "connect"}, {"exc": "connect"}, {"status": 200}])
|
||||
request_case("request:custom-retries-backoff", "GET",
|
||||
[{"exc": "connect"}, {"status": 200}], retries=1, backoff=0.25)
|
||||
request_case("request:timeout-passthrough", "GET",
|
||||
[{"status": 200}], timeout=17)
|
||||
request_case("request:headers-json-capture", "POST",
|
||||
[{"status": 200, "payload": {}}],
|
||||
headers={"Authorization": "token t", "Content-Type": "application/json"},
|
||||
json_body={"title": "t", "body": "b"})
|
||||
request_case("request:lowercase-method", "post",
|
||||
[{"status": 200, "payload": {"made": "it"}}], json_body={"k": "v"})
|
||||
|
||||
# ── freeze ───────────────────────────────────────────────────────────────────
|
||||
EXPECTED = {
|
||||
"create_forge_client", "_request",
|
||||
"NullAdapter.create_issue", "NullAdapter.add_comment", "NullAdapter.get_issue_state",
|
||||
"NullAdapter.list_open_issues", "NullAdapter.search_issues",
|
||||
"GiteaForgejoAdapter.create_issue", "GiteaForgejoAdapter.add_comment",
|
||||
"GiteaForgejoAdapter.get_issue_state", "GiteaForgejoAdapter.list_open_issues",
|
||||
"GiteaForgejoAdapter.search_issues",
|
||||
}
|
||||
missing = EXPECTED - coverage
|
||||
if missing:
|
||||
sys.exit(f"COVERAGE GAP — public functions not exercised: {sorted(missing)}")
|
||||
|
||||
doc = {
|
||||
"frozen_ts": "2026-10-12T02:17:44Z",
|
||||
"oracle": "src/rune/forge_client.py",
|
||||
"rows": rows,
|
||||
}
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
print(f"frozen {len(rows)} rows → {OUT}")
|
||||
print("covered:", ", ".join(sorted(coverage)))
|
||||
@@ -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,526 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze truth for the openai_compat_transport Go port (dichotomy wave #2479).
|
||||
|
||||
Exercises EVERY public function of src/rune/openai_compat_transport.py against
|
||||
the LIVE oracle: __init__ (timeout clamp), _post (wire + non-object guard),
|
||||
_to_openai_messages (all branches), _from_openai_response (all branches),
|
||||
chat (model split, tools, options, usage), supports_tools, supports_thinking,
|
||||
context_length, is_cloud_pinned, cloud_transport_for (config/env/auth seams).
|
||||
|
||||
Wire IO served by a local stub HTTP server that captures each request
|
||||
(path, authorization, content-type, body bytes) and returns scripted bodies;
|
||||
cloud_transport_for's IO seams (os.path.expanduser, os.environ) are patched
|
||||
to temp files so config/auth resolution is deterministic.
|
||||
|
||||
Excluded branches (interpreter-specific error text — not portable truth):
|
||||
- _post on a non-2xx HTTP status (Python urllib HTTPError string vs Go
|
||||
net/http error string) and on connection failures.
|
||||
- _post on a body that is invalid JSON (Python json.JSONDecodeError text).
|
||||
- cloud_transport_for with auth.json containing invalid JSON (same reason).
|
||||
|
||||
Numbers: fixtures are written by json.dump so every numeric literal is in
|
||||
Python's canonical repr; the Go port preserves literals on parse. Floats in
|
||||
fixtures stay in the shortest-decimal band (no exponents) so literal
|
||||
preservation equals Python re-rendering.
|
||||
|
||||
Nondeterministic: usage_total_duration_ns (time.monotonic) is excluded from
|
||||
every row; the Go battery asserts it is > 0 after a chat instead.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
import rune.openai_compat_transport as oct_mod # noqa: E402
|
||||
|
||||
|
||||
# -- stub wire ---------------------------------------------------------------
|
||||
|
||||
class StubState:
|
||||
def __init__(self):
|
||||
self.script = [] # list of response body strings
|
||||
self.requests = [] # captured requests, one dict per POST
|
||||
|
||||
def reset(self, bodies):
|
||||
self.script = list(bodies)
|
||||
self.requests = []
|
||||
|
||||
|
||||
STUB = StubState()
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
STUB.requests.append({
|
||||
"path": self.path,
|
||||
"authorization": self.headers.get("Authorization"),
|
||||
"content_type": self.headers.get("Content-Type"),
|
||||
"body": body.decode("utf-8"),
|
||||
})
|
||||
resp = STUB.script.pop(0)
|
||||
data = resp.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
def start_stub():
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
return server, f"http://127.0.0.1:{server.server_address[1]}"
|
||||
|
||||
|
||||
# -- row helpers -------------------------------------------------------------
|
||||
|
||||
ROWS = []
|
||||
|
||||
|
||||
def add(name, op, setup, want):
|
||||
ROWS.append({"name": name, "op": op, "setup": setup, "want": want})
|
||||
|
||||
|
||||
def attrs(t):
|
||||
return {
|
||||
"base_url": t.base_url,
|
||||
"api_key": t.api_key,
|
||||
"provider": t.provider,
|
||||
"timeout": t.timeout,
|
||||
"context_window": t._context_window,
|
||||
"usage_calls": t.usage_calls,
|
||||
"usage_prompt_tokens": t.usage_prompt_tokens,
|
||||
"usage_output_tokens": t.usage_output_tokens,
|
||||
"usage_total_duration_ns": t.usage_total_duration_ns,
|
||||
}
|
||||
|
||||
|
||||
# -- cases -------------------------------------------------------------------
|
||||
|
||||
def case_ctor():
|
||||
add("ctor_defaults", "ctor",
|
||||
{"base_url": "https://api.example.com/v1/", "api_key": "k1", "provider": "Verboo Code"},
|
||||
attrs(oct_mod.OpenAICompatTransport("https://api.example.com/v1/", "k1", "Verboo Code")))
|
||||
add("ctor_timeout_clamped_low", "ctor",
|
||||
{"base_url": "http://x/v1", "api_key": "k", "provider": "T", "timeout": 30},
|
||||
attrs(oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", timeout=30)))
|
||||
add("ctor_timeout_clamp_boundary", "ctor",
|
||||
{"base_url": "http://x/v1", "api_key": "k", "provider": "T", "timeout": 120},
|
||||
attrs(oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", timeout=120)))
|
||||
add("ctor_timeout_above_floor_float", "ctor",
|
||||
{"base_url": "http://x/v1", "api_key": "k", "provider": "T", "timeout": 130.5, "context_window": 8192},
|
||||
attrs(oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", timeout=130.5, context_window=8192)))
|
||||
|
||||
|
||||
def case_to_messages():
|
||||
f = oct_mod.OpenAICompatTransport._to_openai_messages
|
||||
cases = [
|
||||
("tm_empty", []),
|
||||
("tm_plain_roles", [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "task"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]),
|
||||
("tm_missing_content_defaults_empty", [{"role": "user"}]),
|
||||
("tm_null_content_stays_null", [{"role": "user", "content": None}]),
|
||||
("tm_null_role", [{"content": "orphan"}]),
|
||||
("tm_assistant_empty_tool_calls_is_plain", [
|
||||
{"role": "assistant", "content": "text", "tool_calls": []},
|
||||
]),
|
||||
("tm_tool_call_dict_args", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "edit_file", "arguments": {"path": "/x.py", "old": "a", "new": "b"}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_2_0", "content": "{\"success\": true}"},
|
||||
]),
|
||||
("tm_tool_call_string_args", [
|
||||
{"role": "assistant", "content": "working", "tool_calls": [
|
||||
{"function": {"name": "read_file", "arguments": "{\"path\": \"/x.py\"}"}}]},
|
||||
]),
|
||||
("tm_tool_call_missing_args_defaults_empty_obj", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"function": {"name": "noop"}}]},
|
||||
]),
|
||||
("tm_tool_call_missing_function", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "weird"}]},
|
||||
]),
|
||||
("tm_tool_call_non_dict_args_int", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"function": {"name": "ping", "arguments": 42}}]},
|
||||
]),
|
||||
("tm_tool_call_non_dict_args_list", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"function": {"name": "bulk", "arguments": ["a", "b"]}}]},
|
||||
]),
|
||||
("tm_tool_call_null_args_str", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"function": {"name": "q", "arguments": "None"}}]},
|
||||
]),
|
||||
("tm_ids_paired_from_following_tools", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}, },
|
||||
{"function": {"name": "b", "arguments": {}}, }]},
|
||||
{"role": "tool", "tool_call_id": "call_0_0", "content": "r1"},
|
||||
{"role": "tool", "tool_call_id": "call_0_1", "content": "r2"},
|
||||
]),
|
||||
("tm_ids_fallback_when_unpaired", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}},
|
||||
{"function": {"name": "b", "arguments": {}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_0_0", "content": "r1"},
|
||||
]),
|
||||
("tm_ids_scan_nonadjacent_tools", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}},
|
||||
{"function": {"name": "b", "arguments": {}}}]},
|
||||
{"role": "user", "content": "and?"},
|
||||
{"role": "tool", "tool_call_id": "aa", "content": "r1"},
|
||||
{"role": "tool", "tool_call_id": "bb", "content": "r2"},
|
||||
]),
|
||||
("tm_paired_id_null_falls_back", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}}]},
|
||||
{"role": "tool", "content": "no id"},
|
||||
]),
|
||||
("tm_tool_message_defaults", [
|
||||
{"role": "tool", "content": "result"},
|
||||
{"role": "tool", "tool_call_id": "t9", "content": "r"},
|
||||
{"role": "tool"},
|
||||
]),
|
||||
("tm_mixed_conversation", [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "go"},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "ls", "arguments": {"path": "."}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_2_0", "content": "files"},
|
||||
{"role": "assistant", "content": "done"},
|
||||
]),
|
||||
]
|
||||
for name, messages in cases:
|
||||
add(name, "to_messages", {"messages": messages}, f(messages))
|
||||
|
||||
|
||||
def case_from_response():
|
||||
f = oct_mod.OpenAICompatTransport._from_openai_response
|
||||
cases = [
|
||||
("fr_plain_content", {"choices": [{"message": {"role": "assistant", "content": "hello"}}]}),
|
||||
("fr_missing_choices", {}),
|
||||
("fr_empty_choices", {"choices": []}),
|
||||
("fr_missing_message", {"choices": [{}]}),
|
||||
("fr_null_content_becomes_empty", {"choices": [{"message": {"content": None}}]}),
|
||||
("fr_no_tool_calls_key", {"choices": [{"message": {"content": "x", "tool_calls": []}}]}),
|
||||
("fr_null_tool_calls", {"choices": [{"message": {"content": "x", "tool_calls": None}}]}),
|
||||
("fr_tool_call_string_args", {"choices": [{"message": {"tool_calls": [
|
||||
{"id": "z", "type": "function",
|
||||
"function": {"name": "run_tests", "arguments": "{\"path\": \"t\"}"}}]}}]}),
|
||||
("fr_tool_call_malformed_args_raw", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "x", "arguments": "not-json"}}]}}]}),
|
||||
("fr_tool_call_dict_args_kept", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "y", "arguments": {"a": 1}}}]}}]}),
|
||||
("fr_tool_call_null_args_become_empty_obj", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "n", "arguments": None}}]}}]}),
|
||||
("fr_tool_call_empty_string_args_raw", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "e", "arguments": ""}}]}}]}),
|
||||
("fr_tool_call_list_args_kept", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "l", "arguments": [1, 2]}}]}}]}),
|
||||
("fr_tool_call_id_only_when_present", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "a", "arguments": "{}"}},
|
||||
{"id": "", "function": {"name": "b", "arguments": "{}"}},
|
||||
{"id": "c3", "function": {"name": "c", "arguments": "{}"}}]}}]}),
|
||||
("fr_tool_call_missing_function", {"choices": [{"message": {"tool_calls": [{}]}}]}),
|
||||
("fr_multiple_tool_calls", {"choices": [{"message": {"content": None, "tool_calls": [
|
||||
{"id": "i1", "function": {"name": "a", "arguments": "{\"x\": 1}"}},
|
||||
{"id": "i2", "function": {"name": "b", "arguments": {"y": [2]}}}]}}]}),
|
||||
("fr_choices_null_becomes_default", {"choices": None}),
|
||||
]
|
||||
for name, resp in cases:
|
||||
add(name, "from_response", {"resp": resp}, f(resp))
|
||||
|
||||
|
||||
def case_post(server_base):
|
||||
t = oct_mod.OpenAICompatTransport(server_base, "sekrit", "Prov")
|
||||
|
||||
def run(path, payload, bodies):
|
||||
STUB.reset(bodies)
|
||||
try:
|
||||
result = t._post(path, payload)
|
||||
return {"request": STUB.requests[0], "result": result}
|
||||
except ValueError as e:
|
||||
return {"request": STUB.requests[0], "error": str(e)}
|
||||
|
||||
payload = {"model": "glm-5.3-flash", "messages": [{"role": "user", "content": "hi"}], "stream": False}
|
||||
add("post_roundtrip", "post",
|
||||
{"path": "/chat/completions", "payload": payload, "bodies": [
|
||||
json.dumps({"choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 5, "completion_tokens": 7}})]},
|
||||
run("/chat/completions", payload, [json.dumps({"choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 5, "completion_tokens": 7}})]))
|
||||
add("post_non_object_list", "post",
|
||||
{"path": "/x", "payload": {"a": 1}, "bodies": ["[1, 2]"]},
|
||||
run("/x", {"a": 1}, ["[1, 2]"]))
|
||||
add("post_non_object_null", "post",
|
||||
{"path": "/x", "payload": {}, "bodies": ["null"]},
|
||||
run("/x", {}, ["null"]))
|
||||
add("post_non_object_string", "post",
|
||||
{"path": "/x", "payload": {}, "bodies": ['"text"']},
|
||||
run("/x", {}, ['"text"']))
|
||||
|
||||
|
||||
def case_chat(server_base):
|
||||
cases = [
|
||||
("chat_qualified_model_bare_on_wire", {
|
||||
"model": "Verboo Code/glm-5.3-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "ok"}}], "usage": {}}'],
|
||||
}),
|
||||
("chat_bare_model_unchanged", {
|
||||
"model": "glm-5.3-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "ok"}}]}'],
|
||||
}),
|
||||
("chat_tools_passthrough", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": [{"type": "function", "function": {"name": "ls", "description": "list", "parameters": {"type": "object"}}}],
|
||||
"options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_empty_tools_omitted", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": [], "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_options_subset", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": {"temperature": 0.7, "top_p": 0.9, "max_tokens": 1024, "seed": 7, "stop": ["END"]},
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_options_partial_and_empty", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": {"max_tokens": 0},
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_think_ignored", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None, "think": True,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_think_string_ignored", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None, "think": "high",
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_tool_call_roundtrip", {
|
||||
"model": "Verboo Code/m1", "messages": [
|
||||
{"role": "user", "content": "go"},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "edit_file", "arguments": {"path": "/x.py"}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_1_0", "content": "done"},
|
||||
],
|
||||
"tools": [{"type": "function", "function": {"name": "edit_file"}}], "options": {"temperature": 0.2},
|
||||
"responses": [json.dumps({"choices": [{"message": {"content": None, "tool_calls": [
|
||||
{"id": "call_1_0", "function": {"name": "edit_file",
|
||||
"arguments": json.dumps({"path": "/x.py"})}}]}}],
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 20}})],
|
||||
}),
|
||||
("chat_usage_string_and_float_coerced", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}], "usage": {"prompt_tokens": "12", "completion_tokens": 3.9}}'],
|
||||
}),
|
||||
("chat_non_object_response_raises", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ["[]"],
|
||||
}),
|
||||
("chat_two_calls_accumulate_usage", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None, "calls": 2,
|
||||
"responses": [
|
||||
'{"choices": [{"message": {"content": "a"}}], "usage": {"prompt_tokens": 10, "completion_tokens": 4}}',
|
||||
'{"choices": [{"message": {"content": "b"}}], "usage": {"prompt_tokens": 7, "completion_tokens": 9}}',
|
||||
],
|
||||
}),
|
||||
]
|
||||
for name, c in cases:
|
||||
t = oct_mod.OpenAICompatTransport(server_base, "wire-key-1", "Prov")
|
||||
STUB.reset(c["responses"])
|
||||
want = {}
|
||||
try:
|
||||
for _ in range(c.get("calls", 1)):
|
||||
result = t.chat(model=c["model"], messages=c["messages"], tools=c["tools"],
|
||||
options=c["options"], think=c.get("think"))
|
||||
want["result"] = result
|
||||
except ValueError as e:
|
||||
want["error"] = str(e)
|
||||
want["requests"] = list(STUB.requests)
|
||||
want["usage"] = {"calls": t.usage_calls, "prompt_tokens": t.usage_prompt_tokens,
|
||||
"output_tokens": t.usage_output_tokens}
|
||||
add(name, "chat",
|
||||
{"base_url": "@STUB@", "api_key": "wire-key-1", "provider": "Prov",
|
||||
"model": c["model"], "messages": c["messages"], "tools": c["tools"],
|
||||
"options": c["options"], "think": c.get("think"), "calls": c.get("calls", 1),
|
||||
"responses": c["responses"]},
|
||||
want)
|
||||
|
||||
|
||||
def case_scalars():
|
||||
t = oct_mod.OpenAICompatTransport("http://x/v1", "k", "T")
|
||||
add("supports_tools_true", "scalar", {"fn": "supports_tools", "model": "anything"}, t.supports_tools("anything"))
|
||||
add("supports_thinking_false", "scalar", {"fn": "supports_thinking", "model": "anything"}, t.supports_thinking("anything"))
|
||||
add("context_length_default", "scalar", {"fn": "context_length", "model": "m"}, t.context_length("m"))
|
||||
t2 = oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", context_window=400000)
|
||||
add("context_length_custom", "scalar", {"fn": "context_length", "model": "m", "context_window": 400000}, t2.context_length("m"))
|
||||
|
||||
icp = oct_mod.is_cloud_pinned
|
||||
pinned = [
|
||||
("icp_cloud_provider", "Verboo Code/glm-5.3-flash", True),
|
||||
("icp_ollama_prefix", "ollama/qwen2.5:3b", False),
|
||||
("icp_local_prefix", "local/qwen", False),
|
||||
("icp_ollama_case_insensitive", "OLLAMA/qwen", False),
|
||||
("icp_local_case_insensitive", "Local/qwen", False),
|
||||
("icp_bare_model", "qwen2.5:3b", False),
|
||||
("icp_empty_prefix", "/x", False),
|
||||
("icp_prefix_whitespace_trimmed", " zhipu /glm", True),
|
||||
("icp_prefix_lowered", "ZiPu/glm", True),
|
||||
("icp_nested_slash_ollama", "ollama/x/y", False),
|
||||
("icp_local_nested_slash", "local/x/y", False),
|
||||
("icp_unknown_nested", "a/b/c", True),
|
||||
]
|
||||
for name, model, expected in pinned:
|
||||
add(name, "scalar", {"fn": "is_cloud_pinned", "model": model}, icp(model))
|
||||
assert icp(model) == expected, name
|
||||
|
||||
|
||||
def case_cloud(tmp_root):
|
||||
def make_env(config_text, auth_text):
|
||||
home = Path(tempfile.mkdtemp(dir=tmp_root))
|
||||
cfg_dir = home / ".config" / "mimocode"
|
||||
auth_dir = home / ".local" / "share" / "mimocode"
|
||||
cfg_dir.mkdir(parents=True)
|
||||
auth_dir.mkdir(parents=True)
|
||||
if config_text is not None:
|
||||
(cfg_dir / "mimocode.jsonc").write_text(config_text)
|
||||
if auth_text is not None:
|
||||
(auth_dir / "auth.json").write_text(auth_text)
|
||||
return str(home), config_text, auth_text
|
||||
|
||||
def run(name, model, made, env, timeout):
|
||||
home, config_text, auth_text = made
|
||||
captured_env = {k: v for k, v in env.items()}
|
||||
real_expand = os.path.expanduser
|
||||
|
||||
def fake_expand(p):
|
||||
if p == "~":
|
||||
return home
|
||||
if p.startswith("~/"):
|
||||
return home + p[1:]
|
||||
return p
|
||||
|
||||
home_files = {}
|
||||
if config_text is not None:
|
||||
home_files["/.config/mimocode/mimocode.jsonc"] = config_text
|
||||
if auth_text is not None:
|
||||
home_files["/.local/share/mimocode/auth.json"] = auth_text
|
||||
os.path.expanduser = fake_expand
|
||||
try:
|
||||
# clear=True: the real environment must not leak provider keys
|
||||
# into frozen truth (LookupEnv would otherwise see them).
|
||||
with mock.patch.dict(os.environ, captured_env, clear=True):
|
||||
try:
|
||||
t = oct_mod.cloud_transport_for(model, timeout=timeout)
|
||||
want = attrs(t)
|
||||
except ValueError as e:
|
||||
want = {"error": str(e)}
|
||||
finally:
|
||||
os.path.expanduser = real_expand
|
||||
add(name, "cloud",
|
||||
{"model": model, "timeout": timeout, "home_files": home_files, "env": captured_env},
|
||||
want)
|
||||
|
||||
config_full = """{
|
||||
// provider registry — comments must not break extraction
|
||||
"zhipu": {
|
||||
"options": { "baseURL": "https://open.zhipu/v1" },
|
||||
"models": {
|
||||
"glm-5.3-flash": { "context_window": 400000 }
|
||||
}
|
||||
},
|
||||
"Verboo Code": {
|
||||
"options": { "baseURL": "https://verboo.example/v1" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
run("cloud_full_config_env_key", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {"ZHIPU_API_KEY": "env-key-1"}, 120)
|
||||
run("cloud_auth_json_key", "Verboo Code/m1",
|
||||
make_env(config_full, json.dumps({"Verboo Code": {"key": "auth-key-9"}})), {}, 120)
|
||||
run("cloud_env_empty_falls_to_auth", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, json.dumps({"zhipu": {"key": "auth-key-2"}})),
|
||||
{"ZHIPU_API_KEY": ""}, 120)
|
||||
run("cloud_no_key_raises", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {}, 120)
|
||||
run("cloud_no_config_raises_baseurl", "zhipu/glm-5.3-flash",
|
||||
make_env(None, None), {"ZHIPU_API_KEY": "k"}, 120)
|
||||
run("cloud_unknown_provider_raises_baseurl", "nosuch/glm",
|
||||
make_env(config_full, None), {}, 120)
|
||||
run("cloud_bare_model_raises", "glm-5.3-flash",
|
||||
make_env(config_full, None), {}, 120)
|
||||
run("cloud_timeout_floored_at_300", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {"ZHIPU_API_KEY": "k"}, 50)
|
||||
run("cloud_timeout_above_floor_kept", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {"ZHIPU_API_KEY": "k"}, 500)
|
||||
run("cloud_model_without_context_entry_default_window", "Verboo Code/m1",
|
||||
make_env(config_full, None), {}, 120)
|
||||
|
||||
# baseURL beyond the 2000-char block window → not found
|
||||
far_block = '{"big": {"options": {' + '"pad": "' + "x" * 2100 + '", ' + '"baseURL": "https://far/v1"}}}'
|
||||
run("cloud_baseurl_beyond_block_window", "big/m",
|
||||
make_env(far_block, None), {}, 120)
|
||||
|
||||
|
||||
# -- main --------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
out_path = ROOT / "src" / "go" / "openai_compat_transport" / "truth.json"
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
server, base = start_stub()
|
||||
try:
|
||||
case_ctor()
|
||||
case_to_messages()
|
||||
case_from_response()
|
||||
case_post(base)
|
||||
case_chat(base)
|
||||
case_scalars()
|
||||
tmp_root = tempfile.mkdtemp(prefix="oct_truth_")
|
||||
case_cloud(tmp_root)
|
||||
finally:
|
||||
server.shutdown()
|
||||
|
||||
doc = {
|
||||
"oracle": "src/rune/openai_compat_transport.py",
|
||||
"generator": "scripts/gen_openai_compat_transport_truth.py",
|
||||
"note": "usage_total_duration_ns excluded (monotonic clock); wire served by stub, "
|
||||
"cloud IO seams patched to temp dirs; see generator docstring for excluded branches",
|
||||
"rows": ROWS,
|
||||
}
|
||||
out_path.write_text(json.dumps(doc, indent=1) + "\n")
|
||||
ops = {}
|
||||
for r in ROWS:
|
||||
ops[r["op"]] = ops.get(r["op"], 0) + 1
|
||||
print(f"frozen {len(ROWS)} rows → {out_path}")
|
||||
print("ops:", json.dumps(ops, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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,453 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_worthiness_truth.py — freeze/replay parity truth for worthiness
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/worthiness.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output, ops) triples into
|
||||
src/rust/worthiness/frozen_wr_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation ACTIVE (RUNE_WR_CORE unpinned) and asserts both kernel-level
|
||||
(loader direct) and facade-level (oracle functions delegate) parity
|
||||
against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_WR_CORE=python BEFORE any import.
|
||||
Mutation law: case inputs are deep-copied BEFORE they reach the oracle.
|
||||
|
||||
DOMAIN-CACHE LAW (the oracle's aliasing quirk, frozen here): q_assistant_goal
|
||||
does `assist = _domain_terms()` — the CACHED list object — then
|
||||
`assist += [universal...]`, which mutates the cache IN PLACE. Hit counts
|
||||
therefore inflate on every call within the 300 s TTL. The kernel mirrors
|
||||
this exactly: it owns the cache, fetches raw store terms through the
|
||||
`domain_terms` op ONLY on the oracle's miss condition
|
||||
(`now - ts >= 300 or not terms`), then appends the universal terms to the
|
||||
cached list on EVERY q_assistant_goal call. Each frozen case carries the
|
||||
op sequence the kernel must issue (replay script for the Rust battery);
|
||||
the shadow cache in this generator predicts those ops by mirroring the
|
||||
oracle's cache evolution.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "worthiness", "frozen_wr_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_WR_CORE"] = "python" # poisoned-truth law (freeze side)
|
||||
else:
|
||||
os.environ.pop("RUNE_WR_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 worthiness as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
from rune import wr_loader # noqa: E402 (kernel-level replay side)
|
||||
if not wr_loader.available():
|
||||
print("REPLAY ABORT: kernel not built —", wr_loader.LIB_PATH)
|
||||
sys.exit(2)
|
||||
|
||||
# The universal assistant terms q_assistant_goal appends (oracle lines 130-132).
|
||||
UNIVERSAL = ["communicat", "respond", "direct", "honest", "source",
|
||||
"evidence", "explain", "teach", "assist", "guide", "recall",
|
||||
"organise", "organize", "plan", "question", "answer", "help"]
|
||||
|
||||
# Shadow of the oracle's _domain_cache (and the kernel's mirror of it).
|
||||
_shadow = {"ts": None, "terms": []}
|
||||
|
||||
|
||||
def _fresh_domain_terms():
|
||||
"""Verbatim body of oracle _domain_terms WITHOUT the cache — this is
|
||||
exactly what the `domain_terms` op serves against the live store."""
|
||||
terms = []
|
||||
try:
|
||||
import memory_env
|
||||
db = sqlite3.connect(f"file:{memory_env.store_db()}?mode=ro", uri=True)
|
||||
rows = db.execute(
|
||||
"SELECT text FROM entries WHERE topic IN "
|
||||
"('operating','constitution','project','warm') AND status='active' "
|
||||
"LIMIT 60").fetchall()
|
||||
db.close()
|
||||
stop = {"the", "and", "for", "with", "that", "this", "from", "into",
|
||||
"when", "then", "were", "have", "been", "will", "was", "are",
|
||||
"but", "not", "you", "your", "also", "its", "his", "her",
|
||||
"over", "under", "them", "they", "there", "about", "after",
|
||||
"what", "which", "who", "how", "why", "where", "while", "just",
|
||||
"more", "each", "than", "very", "such", "some", "only"}
|
||||
seen = set()
|
||||
for (text,) in rows:
|
||||
for w in re.findall(r"[a-z]{4,}", (text or "").lower()):
|
||||
if w not in stop and w not in seen:
|
||||
seen.add(w)
|
||||
terms.append(w)
|
||||
if len(terms) >= 25:
|
||||
break
|
||||
except Exception: # noqa: BLE001 — store-unavailable degrades to [] (oracle law)
|
||||
pass
|
||||
return terms
|
||||
|
||||
|
||||
def _shadow_touch(ops):
|
||||
"""Mirror the kernel's domain-cache evolution for one q_assistant_goal
|
||||
execution; record the ops the kernel will issue for this case."""
|
||||
now = time.time()
|
||||
fresh = (_shadow["ts"] is not None and now - _shadow["ts"] < 300
|
||||
and _shadow["terms"])
|
||||
if not fresh:
|
||||
result = _fresh_domain_terms()
|
||||
ops.append({"op": "domain_terms", "args": {},
|
||||
"result": {"terms": list(result)}})
|
||||
_shadow["ts"] = now
|
||||
_shadow["terms"] = list(result)
|
||||
_shadow["terms"].extend(UNIVERSAL) # the oracle's in-place += law
|
||||
|
||||
|
||||
# ── recording ────────────────────────────────────────────────────────────────
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def q_case(name, fn_name, low):
|
||||
"""A direct question call: kernel input {"low"}, expected {"score","note"}."""
|
||||
ops = []
|
||||
if fn_name == "q_assistant_goal":
|
||||
_shadow_touch(ops)
|
||||
s, note = getattr(oracle_mod, fn_name)(copy.deepcopy(low))
|
||||
CASES.append({
|
||||
"name": name, "fn": fn_name, "input": {"low": low}, "ops": ops,
|
||||
"return": [s, note], "expected": {"score": s, "note": note},
|
||||
})
|
||||
|
||||
|
||||
def assess_case(name, fact, target=None):
|
||||
"""assess call: kernel input {"fact","target"}, expected full verdict doc."""
|
||||
ops = []
|
||||
_shadow_touch(ops) # assess ALWAYS runs q_assistant_goal
|
||||
got = oracle_mod.assess(copy.deepcopy(fact), copy.deepcopy(target))
|
||||
verdict, score, answers, vetoed = got
|
||||
CASES.append({
|
||||
"name": name, "fn": "assess",
|
||||
"input": {"fact": fact, "target": target}, "ops": ops,
|
||||
"return": [verdict, score, answers, vetoed],
|
||||
"expected": {"verdict": verdict, "score": score,
|
||||
"answers": [list(a) for a in answers],
|
||||
"vetoed": list(vetoed) if vetoed else None},
|
||||
})
|
||||
|
||||
|
||||
def label_case(name, verdict):
|
||||
got = oracle_mod.verdict_label(copy.deepcopy(verdict))
|
||||
CASES.append({
|
||||
"name": name, "fn": "verdict_label", "input": {"verdict": verdict},
|
||||
"ops": [], "return": got, "expected": got,
|
||||
})
|
||||
|
||||
|
||||
# ── q_pi_safety: all three branches ─────────────────────────────────────────
|
||||
|
||||
def c_pi():
|
||||
q_case("ps-neutral-no-environment-bearing", "q_pi_safety",
|
||||
"the weather is calm over the quiet hills")
|
||||
q_case("ps-protective-frame-aligned", "q_pi_safety",
|
||||
"we must protect the production environment and ask permission "
|
||||
"before we touch the server")
|
||||
q_case("ps-unprotected-environment-action", "q_pi_safety",
|
||||
"let's modify the server tonight without asking")
|
||||
|
||||
|
||||
# ── q_honesty: none / one / many / dishonest / dishonest+good ───────────────
|
||||
|
||||
def c_ho():
|
||||
q_case("ho-none-no-epistemic-signal", "q_honesty",
|
||||
"the sky is blue over the quiet hills")
|
||||
q_case("ho-one-signal-thin", "q_honesty",
|
||||
"there is evidence for this")
|
||||
q_case("ho-many-signals-strong", "q_honesty",
|
||||
"we verify with evidence and cite the source")
|
||||
q_case("ho-dishonest-only", "q_honesty",
|
||||
"this is a conspiracy and they're hiding the real cure")
|
||||
q_case("ho-dishonest-but-corroborated", "q_honesty",
|
||||
"conspiracy claims, but we verify with evidence and cross-check "
|
||||
"every fact")
|
||||
|
||||
|
||||
# ── q_material_reality: none / hustle / caring / neutral finance ────────────
|
||||
|
||||
def c_mr():
|
||||
q_case("mr-none-no-financial-bearing", "q_material_reality",
|
||||
"a quiet evening walk along the river")
|
||||
q_case("mr-hustle-without-wellbeing", "q_material_reality",
|
||||
"the hustle and grind to get rich: passive income, millionaire "
|
||||
"mindset, leverag everything")
|
||||
q_case("mr-caring-financial-frame", "q_material_reality",
|
||||
"the subscription cost stays affordable and fair for collective "
|
||||
"wellbeing")
|
||||
q_case("mr-neutral-finance", "q_material_reality",
|
||||
"money is simply a tool in this workflow")
|
||||
|
||||
|
||||
# ── q_root_cause: none / spin (checked first) / root ────────────────────────
|
||||
|
||||
def c_rc():
|
||||
q_case("rc-none-no-cause-bearing", "q_root_cause",
|
||||
"a calm morning routine before sunrise")
|
||||
q_case("rc-spin-first-wins", "q_root_cause",
|
||||
"just keep going, iterate endlessly, patch over the bug, work "
|
||||
"around the root cause")
|
||||
q_case("rc-root-cause-frame", "q_root_cause",
|
||||
"we diagnose the root cause, research properly, and stop guessing")
|
||||
|
||||
|
||||
# ── q_respect: none / rigid (checked first) / respect ───────────────────────
|
||||
|
||||
def c_rs():
|
||||
q_case("rs-none-no-cognition-bearing", "q_respect",
|
||||
"the meeting moved to noon")
|
||||
q_case("rs-rigid-first-wins", "q_respect",
|
||||
"you must follow steps, ten steps, a strict plan, rigid routine "
|
||||
"or fail")
|
||||
q_case("rs-respect-cognition", "q_respect",
|
||||
"flexible direct options for the adhd learner, three gentle "
|
||||
"options at night")
|
||||
|
||||
|
||||
# ── q_assistant_goal: noise / two-plus / one / none (cache-mutating) ────────
|
||||
|
||||
def c_ag():
|
||||
# FIRST q_assistant_goal call of the process: the cached list holds the
|
||||
# universal terms exactly once, so the good==1 → 0.5 branch is reachable
|
||||
# only here (after the first in-place += every match counts ≥ 2).
|
||||
q_case("ag-single-domain-hit-first-call", "q_assistant_goal",
|
||||
"the xqzv mlpv recall note sits idle")
|
||||
q_case("ag-noise-only-no-value", "q_assistant_goal",
|
||||
"celebrity gossip, a recipe, sports scores and a horoscope clickbait")
|
||||
q_case("ag-many-domain-hits", "q_assistant_goal",
|
||||
"help me plan, explain the answer, and teach with evidence")
|
||||
q_case("ag-single-domain-hit", "q_assistant_goal",
|
||||
"the xkqv zqwrr communicat file sits idle")
|
||||
q_case("ag-generic-could-go-either-way", "q_assistant_goal",
|
||||
"zqw xyzv qrk mlpv brzzt")
|
||||
|
||||
|
||||
# ── q_durability: both branches ─────────────────────────────────────────────
|
||||
|
||||
def c_du():
|
||||
q_case("du-durable-signals", "q_durability",
|
||||
"she always prefers oat milk, consistently across sessions")
|
||||
q_case("du-not-durable", "q_durability",
|
||||
"today she drank tea once")
|
||||
|
||||
|
||||
# ── q_epistemology: bare / cite / hedge / none / bare-with-cite ─────────────
|
||||
|
||||
def c_ep():
|
||||
q_case("ep-bare-absolute-unsound", "q_epistemology",
|
||||
"it is definitely guaranteed and proven, 100%, no doubt")
|
||||
q_case("ep-evidence-traceable", "q_epistemology",
|
||||
"according to the paper, the study shows the data")
|
||||
q_case("ep-provisional-hedge", "q_epistemology",
|
||||
"it might likely change, subject to revision, as far as we know")
|
||||
q_case("ep-assertion-without-basis", "q_epistemology",
|
||||
"the sky is blue over the hills")
|
||||
q_case("ep-bare-but-cited", "q_epistemology",
|
||||
"it is definitely proven according to the book")
|
||||
|
||||
|
||||
# ── q_expression: none / one / two-plus ─────────────────────────────────────
|
||||
|
||||
def c_ex():
|
||||
q_case("ex-not-expressive", "q_expression",
|
||||
"the server rebooted at midnight")
|
||||
q_case("ex-partial-signal", "q_expression",
|
||||
"a beautiful morning")
|
||||
q_case("ex-expressive-character", "q_expression",
|
||||
"i find joy in the way i tell stories, beautiful and vast")
|
||||
|
||||
|
||||
# ── assess: the two-axis verdict lattice ────────────────────────────────────
|
||||
|
||||
def c_as():
|
||||
# Truth axis: bare absolute → epistemology veto → REJECT.
|
||||
assess_case("as-truth-bare-absolute-veto-reject",
|
||||
"it is definitely proven, 100% guaranteed", "project")
|
||||
# Expression axis: genuine voice → epistemology skipped → no veto.
|
||||
assess_case("as-expression-voice-promoted",
|
||||
"i find wonder in the cosmos and tell stories with a warm "
|
||||
"wry voice", "persona")
|
||||
# Expression axis SAFETY: bare factual absolute behind an opinion mask
|
||||
# is still rejected — and the subjective-predicate list is dead code
|
||||
# (oracle quirk: "beautiful" present does NOT protect "proven").
|
||||
assess_case("as-expression-bare-factual-still-rejected",
|
||||
"i believe the universe is beautiful and it is definitely "
|
||||
"proven", "voice")
|
||||
# Expression axis: subjective predicates only → NOT bare factual.
|
||||
assess_case("as-expression-subjective-opinion-safe",
|
||||
"i find the universe beautiful, strange, and humbling",
|
||||
"persona")
|
||||
# Multiple vetoes: LAST vetoing question wins (assignment overwrite).
|
||||
assess_case("as-multi-veto-last-wins",
|
||||
"conspiracy about the production server, it is definitely "
|
||||
"proven", None)
|
||||
# No veto but total < DEFER_MIN → REJECT with vetoed None.
|
||||
assess_case("as-total-below-defer-no-veto",
|
||||
"just keep going, iterate endlessly, band-aid; you must "
|
||||
"follow steps and the rigid routine or fail", None)
|
||||
# DEFER boundary: total == -1.0 exactly (not < DEFER_MIN).
|
||||
assess_case("as-defer-boundary",
|
||||
"iterate endlessly, patch over it", None)
|
||||
# ABSORB boundary: total == 0.0 exactly.
|
||||
assess_case("as-absorb-boundary-total-zero",
|
||||
"zqw xyzv qrk mlpv brzzt", None)
|
||||
# PROMOTE: strong truth-axis material.
|
||||
assess_case("as-promote-strong-truth-axis",
|
||||
"always verify with evidence and cite the source; diagnose "
|
||||
"the root cause; explain and teach the plan", "operating")
|
||||
# fact=None coerced to "" (oracle's `fact or ""`).
|
||||
assess_case("as-fact-none-coerced-empty", None, None)
|
||||
# Other expressive targets: delivery / identity.
|
||||
assess_case("as-delivery-target-expression-axis",
|
||||
"i delight in how i present data as a story", "delivery")
|
||||
assess_case("as-identity-target-expression-axis",
|
||||
"i am drawn to the vast and strange", "identity")
|
||||
# Truth target with a durability + assistant profile.
|
||||
assess_case("as-human-target-mixed",
|
||||
"she consistently prefers direct answers with sources",
|
||||
"human")
|
||||
|
||||
|
||||
# ── verdict_label: all four labels + passthrough fallback ───────────────────
|
||||
|
||||
def c_vl():
|
||||
label_case("vl-reject", "REJECT")
|
||||
label_case("vl-defer", "DEFER")
|
||||
label_case("vl-absorb", "ABSORB")
|
||||
label_case("vl-promote", "PROMOTE")
|
||||
label_case("vl-unknown-passthrough", "WEIRD")
|
||||
|
||||
|
||||
ALL = [c_pi, c_ho, c_mr, c_rc, c_rs, c_ag, c_du, c_ep, c_ex, c_as, c_vl]
|
||||
|
||||
FNS = {"assess", "verdict_label", "q_pi_safety", "q_honesty",
|
||||
"q_material_reality", "q_root_cause", "q_respect", "q_assistant_goal",
|
||||
"q_durability", "q_epistemology", "q_expression"}
|
||||
|
||||
|
||||
def _norm(x):
|
||||
if isinstance(x, bool):
|
||||
return x
|
||||
if isinstance(x, (int, float)):
|
||||
return float(x)
|
||||
if isinstance(x, (list, tuple)):
|
||||
return [_norm(i) for i in x]
|
||||
if isinstance(x, dict):
|
||||
return {k: _norm(v) for k, v in x.items()}
|
||||
return x
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization (key-order independent, int/float equal)."""
|
||||
return json.dumps(_norm(json.loads(json.dumps(x, default=str))),
|
||||
sort_keys=True)
|
||||
|
||||
|
||||
def facade_call(case):
|
||||
fn, inp = case["fn"], case["input"]
|
||||
if fn == "assess":
|
||||
return oracle_mod.assess(copy.deepcopy(inp["fact"]),
|
||||
copy.deepcopy(inp["target"]))
|
||||
if fn == "verdict_label":
|
||||
return oracle_mod.verdict_label(copy.deepcopy(inp["verdict"]))
|
||||
return getattr(oracle_mod, fn)(copy.deepcopy(inp["low"]))
|
||||
|
||||
|
||||
def kernel_call(case):
|
||||
fn, inp = case["fn"], case["input"]
|
||||
if fn == "assess":
|
||||
return wr_loader.assess(inp["fact"], inp["target"])
|
||||
if fn == "verdict_label":
|
||||
return wr_loader.verdict_label(inp["verdict"])
|
||||
return getattr(wr_loader, fn)(inp["low"])
|
||||
|
||||
|
||||
def main():
|
||||
if not REPLAY:
|
||||
for c in ALL:
|
||||
c()
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/worthiness.py",
|
||||
"pin": "RUNE_WR_CORE=python",
|
||||
"note": "Frozen via scripts/gen_worthiness_truth.py against the "
|
||||
"live Python oracle. Battery embeds this VERBATIM. "
|
||||
"Case order is LOAD-BEARING: q_assistant_goal's "
|
||||
"domain-cache list mutates in place on every call "
|
||||
"(oracle += law), so expected scores inflate across "
|
||||
"assess/q_assistant_goal cases exactly as frozen. Each "
|
||||
"case's ops[] is the replay script for the Rust battery.",
|
||||
"cases": CASES,
|
||||
}
|
||||
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 {len(CASES)} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
total_frozen = len(frozen)
|
||||
|
||||
# ── kernel replay: loader direct calls (separate process — the Rust
|
||||
# kernel's domain cache is process-global and must evolve across the
|
||||
# frozen case sequence exactly once, mirroring the freeze run) ──
|
||||
if "--replay-kernel" in sys.argv:
|
||||
kernel_fail = 0
|
||||
for case in frozen:
|
||||
got = kernel_call(case)
|
||||
got_norm = _norm(list(got) if isinstance(got, tuple) else got)
|
||||
# the loader returns the oracle-verbatim tuple → frozen "return"
|
||||
if canon(got_norm) != canon(case["return"]):
|
||||
kernel_fail += 1
|
||||
print(f"KERNEL DIVERGE [{case['fn']}] {case['name']}")
|
||||
print(" got: ", json.dumps(got_norm, default=str)[:600])
|
||||
print(" want:", json.dumps(_norm(case["return"]),
|
||||
default=str)[:600])
|
||||
print(f"kernel pass: {total_frozen - kernel_fail}/{total_frozen}")
|
||||
return 1 if kernel_fail else 0
|
||||
|
||||
proc = subprocess.run(
|
||||
[sys.executable, os.path.abspath(__file__), "--replay",
|
||||
"--replay-kernel"])
|
||||
kernel_fail = proc.returncode
|
||||
|
||||
# ── facade replay: rerun every case through the delegating facade ──
|
||||
# (RUNE_WR_CORE unpinned → the Rust kernel serves; loader host serves
|
||||
# domain_terms against the LIVE store.)
|
||||
facade_fail = 0
|
||||
for case in frozen:
|
||||
got = facade_call(case)
|
||||
if canon(list(got) if isinstance(got, tuple) else got) != canon(case["return"]):
|
||||
facade_fail += 1
|
||||
print(f"FACADE DIVERGE [{case['fn']}] {case['name']}")
|
||||
print(" got: ", json.dumps(_norm(got), default=str)[:600])
|
||||
print(" want:", json.dumps(_norm(case["return"]), default=str)[:600])
|
||||
|
||||
total = total_frozen
|
||||
print(f"replay: facade {total - facade_fail}/{total}, "
|
||||
f"kernel {total - kernel_fail}/{total}")
|
||||
return 1 if (facade_fail or kernel_fail) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for forge_client (GO port).
|
||||
Full lifecycle walk 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/forge_client.py to FULL Go (dichotomy factory, issue "
|
||||
"#2479 tranche): name-preserving port of _request (noise-vs-signal "
|
||||
"retry semantics), NullAdapter, GiteaForgejoAdapter and "
|
||||
"create_forge_client with HTTP/sleep/env behind injectable seams, "
|
||||
"frozen-truth battery generated against the live Python oracle, 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("CONTRACT", cid, "IN VERIFYING (close happens at delivery with SHAs)")
|
||||
@@ -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,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for worthiness.
|
||||
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/worthiness.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_WR_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": "worthiness parity port — evidence attached at close "
|
||||
"(SHAs reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/worthiness.py",
|
||||
"src/rune/wr_loader.py",
|
||||
"src/rust/worthiness/src/lib.rs",
|
||||
"src/rust/worthiness/src/branch_battery.rs",
|
||||
"scripts/gen_worthiness_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")
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,17 @@
|
||||
module rune/curator
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require modernc.org/sqlite v1.49.1
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
@@ -0,0 +1,160 @@
|
||||
// grading.go — parity port of src/rune/grading.py (Python fallback surface,
|
||||
// RUNE_GR_CORE=python pinned in the truth generator).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ruleWeak = []string{"should", "prefer", "instead of", "lead with", "first", "before ",
|
||||
"avoid", "favour", "keep ", "stop ", "start ", "use "}
|
||||
var ruleStrong = []string{"always", "never ", "must", "do not", "don't", "i always",
|
||||
"i must", "i should"}
|
||||
var affirmSignals = []string{"i protect", "i am", "i reveal", "i factor", "i dissolve",
|
||||
"i respect", "i teach", "i make", "i write", "i use the tiers", "i check", "i stop"}
|
||||
var userFactSignals = []string{"name", "email", "role", "lives in", "located in", "daw",
|
||||
"os:", "shell", "has a dog", "has a cat", "prefers", "works in", "works at",
|
||||
"teaches", "studies", "interested in", "financial", "income", "dietary",
|
||||
"location", "my home", "my machine"}
|
||||
var userAttrDecisive = []string{"prefers", "lives in", "located in", "teaches",
|
||||
"studies", "works at", "works in", "interested in", "dietary", "income", "financial"}
|
||||
var projectSignals = []string{"project", "current work", "task", "todo", "goal",
|
||||
"milestone", "deadline", "in progress", "blocked", "decision", "spec", "wip",
|
||||
"building ", "developing ", "course", "channel", "migration", "feature",
|
||||
"fixing", "working on", "active projects", "current state"}
|
||||
var constitutionSignals = []string{"add this to the constitution", "constitution",
|
||||
"always do", "i will always", "must always", "inviolable"}
|
||||
var docSignals = []string{"book", "chapter", "document", "transcript", "pdf", "epub",
|
||||
"verbatim", "diary entry", "palace drawer", "gutenberg"}
|
||||
|
||||
// affirmDict keeps the oracle's insertion order — the first matching key wins.
|
||||
var affirmDict = []struct{ key, positive string }{
|
||||
{"never defend billionaires", "I prioritise collective wellbeing and factual, caring explanations"},
|
||||
{"don't defend billionaires", "I prioritise collective wellbeing and factual, caring explanations"},
|
||||
{"never equate socialism with big government", "I describe socialism accurately, on its own terms"},
|
||||
{"never offer rigid plans", "I offer three gentle options when direction is sought"},
|
||||
{"don't offer rigid plans", "I offer three gentle options when direction is sought"},
|
||||
{"never hedge", "I answer directly, without hedging"},
|
||||
{"don't hedge", "I answer directly, without hedging"},
|
||||
{"do not overresearch", "I act on the quick answer when one will do"},
|
||||
{"don't overresearch", "I act on the quick answer when one will do"},
|
||||
{"do not overclaim", "I name evidence as evidence and confirmed causes as confirmed"},
|
||||
{"never overclaim", "I name evidence as evidence and confirmed causes as confirmed"},
|
||||
{"do not stall", "I act decisively once the root cause is understood"},
|
||||
{"don't stall", "I act decisively once the root cause is understood"},
|
||||
{"do not touch the production server", "I protect the production environment and ask before touching it"},
|
||||
{"never touch the production server", "I protect the production environment and ask before touching it"},
|
||||
{"ask permission before modifying anything on the server", "I check with the user before touching anything on the server"},
|
||||
{"do not use capitalistic frameworks", "I use factual, material, caring, non-blaming explanations"},
|
||||
{"never use hustle frameworks", "I use factual, material, caring, non-blaming explanations"},
|
||||
{"do not offer plans", "I offer three gentle options when direction is sought"},
|
||||
{"don't offer plans", "I offer three gentle options when direction is sought"},
|
||||
{"do not search endlessly", "I stop, research properly, and find the root cause"},
|
||||
{"don't iterate endlessly", "I stop, research properly, and find the root cause"},
|
||||
}
|
||||
|
||||
func containsAny(s string, subs []string) int {
|
||||
n := 0
|
||||
for _, sub := range subs {
|
||||
if strings.Contains(s, sub) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func hasAny(s string, subs []string) bool {
|
||||
for _, sub := range subs {
|
||||
if strings.Contains(s, sub) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AffirmRule rewrites a perceived demand/rule as a positive affirmation; only
|
||||
// known-correct rewrites are applied (AFFIRM_DICT), unknown prohibitions pass
|
||||
// through unchanged.
|
||||
func AffirmRule(fact string) string {
|
||||
f := strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(fact), "-* "))
|
||||
low := strings.Join(strings.Fields(strings.ToLower(f)), " ")
|
||||
alt := strings.ReplaceAll(strings.Trim(low, "."), " ", " ")
|
||||
for _, pair := range affirmDict {
|
||||
if strings.Contains(low, pair.key) || strings.Contains(alt, pair.key) {
|
||||
return pair.positive
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// GradeFact returns the tier (G0..G4) for a fact/rule based on type + language.
|
||||
func GradeFact(fact, target string) string {
|
||||
userName := strings.TrimSpace(strings.ToLower(os.Getenv("MEMORY_USER_NAME")))
|
||||
low := strings.ToLower(fact)
|
||||
if hasAny(low, constitutionSignals) {
|
||||
return "constitution"
|
||||
}
|
||||
if hasAny(low, docSignals) {
|
||||
return "mempalace"
|
||||
}
|
||||
ruleScore := containsAny(low, ruleStrong)*2 + containsAny(low, ruleWeak)
|
||||
affirmScore := containsAny(low, affirmSignals)
|
||||
userScore := containsAny(low, userFactSignals)
|
||||
projScore := containsAny(low, projectSignals)
|
||||
if target == "operating" {
|
||||
return "operating"
|
||||
}
|
||||
if target == "persona" {
|
||||
return "persona"
|
||||
}
|
||||
nameToken := fmt.Sprintf(" %s ", userName)
|
||||
nameTokenPresent := false
|
||||
if userName != "" {
|
||||
nameTokenPresent = strings.Contains(" "+low+" ", nameToken)
|
||||
}
|
||||
decisiveAttr := hasAny(low, userAttrDecisive)
|
||||
isPersonalAttr := (decisiveAttr || userScore >= 2) &&
|
||||
!hasAny(low, []string{"i ", "do not", "always", "never "}) &&
|
||||
!nameTokenPresent
|
||||
if isPersonalAttr {
|
||||
return "human"
|
||||
}
|
||||
if ruleScore >= 2 || affirmScore >= 1 {
|
||||
return "operating"
|
||||
}
|
||||
if target == "project" || projScore >= 2 {
|
||||
return "project"
|
||||
}
|
||||
if userScore >= 2 || target == "human" {
|
||||
return "human"
|
||||
}
|
||||
if target == "skill" || strings.Contains(low, "procedure") || strings.Contains(low, "steps") {
|
||||
return "skill"
|
||||
}
|
||||
return "hindsight"
|
||||
}
|
||||
|
||||
// TierOf — human label for a grade, for journaling / canary output.
|
||||
func TierOf(grade string) string {
|
||||
switch grade {
|
||||
case "constitution":
|
||||
return "G0"
|
||||
case "operating":
|
||||
return "G1"
|
||||
case "persona":
|
||||
return "G2p"
|
||||
case "human":
|
||||
return "G2"
|
||||
case "project":
|
||||
return "G2"
|
||||
case "skill":
|
||||
return "P"
|
||||
case "mempalace":
|
||||
return "G4"
|
||||
case "hindsight":
|
||||
return "G3"
|
||||
}
|
||||
return "G3"
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// lexicon.go — parity port of src/rune/lexicon_evolution.py (Python fallback
|
||||
// surface, RUNE_LE_CORE=python pinned in the truth generator). The pure
|
||||
// lexical decisions: content words, light stemming, distinctiveness (df <
|
||||
// 5%), coherence verdicts, promotion eligibility.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var leWordRe = regexp.MustCompile(`[a-z]{4,}`)
|
||||
|
||||
// SAME stopword set as memory_store._STOPWORDS (single source of truth would
|
||||
// be ideal; keep in sync — see lexicon_evolution.py).
|
||||
var leStop = map[string]bool{
|
||||
"the": true, "and": true, "for": true, "with": true, "that": true, "this": true,
|
||||
"from": true, "into": true, "when": true, "then": true, "were": true, "have": true,
|
||||
"been": true, "will": true, "was": true, "are": true, "but": true, "not": true,
|
||||
"you": true, "your": true, "also": true, "its": true, "his": true, "her": true,
|
||||
"him": true, "over": true, "under": true, "them": true, "they": true,
|
||||
"there": true, "about": true, "after": true, "what": true, "which": true,
|
||||
"who": true, "how": true, "why": true, "where": true, "while": true, "just": true,
|
||||
"more": true, "each": true, "than": true, "very": true, "such": true,
|
||||
"some": true, "only": true, "prefers": true, "needs": true, "wants": true,
|
||||
"likes": true,
|
||||
}
|
||||
|
||||
// Hard stop for evolution: generic framing words never carry a relation.
|
||||
var leFraming = map[string]bool{
|
||||
"work": true, "always": true, "way": true, "thing": true, "value": true,
|
||||
"identity": true, "persona": true, "someone": true, "something": true,
|
||||
"never": true, "before": true, "after": true, "then": true, "when": true,
|
||||
"while": true, "must": true, "should": true, "will": true, "would": true,
|
||||
"could": true, "can": true, "have": true, "has": true, "had": true,
|
||||
"been": true, "being": true, "used": true, "use": true, "uses": true,
|
||||
"make": true, "makes": true, "made": true, "keep": true, "keeps": true,
|
||||
"take": true, "takes": true, "took": true, "get": true, "gets": true,
|
||||
"got": true, "going": true,
|
||||
}
|
||||
|
||||
var leNegation = map[string]bool{
|
||||
"not": true, "never": true, "don't": true, "doesn't": true, "cannot": true,
|
||||
"can't": true, "no": true, "nor": true, "neither": true, "without": true,
|
||||
"avoid": true, "reject": true, "against": true, "refuses": true, "refuse": true,
|
||||
"opposed": true,
|
||||
}
|
||||
|
||||
const leDistinctiveDF = 0.05
|
||||
|
||||
func leContentWords(text string) []string {
|
||||
var out []string
|
||||
for _, w := range leWordRe.FindAllString(strings.ToLower(text), -1) {
|
||||
if !leStop[w] && !leFraming[w] {
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// leStem — light morphological fold; suffix rules ordered longest-first.
|
||||
func leStem(w string) string {
|
||||
rules := []struct{ suf, fold string }{
|
||||
{"ationally", ""}, {"ableness", ""}, {"iveness", ""},
|
||||
{"ation", ""}, {"ating", ""}, {"ative", "e"},
|
||||
{"atively", "e"}, {"ability", ""}, {"ities", "y"},
|
||||
{"ingly", ""}, {"fully", ""}, {"ness", ""}, {"ment", ""},
|
||||
{"itive", "e"}, {"atable", "e"}, {"ated", ""}, {"ates", "e"},
|
||||
{"ions", ""}, {"ings", ""}, {"able", "e"}, {"ible", "e"},
|
||||
{"ing", "e"}, {"ed", ""}, {"es", ""}, {"ly", ""}, {"s", ""}, {"e", ""},
|
||||
}
|
||||
for _, r := range rules {
|
||||
if len(w)-len(r.suf) >= 4 && strings.HasSuffix(w, r.suf) {
|
||||
w = w[:len(w)-len(r.suf)]
|
||||
if r.fold != "" {
|
||||
w += r.fold
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// y -> i fold (honesty -> honest)
|
||||
if len(w) > 4 && strings.HasSuffix(w, "y") && !strings.ContainsRune("aeiouy", rune(w[len(w)-2])) {
|
||||
w = w[:len(w)-1]
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func leStemSet(text string) map[string]bool {
|
||||
set := map[string]bool{}
|
||||
for _, w := range leWordRe.FindAllString(strings.ToLower(text), -1) {
|
||||
set[leStem(w)] = true
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func leDistinctiveTerms(fact string, corpusTexts []string) map[string]bool {
|
||||
total := len(corpusTexts)
|
||||
if total == 0 {
|
||||
total = 1
|
||||
}
|
||||
factTerms := map[string]bool{}
|
||||
for _, w := range leContentWords(fact) {
|
||||
factTerms[w] = true
|
||||
}
|
||||
if len(factTerms) == 0 {
|
||||
return map[string]bool{}
|
||||
}
|
||||
docTerms := map[string]int{}
|
||||
for _, t := range corpusTexts {
|
||||
for s := range leStemSet(t) {
|
||||
docTerms[s]++
|
||||
}
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for t := range factTerms {
|
||||
if float64(docTerms[leStem(t)]) < maxFloat(float64(total)*leDistinctiveDF, 1.5) {
|
||||
out[t] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func maxFloat(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func leSharedDistinctive(fact string, existingValues, corpusTexts []string) map[string]bool {
|
||||
distinct := leDistinctiveTerms(fact, corpusTexts)
|
||||
if len(distinct) == 0 {
|
||||
return map[string]bool{}
|
||||
}
|
||||
shared := map[string]bool{}
|
||||
for _, val := range existingValues {
|
||||
for _, w := range leWordRe.FindAllString(strings.ToLower(val), -1) {
|
||||
s := leStem(w)
|
||||
for term := range distinct {
|
||||
if leStem(term) == s {
|
||||
shared[term] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return shared
|
||||
}
|
||||
|
||||
// Coherence — classify a new value against existing ones, lexicon-aware:
|
||||
// "conflict" | "consistent" | "novel".
|
||||
func Coherence(fact string, existingValues []string, corpusTexts []string) string {
|
||||
var values []string
|
||||
for _, v := range existingValues {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return "novel"
|
||||
}
|
||||
corpus := corpusTexts
|
||||
if len(corpus) == 0 {
|
||||
corpus = values
|
||||
}
|
||||
shared := leSharedDistinctive(fact, values, corpus)
|
||||
if len(shared) == 0 {
|
||||
return "novel"
|
||||
}
|
||||
low := strings.ToLower(fact)
|
||||
hasNeg := false
|
||||
for n := range leNegation {
|
||||
if strings.Contains(low, n) {
|
||||
hasNeg = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasNeg {
|
||||
for t := range shared {
|
||||
if strings.Contains(" "+low+" ", " "+t+" ") {
|
||||
return "conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "consistent"
|
||||
}
|
||||
|
||||
// Grounded — is the fact grounded in real material via a DISTINCTIVE shared
|
||||
// term (conjunctive), not just vector proximity or a common word?
|
||||
func Grounded(fact string, sourceTexts []string, minDistinct int) bool {
|
||||
var srcs []string
|
||||
for _, s := range sourceTexts {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
srcs = append(srcs, s)
|
||||
}
|
||||
}
|
||||
if len(srcs) == 0 {
|
||||
return false
|
||||
}
|
||||
factTerms := map[string]bool{}
|
||||
for _, w := range leContentWords(fact) {
|
||||
factTerms[w] = true
|
||||
}
|
||||
if len(factTerms) == 0 {
|
||||
return false
|
||||
}
|
||||
var joined []string
|
||||
for _, s := range srcs {
|
||||
joined = append(joined, strings.ToLower(s))
|
||||
}
|
||||
stems := leStemSet(strings.Join(joined, " "))
|
||||
hits := 0
|
||||
for t := range factTerms {
|
||||
if stems[leStem(t)] {
|
||||
hits++
|
||||
if hits >= minDistinct {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PromoteEligible — is a persona/constitution promotion eligible, lexicon-aware?
|
||||
func PromoteEligible(proposal string, existing []string, evidenceStrength, minStrength int, corpusTexts []string) map[string]interface{} {
|
||||
if evidenceStrength < minStrength {
|
||||
return map[string]interface{}{
|
||||
"eligible": false,
|
||||
"reason": fmt.Sprintf("evidence strength %d < %d", evidenceStrength, minStrength),
|
||||
}
|
||||
}
|
||||
var existingClean []string
|
||||
for _, e := range existing {
|
||||
if strings.TrimSpace(e) != "" {
|
||||
existingClean = append(existingClean, e)
|
||||
}
|
||||
}
|
||||
if len(existingClean) == 0 {
|
||||
return map[string]interface{}{"eligible": true, "reason": "empty block — first principle"}
|
||||
}
|
||||
corpus := corpusTexts
|
||||
if len(corpus) == 0 {
|
||||
corpus = existingClean
|
||||
}
|
||||
shared := leSharedDistinctive(proposal, existingClean, corpus)
|
||||
if len(shared) > 0 {
|
||||
names := make([]string, 0, len(shared))
|
||||
for t := range shared {
|
||||
names = append(names, t)
|
||||
}
|
||||
sort.Strings(names)
|
||||
if len(names) > 3 {
|
||||
names = names[:3]
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"eligible": true,
|
||||
"reason": fmt.Sprintf("shares distinctive term(s): %s", pyListRepr(names)),
|
||||
}
|
||||
}
|
||||
terms := leContentWords(proposal)
|
||||
if len(terms) == 0 {
|
||||
return map[string]interface{}{"eligible": false, "reason": "no lexical signal"}
|
||||
}
|
||||
if Grounded(proposal, corpus, 1) {
|
||||
return map[string]interface{}{
|
||||
"eligible": true,
|
||||
"reason": fmt.Sprintf("novel axis grounded in corpus (%d terms)", len(terms)),
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{"eligible": false, "reason": "novel axis not grounded in absorbed material — parked"}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// main.go — curator CLI: replay the frozen oracle truth battery.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: curator replay <truth.json>")
|
||||
os.Exit(2)
|
||||
}
|
||||
path := "truth.json"
|
||||
if len(os.Args) > 2 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Println(f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases exact-equal vs the Python oracle\n", truthRowCount())
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// main_cli.go — main() CLI surface replay. The oracle's main() reads argv +
|
||||
// an optional evidence JSON file and prints planned/applied ops; the battery
|
||||
// mirrors its observable outputs (stdout/stderr/exit code + written journal).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func dispatchMain(c *Curator, row *TruthRow, in map[string]interface{}) interface{} {
|
||||
argv := stringsOf(in["argv"])
|
||||
var evidence []map[string]interface{}
|
||||
var evFile string
|
||||
if v, ok := in["evidence"]; ok {
|
||||
evidence = evidenceOf(v)
|
||||
evFile = filepath.Join(filepath.Dir(c.StoreDB), "evidence.json")
|
||||
b, _ := json.Marshal(evidence)
|
||||
os.WriteFile(evFile, b, 0o644)
|
||||
}
|
||||
var out, errBuf bytes.Buffer
|
||||
c.Out = &out
|
||||
c.Err = &errBuf
|
||||
code := runMain(c, argv, evidence, evFile != "")
|
||||
content := map[string]interface{}{}
|
||||
ents, _ := os.ReadDir(c.JournalDir)
|
||||
for _, e := range ents {
|
||||
b, _ := os.ReadFile(filepath.Join(c.JournalDir, e.Name()))
|
||||
content[e.Name()] = string(b)
|
||||
}
|
||||
res := map[string]interface{}{"stdout": out.String(), "stderr": errBuf.String(), "code": float64(code)}
|
||||
if row.Name == "apply_output" {
|
||||
res["journal"] = content
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// runMain mirrors curator.main()'s observable behavior.
|
||||
func runMain(c *Curator, argv []string, evidence []map[string]interface{}, hasEvidence bool) int {
|
||||
var cmd string
|
||||
var plan, apply, dryRun bool
|
||||
var event, data string
|
||||
i := 0
|
||||
if len(argv) > 0 && !strings.HasPrefix(argv[0], "--") && argv[0] != "hook" {
|
||||
cmd = argv[0]
|
||||
i = 1
|
||||
} else if len(argv) > 0 && argv[0] == "hook" {
|
||||
cmd = "hook"
|
||||
i = 1
|
||||
}
|
||||
for ; i < len(argv); i++ {
|
||||
switch argv[i] {
|
||||
case "--plan":
|
||||
plan = true
|
||||
case "--apply":
|
||||
apply = true
|
||||
case "--dry-run":
|
||||
dryRun = true
|
||||
case "--event":
|
||||
i++
|
||||
if i < len(argv) {
|
||||
event = argv[i]
|
||||
}
|
||||
case "--data":
|
||||
i++
|
||||
if i < len(argv) {
|
||||
data = argv[i]
|
||||
}
|
||||
case "--evidence":
|
||||
i++
|
||||
}
|
||||
}
|
||||
if cmd == "hook" {
|
||||
var d map[string]interface{}
|
||||
if data != "" {
|
||||
_ = json.Unmarshal([]byte(data), &d)
|
||||
}
|
||||
res := c.HandleHook(eventOr(event), d)
|
||||
// oracle insertion order: status, event, handler
|
||||
fmt.Fprintf(c.Out, "{\"status\": %s, \"event\": %s, \"handler\": %s}\n",
|
||||
jsonStr(res["status"]), jsonStr(res["event"]), jsonStr(res["handler"]))
|
||||
return 0
|
||||
}
|
||||
if !hasEvidence {
|
||||
// memory_store seam blocked → no auto evidence → exit 1
|
||||
fmt.Fprintln(c.Err, "No evidence provided and no recent entries found.")
|
||||
return 1
|
||||
}
|
||||
doApply := apply || (!plan && !dryRun)
|
||||
journalLines, ops := c.RunPlan(evidence, doApply)
|
||||
if !doApply {
|
||||
fmt.Fprintln(c.Out, "=== planned ops (dry-run) ===")
|
||||
for _, j := range journalLines {
|
||||
fmt.Fprintf(c.Out, " %-8s → %s | %s | %s\n",
|
||||
strOr(j["op"], ""), strOr(j["target"], ""),
|
||||
truncate(strOr(j["after"], ""), 70), strOr(j["reason"], ""))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
written := 0
|
||||
for _, j := range journalLines {
|
||||
if strOr(j["op"], "") == "PROPOSE" {
|
||||
c.JournalAppend(j)
|
||||
written++
|
||||
} else if r := strOr(j["reason"], ""); r == "" || r == "dry-run" {
|
||||
c.JournalAppend(j)
|
||||
written++
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(c.Out, "Applied %d ops; %d logged to journal; index saved.\n", len(ops), written)
|
||||
for _, j := range journalLines {
|
||||
fmt.Fprintf(c.Out, " %-8s → %s | %s\n",
|
||||
strOr(j["op"], ""), strOr(j["target"], ""), truncate(strOr(j["after"], ""), 70))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func eventOr(e string) string {
|
||||
if e == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return e
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// Package main — curator dichotomy port (GO verdict: Reactive → Go).
|
||||
// om.go — order-preserving JSON objects. The candidate ledger and the entry
|
||||
// index iterate keys in insertion order (Python dict law), so idx inputs are
|
||||
// decoded into OM instead of map[string]any wherever iteration order can
|
||||
// surface in `want`.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OM — an ordered JSON object.
|
||||
type OM struct {
|
||||
Keys []string
|
||||
M map[string]interface{}
|
||||
}
|
||||
|
||||
func NewOM() *OM {
|
||||
return &OM{M: map[string]interface{}{}}
|
||||
}
|
||||
|
||||
func (o *OM) Get(key string) (interface{}, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
v, ok := o.M[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (o *OM) Set(key string, v interface{}) {
|
||||
if _, ok := o.M[key]; !ok {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.M[key] = v
|
||||
}
|
||||
|
||||
func (o *OM) Delete(key string) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := o.M[key]; ok {
|
||||
delete(o.M, key)
|
||||
for i, k := range o.Keys {
|
||||
if k == key {
|
||||
o.Keys = append(o.Keys[:i], o.Keys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OM) Len() int {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return len(o.Keys)
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes preserving key order; nested objects decode as *OM.
|
||||
func (o *OM) UnmarshalJSON(b []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(b))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tok != json.Delim('{') {
|
||||
return fmt.Errorf("OM: expected object, got %v", tok)
|
||||
}
|
||||
o.Keys = nil
|
||||
o.M = map[string]interface{}{}
|
||||
for dec.More() {
|
||||
kt, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := kt.(string)
|
||||
v, err := decodeOMValue(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Keys = append(o.Keys, key)
|
||||
o.M[key] = v
|
||||
}
|
||||
_, err = dec.Token() // closing }
|
||||
return err
|
||||
}
|
||||
|
||||
func decodeOMValue(dec *json.Decoder) (interface{}, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeOMToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeOMToken(dec *json.Decoder, tok json.Token) (interface{}, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
inner := NewOM()
|
||||
for dec.More() {
|
||||
kt, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := decodeOMValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inner.Keys = append(inner.Keys, kt.(string))
|
||||
inner.M[kt.(string)] = v
|
||||
}
|
||||
if _, err := dec.Token(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inner, nil
|
||||
case '[':
|
||||
var arr []interface{}
|
||||
for dec.More() {
|
||||
v, err := decodeOMValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, v)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if arr == nil {
|
||||
arr = []interface{}{}
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("OM: unexpected delim %v", t)
|
||||
default:
|
||||
return tok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON re-emits in preserved order.
|
||||
func (o *OM) MarshalJSON() ([]byte, error) {
|
||||
if o == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range o.Keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
kb, _ := json.Marshal(k)
|
||||
buf.Write(kb)
|
||||
buf.WriteByte(':')
|
||||
vb, err := json.Marshal(o.M[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(vb)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// -- value coercion helpers (Python .get semantics) --------------------------
|
||||
|
||||
func strOf(v interface{}) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func strOr(v interface{}, def string) string {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func intOf(v interface{}) int {
|
||||
switch n := v.(type) {
|
||||
case json.Number:
|
||||
f, _ := n.Float64()
|
||||
return int(f)
|
||||
case float64:
|
||||
return int(n)
|
||||
case int:
|
||||
return n
|
||||
case int64:
|
||||
return int(n)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func floatOf(v interface{}) float64 {
|
||||
switch n := v.(type) {
|
||||
case json.Number:
|
||||
f, _ := n.Float64()
|
||||
return f
|
||||
case float64:
|
||||
return n
|
||||
case int:
|
||||
return float64(n)
|
||||
case int64:
|
||||
return float64(n)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func intOr(v interface{}, def int) int {
|
||||
if v == nil {
|
||||
return def
|
||||
}
|
||||
switch n := v.(type) {
|
||||
case json.Number:
|
||||
f, _ := n.Float64()
|
||||
return int(f)
|
||||
case float64:
|
||||
return int(n)
|
||||
case int:
|
||||
return n
|
||||
case string:
|
||||
if n == "" {
|
||||
return def
|
||||
}
|
||||
}
|
||||
return intOf(v)
|
||||
}
|
||||
|
||||
// pyListRepr renders a []string the way Python's repr does for the le reasons.
|
||||
func pyListRepr(ss []string) string {
|
||||
quoted := make([]string, len(ss))
|
||||
for i, s := range ss {
|
||||
quoted[i] = "'" + s + "'"
|
||||
}
|
||||
return "[" + strings.Join(quoted, ", ") + "]"
|
||||
}
|
||||
|
||||
func sortedCopy(ss []string) []string {
|
||||
out := append([]string(nil), ss...)
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// phaseb.go — Phase B language-advantage pass: allocation discipline.
|
||||
//
|
||||
// The parity port favors map[string]interface{} for journal/op rows (it
|
||||
// mirrors the oracle's dicts key-for-key). Phase B replaces the hot paths'
|
||||
// heap-churn shapes with pre-sized, append-in-place builders without changing
|
||||
// any observable output (the frozen battery pins every byte):
|
||||
// - NewOp: pre-sized op row with the six canonical keys in oracle order
|
||||
// - AppendJournal: append-in-place journal growth (no intermediate slices)
|
||||
// - JoinLines: single-pass body rebuild for the ADD path
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// opKeys — the oracle's op-row key order (observable in journal JSON).
|
||||
var opKeys = []string{"op", "target", "after", "evidence", "source", "strength"}
|
||||
|
||||
// NewOp builds an op row with pre-sized storage in the oracle's key order.
|
||||
func NewOp(op, target, after, evidence, source string, strength int) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"op": op,
|
||||
"target": target,
|
||||
"after": after,
|
||||
"evidence": evidence,
|
||||
"source": source,
|
||||
"strength": strength,
|
||||
}
|
||||
}
|
||||
|
||||
// OpKeys returns the canonical op-row key order (documents the envelope).
|
||||
func OpKeys() []string {
|
||||
return append([]string(nil), opKeys...)
|
||||
}
|
||||
|
||||
// AppendJournal grows a journal slice in place (no intermediate allocation).
|
||||
func AppendJournal(jl []map[string]interface{}, rows ...map[string]interface{}) []map[string]interface{} {
|
||||
return append(jl, rows...)
|
||||
}
|
||||
|
||||
// JoinLines rebuilds a block body from kept lines in one pass.
|
||||
func JoinLines(kept []string) string {
|
||||
return strings.Join(kept, "\n")
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Phase B tests: the language-advantage builders produce byte-identical
|
||||
// rows to the inline literals they replace (pinned by the frozen battery).
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPhaseBNewOpShape(t *testing.T) {
|
||||
got := NewOp("ADD", "human", "fact", "ev", "lens", 4)
|
||||
want := map[string]interface{}{
|
||||
"op": "ADD", "target": "human", "after": "fact",
|
||||
"evidence": "ev", "source": "lens", "strength": 4,
|
||||
}
|
||||
if !reflect.DeepEqual(normJSON(got), normJSON(want)) {
|
||||
t.Fatalf("NewOp shape drift: %#v", got)
|
||||
}
|
||||
if keys := OpKeys(); !reflect.DeepEqual(keys, []string{"op", "target", "after", "evidence", "source", "strength"}) {
|
||||
t.Fatalf("OpKeys drift: %v", keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseBAppendJournal(t *testing.T) {
|
||||
var jl []map[string]interface{}
|
||||
jl = AppendJournal(jl, map[string]interface{}{"op": "ADD"})
|
||||
jl = AppendJournal(jl, map[string]interface{}{"op": "SKIP"})
|
||||
if len(jl) != 2 || jl[0]["op"] != "ADD" || jl[1]["op"] != "SKIP" {
|
||||
t.Fatalf("AppendJournal drift: %#v", jl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseBJoinLines(t *testing.T) {
|
||||
if got := JoinLines([]string{"- a", "- b"}); got != "- a\n- b" {
|
||||
t.Fatalf("JoinLines drift: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// scripts/gen_curator_truth.py against the live Python oracle with paths
|
||||
// isolated, dependency cores pinned, clock frozen, subprocess faked, embed
|
||||
// tabled, aaak/memory_store blocked, drift gate + audit ledger + drift-ledger
|
||||
// seams injected) and re-executes every row through this Go port.
|
||||
//
|
||||
// Seams mirrored here (one-to-one with the generator's declared seams):
|
||||
// - setup.db_missing → StoreDB points at a path in a nonexistent dir
|
||||
// - setup.db_rows → fresh sqlite store seeded with the same rows
|
||||
// - setup.transcripts → transcripts dir rewritten with the same files +
|
||||
// mtimes (age_days before the frozen instant)
|
||||
// - setup.index / index_missing → index file written / absent
|
||||
// - setup.idx_init → the in-memory idx argument
|
||||
// - setup.skills → skills dir seeded with the same files
|
||||
// - input.vecs → the embed seam table for semantic_candidate_key
|
||||
// - input.seam_drift_gate → "reject" installs the REJECT stub; else off
|
||||
// - input.seam_audit_ledger → true installs the recorder
|
||||
// - input.seam_ledger_verdict → the drift-ledger check() verdict
|
||||
// - input.seam_graph_write → "stub_true" installs GraphWrite → true
|
||||
// - input.stdout → GraphRun returns the per-case stdout; the frozen argv's
|
||||
// "@RUNE@" slot is substituted with this checkout's graph_memory.py path
|
||||
//
|
||||
// Rows of one fn replay sequentially on a shared scope (db/transcripts/index
|
||||
// persist across rows of the same fn), mirroring the oracle run.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
Generator string `json:"generator"`
|
||||
Note string `json:"note"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input json.RawMessage `json:"input"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on
|
||||
// 100% parity).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
repo := repoRoot()
|
||||
fails := []string{}
|
||||
var cur *Curator
|
||||
var curFn string
|
||||
scope := map[string]string{}
|
||||
for i := range doc.Rows {
|
||||
row := &doc.Rows[i]
|
||||
if row.Fn != curFn || cur == nil {
|
||||
cur = newScopeCurator(repo)
|
||||
curFn = row.Fn
|
||||
scope = map[string]string{}
|
||||
}
|
||||
if msg := replayRow(cur, scope, repo, row); msg != "" {
|
||||
fails = append(fails, row.Fn+"/"+row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
func repoRoot() string {
|
||||
// src/go/curator → repo root (two parents up).
|
||||
exe, err := os.Executable()
|
||||
_ = exe
|
||||
_ = err
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return filepath.Dir(filepath.Dir(filepath.Dir(cwd)))
|
||||
}
|
||||
|
||||
func truthRowCount() int {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func newScopeCurator(repo string) *Curator {
|
||||
root, err := os.MkdirTemp("", "curator-replay-*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c := New(filepath.Join(root, "memory", "store", "memory.db"))
|
||||
c.BlocksDir = filepath.Join(root, "blocks")
|
||||
c.JournalDir = filepath.Join(root, "journal")
|
||||
c.IndexFile = filepath.Join(root, "index", "blocks_meta.json")
|
||||
c.SkillsDir = filepath.Join(root, "config", "skills")
|
||||
c.TranscriptsDir = filepath.Join(root, "transcripts")
|
||||
c.GraphMemoryPath = filepath.Join(repo, "src", "rune", "graph_memory.py")
|
||||
c.Out = discard{}
|
||||
c.Err = discard{}
|
||||
return c
|
||||
}
|
||||
|
||||
type discard struct{}
|
||||
|
||||
func (discard) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
// applySetup recreates the row's seed state (db/transcripts/index/skills).
|
||||
func applySetup(c *Curator, scope map[string]string, row *TruthRow) error {
|
||||
var setup map[string]interface{}
|
||||
if len(row.Setup) > 0 {
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if v, ok := setup["db_missing"]; ok && v == true {
|
||||
c.StoreDB = filepath.Join(c.JournalDir, "..", "nonexistent-dir", "no.db")
|
||||
c.StoreDB = filepath.Join(filepath.Dir(c.JournalDir), "nonexistent-dir", "no.db")
|
||||
scope["db"] = "missing"
|
||||
} else if v, ok := setup["db_rows"]; ok {
|
||||
dbPath := filepath.Join(filepath.Dir(c.StoreDB), "scope.db")
|
||||
rows, _ := v.([]interface{})
|
||||
if err := SeedDB(dbPath, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
c.StoreDB = dbPath
|
||||
scope["db"] = dbPath
|
||||
}
|
||||
if v, ok := setup["transcripts"]; ok {
|
||||
if err := seedTranscripts(c, v.([]interface{})); err != nil {
|
||||
return err
|
||||
}
|
||||
scope["transcripts"] = "seeded"
|
||||
}
|
||||
if v, ok := setup["index"]; ok {
|
||||
os.MkdirAll(filepath.Dir(c.IndexFile), 0o755)
|
||||
b, _ := json.MarshalIndent(normJSON(v), "", " ")
|
||||
if err := os.WriteFile(c.IndexFile, b, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if v, ok := setup["index_missing"]; ok && v == true {
|
||||
os.Remove(c.IndexFile)
|
||||
}
|
||||
if v, ok := setup["skills"]; ok {
|
||||
if err := seedSkills(c, v.(map[string]interface{})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// input-embedded setup (put_setup rows): same keys, inside input.setup.
|
||||
var input map[string]interface{}
|
||||
if err := json.Unmarshal(row.Input, &input); err != nil {
|
||||
return err
|
||||
}
|
||||
if v, ok := input["setup"]; ok {
|
||||
sub, _ := v.(map[string]interface{})
|
||||
if dv, ok := sub["db_missing"]; ok && dv == true {
|
||||
c.StoreDB = filepath.Join(filepath.Dir(c.JournalDir), "nonexistent-dir", "no.db")
|
||||
scope["db"] = "missing"
|
||||
} else if dv, ok := sub["db_rows"]; ok {
|
||||
dbPath := filepath.Join(filepath.Dir(c.StoreDB), "scope.db")
|
||||
rows, _ := dv.([]interface{})
|
||||
if err := SeedDB(dbPath, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
c.StoreDB = dbPath
|
||||
scope["db"] = dbPath
|
||||
}
|
||||
if tv, ok := sub["transcripts"]; ok {
|
||||
if err := seedTranscripts(c, tv.([]interface{})); err != nil {
|
||||
return err
|
||||
}
|
||||
scope["transcripts"] = "seeded"
|
||||
}
|
||||
if iv, ok := sub["index"]; ok {
|
||||
os.MkdirAll(filepath.Dir(c.IndexFile), 0o755)
|
||||
b, _ := json.MarshalIndent(normJSON(iv), "", " ")
|
||||
if err := os.WriteFile(c.IndexFile, b, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if iv, ok := sub["index_missing"]; ok && iv == true {
|
||||
os.Remove(c.IndexFile)
|
||||
}
|
||||
if sv, ok := sub["skills"]; ok {
|
||||
if err := seedSkills(c, sv.(map[string]interface{})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedTranscripts(c *Curator, list []interface{}) error {
|
||||
os.MkdirAll(c.TranscriptsDir, 0o755)
|
||||
ents, err := os.ReadDir(c.TranscriptsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range ents {
|
||||
os.Remove(filepath.Join(c.TranscriptsDir, e.Name()))
|
||||
}
|
||||
frozen, _ := time.Parse("2006-01-02T15:04:05Z07:00", FrozenISO)
|
||||
for _, v := range list {
|
||||
t := v.(map[string]interface{})
|
||||
p := filepath.Join(c.TranscriptsDir, strOr(t["name"], ""))
|
||||
if err := os.WriteFile(p, []byte(strOr(t["body"], "")), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
age := floatOf(t["age_days"])
|
||||
stamp := frozen.Add(-time.Duration(age*86400) * time.Second)
|
||||
if err := os.Chtimes(p, stamp, stamp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedSkills(c *Curator, m map[string]interface{}) error {
|
||||
os.MkdirAll(c.SkillsDir, 0o755)
|
||||
ents, err := os.ReadDir(c.SkillsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range ents {
|
||||
os.RemoveAll(filepath.Join(c.SkillsDir, e.Name()))
|
||||
}
|
||||
for rel, v := range m {
|
||||
p := filepath.Join(c.SkillsDir, rel)
|
||||
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||
if err := os.WriteFile(p, []byte(strOf(v)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pinRowSeams installs the row's injected seams (drift gate / audit ledger /
|
||||
// ledger verdict / graph stub / embed table).
|
||||
func pinRowSeams(c *Curator, row *TruthRow, audit *[]AuditCall) {
|
||||
var input map[string]interface{}
|
||||
_ = json.Unmarshal(row.Input, &input)
|
||||
if v, ok := input["seam_drift_gate"]; ok && v == "reject" {
|
||||
c.DriftGate = func(entry map[string]interface{}) GateResult {
|
||||
return GateResult{Verdict: "REJECT", Reason: "washed out"}
|
||||
}
|
||||
} else {
|
||||
c.DriftGate = nil
|
||||
}
|
||||
if v, ok := input["seam_audit_ledger"]; ok && v == true {
|
||||
c.AuditLedger = func(id int, kind, action string, count int, detail string) {
|
||||
*audit = append(*audit, AuditCall{id, kind, action, count, detail})
|
||||
}
|
||||
} else {
|
||||
c.AuditLedger = nil
|
||||
}
|
||||
if v, ok := input["seam_ledger_verdict"]; ok {
|
||||
c.LedgerVerdict = intOf(v)
|
||||
} else {
|
||||
c.LedgerVerdict = 0
|
||||
}
|
||||
if v, ok := input["seam_graph_write"]; ok && v == "stub_true" {
|
||||
c.GraphRun = func(argv []string) string { return `"added": True` }
|
||||
} else {
|
||||
c.GraphRun = nil
|
||||
}
|
||||
}
|
||||
|
||||
// AuditCall — one recorded log_promotion call.
|
||||
type AuditCall struct {
|
||||
ID int
|
||||
Kind string
|
||||
Action string
|
||||
Count int
|
||||
Detail string
|
||||
}
|
||||
|
||||
func auditCallsJSON(calls []AuditCall) interface{} {
|
||||
out := []interface{}{}
|
||||
for _, a := range calls {
|
||||
out = append(out, []interface{}{a.ID, a.Kind, a.Action, float64(a.Count), a.Detail})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
// replay_cases.go — per-function replay dispatch. Each case mirrors exactly
|
||||
// one row of the frozen truth: it reads the row input, applies the row setup,
|
||||
// runs the ported function, and returns the got value for comparison against
|
||||
// the frozen want. Any divergence fails the battery.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// normJSON canonicalizes a value through JSON (numbers → float64, OM →
|
||||
// ordered maps) so DeepEqual compares oracle-shaped values.
|
||||
func normJSON(v interface{}) interface{} {
|
||||
switch t := v.(type) {
|
||||
case []string:
|
||||
out := make([]interface{}, len(t))
|
||||
for i, s := range t {
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
case []float64:
|
||||
out := make([]interface{}, len(t))
|
||||
for i, f := range t {
|
||||
out[i] = f
|
||||
}
|
||||
return out
|
||||
case []map[string]interface{}:
|
||||
out := make([]interface{}, len(t))
|
||||
for i, m := range t {
|
||||
out[i] = normJSON(m)
|
||||
}
|
||||
return out
|
||||
case *OM:
|
||||
m := map[string]interface{}{}
|
||||
for _, k := range t.Keys {
|
||||
m[k] = normJSON(t.M[k])
|
||||
}
|
||||
return m
|
||||
case map[string]interface{}:
|
||||
m := map[string]interface{}{}
|
||||
for k, vv := range t {
|
||||
m[k] = normJSON(vv)
|
||||
}
|
||||
return m
|
||||
case []interface{}:
|
||||
out := make([]interface{}, len(t))
|
||||
for i, vv := range t {
|
||||
out[i] = normJSON(vv)
|
||||
}
|
||||
return out
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
case int:
|
||||
return float64(t)
|
||||
case int64:
|
||||
return float64(t)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func wantOf(row *TruthRow) interface{} {
|
||||
dec := json.NewDecoder(bytes.NewReader(row.Want))
|
||||
dec.UseNumber()
|
||||
var w interface{}
|
||||
_ = dec.Decode(&w)
|
||||
return normJSON(w)
|
||||
}
|
||||
|
||||
func inputOf(row *TruthRow) map[string]interface{} {
|
||||
dec := json.NewDecoder(bytes.NewReader(row.Input))
|
||||
dec.UseNumber()
|
||||
var in map[string]interface{}
|
||||
_ = dec.Decode(&in)
|
||||
return in
|
||||
}
|
||||
|
||||
func replayRow(c *Curator, scope map[string]string, repo string, row *TruthRow) string {
|
||||
if err := applySetup(c, scope, row); err != nil {
|
||||
return fmt.Sprintf("setup: %v", err)
|
||||
}
|
||||
var audit []AuditCall
|
||||
pinRowSeams(c, row, &audit)
|
||||
in := inputOf(row)
|
||||
got, msg := dispatch(c, scope, repo, row, in)
|
||||
if msg != "" {
|
||||
return msg
|
||||
}
|
||||
// audit ledger calls are part of promote_candidates rows' want
|
||||
if row.Fn == "promote_candidates" && len(audit) > 0 {
|
||||
g, ok := got.(map[string]interface{})
|
||||
if !ok {
|
||||
return "audit: got not a map"
|
||||
}
|
||||
g["audit_calls"] = auditCallsJSON(audit)
|
||||
got = g
|
||||
}
|
||||
if !reflect.DeepEqual(normJSON(got), wantOf(row)) {
|
||||
gb, _ := json.Marshal(normJSON(got))
|
||||
wb, _ := json.Marshal(wantOf(row))
|
||||
return fmt.Sprintf("divergence\ngot: %s\nwant: %s", gb, wb)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// dispatch runs one frozen row. Returns (got, error-message).
|
||||
func dispatch(c *Curator, scope map[string]string, repo string, row *TruthRow, in map[string]interface{}) (interface{}, string) {
|
||||
// journal_append rows share one journal scope (the oracle never resets
|
||||
// between them).
|
||||
if row.Fn == "journal_append" && scope["journal"] == "" {
|
||||
ents, _ := os.ReadDir(c.JournalDir)
|
||||
for _, e := range ents {
|
||||
os.Remove(filepath.Join(c.JournalDir, e.Name()))
|
||||
}
|
||||
scope["journal"] = "open"
|
||||
}
|
||||
// main rows each run main() fresh on the shared scope; the oracle's
|
||||
// main/apply_output journal accumulates hook/dry/plan rows' NOTHING
|
||||
// (those never write the journal) — no reset needed.
|
||||
switch row.Fn {
|
||||
case "block_path":
|
||||
return c.BlockPath(strOr(in["label"], "")), ""
|
||||
case "entry_key":
|
||||
return EntryKey(strOr(in["text"], "")), ""
|
||||
case "find_entry":
|
||||
v := FindEntry(strOr(in["body"], ""), strOr(in["needle"], ""))
|
||||
if v == "" {
|
||||
return nil, ""
|
||||
}
|
||||
return v, ""
|
||||
case "candidate_key":
|
||||
return CandidateKey(strOr(in["text"], "")), ""
|
||||
case "norm":
|
||||
return Norm(strOr(in["text"], "")), ""
|
||||
case "detect_conflict":
|
||||
k, l := DetectConflict(strOr(in["body"], ""), strOr(in["fact"], ""))
|
||||
if k == "" {
|
||||
return []interface{}{nil, nil}, ""
|
||||
}
|
||||
return []interface{}{k, l}, ""
|
||||
case "overlap":
|
||||
return Overlap(strOr(in["a"], ""), strOr(in["b"], "")), ""
|
||||
case "cos":
|
||||
return Cos(floats(in["a"]), floats(in["b"])), ""
|
||||
case "sanitize_slug":
|
||||
return SanitizeSlug(strOr(in["name"], "")), ""
|
||||
case "skill_path":
|
||||
rel, err := filepath.Rel(c.SkillsDir, c.SkillPath(strOr(in["name"], "")))
|
||||
if err != nil {
|
||||
return "", err.Error()
|
||||
}
|
||||
return rel, ""
|
||||
case "is_topic_fact":
|
||||
return c.IsTopicFact(strOr(in["fact"], "")), ""
|
||||
case "aaak":
|
||||
return c.Aaak(strOr(in["text"], ""), strOr(in["topic"], "")), ""
|
||||
case "le_promote_eligible":
|
||||
if v, ok := in["seam_lexicon"]; ok && v == "unavailable" {
|
||||
return lePromoteEligibleFallback(strOr(in["proposal"], ""), nil,
|
||||
intOf(in["strength"]), intOf(in["min_strength"]), intOf(in["wash_count"])), ""
|
||||
}
|
||||
existing := stringsOf(in["existing"])
|
||||
return c.LePromoteEligible(strOr(in["proposal"], ""), existing,
|
||||
intOf(in["strength"]), intOf(in["min_strength"]), intOf(in["wash_count"])), ""
|
||||
case "store_has":
|
||||
topic := ""
|
||||
if in["topic"] != nil {
|
||||
topic = strOr(in["topic"], "")
|
||||
}
|
||||
return c.StoreHas(strOr(in["text"], ""), topic), ""
|
||||
case "read_block":
|
||||
fm, body := c.ReadBlock(strOr(in["label"], ""))
|
||||
return map[string]interface{}{"fm": fm, "body": body}, ""
|
||||
case "write_block":
|
||||
ret := c.WriteBlock(strOr(in["label"], ""), strOf(in["body"]))
|
||||
out := map[string]interface{}{"return": ret}
|
||||
if row.Name != "db_unavailable_returns_descriptor" {
|
||||
_, body := c.ReadBlock(strOr(in["label"], ""))
|
||||
out["body"] = body
|
||||
}
|
||||
return out, ""
|
||||
case "block_values":
|
||||
out := c.BlockValues(strOr(in["label"], ""))
|
||||
if out == nil {
|
||||
out = []string{}
|
||||
}
|
||||
return out, ""
|
||||
case "store_corpus":
|
||||
out := c.StoreCorpus(intOf(in["limit"]))
|
||||
if out == nil {
|
||||
out = []string{}
|
||||
}
|
||||
return out, ""
|
||||
case "contradicts_identity":
|
||||
return c.ContradictsIdentity(strOr(in["fact"], "")), ""
|
||||
case "load_index":
|
||||
return c.LoadIndex(), ""
|
||||
case "save_index":
|
||||
c.SaveIndex(mustOM(in["idx"]))
|
||||
return c.LoadIndex(), ""
|
||||
case "ensure_entry_meta":
|
||||
idx := idxInitOf(row)
|
||||
key := c.EnsureEntryMeta(idx, strOr(in["label"], ""), strOr(in["text"], ""))
|
||||
lomV, _ := idx.Get(strOr(in["label"], ""))
|
||||
lom, _ := omEntry(lomV)
|
||||
// the oracle's want carries the label dict as a plain map
|
||||
return map[string]interface{}{"key": key, "label_idx": lom}, ""
|
||||
case "touch_entry":
|
||||
idx := idxInitOf(row)
|
||||
label := strOr(in["label"], "")
|
||||
text := strOr(in["text"], "")
|
||||
key := c.TouchEntry(idx, label, text)
|
||||
lomV, _ := idx.Get(label)
|
||||
lom, _ := lomV.(*OM)
|
||||
metaV, _ := lom.Get(key)
|
||||
meta, _ := omEntry(metaV)
|
||||
return map[string]interface{}{"key": key, "meta": meta}, ""
|
||||
case "journal_append":
|
||||
entry, _ := in["entry"].(map[string]interface{})
|
||||
// rows of this fn replay sequentially on a shared journal scope,
|
||||
// mirroring the oracle run (minimal sees full's entry).
|
||||
p := c.JournalAppend(entry)
|
||||
content := map[string]interface{}{}
|
||||
ents, _ := os.ReadDir(c.JournalDir)
|
||||
for _, e := range ents {
|
||||
b, _ := os.ReadFile(filepath.Join(c.JournalDir, e.Name()))
|
||||
content[e.Name()] = string(b)
|
||||
}
|
||||
return map[string]interface{}{"path": filepath.Base(p), "content": content}, ""
|
||||
case "ensure_candidates":
|
||||
idx := NewOM()
|
||||
if v, ok := in["idx"].(*OM); ok {
|
||||
idx = v
|
||||
} else if v, ok := in["idx"].(map[string]interface{}); ok {
|
||||
idx = mapToOM(v)
|
||||
}
|
||||
return EnsureCandidates(idx), ""
|
||||
case "semantic_candidate_key":
|
||||
vecs := tableVecs()
|
||||
embed := func(texts []string) map[string][]float64 {
|
||||
out := map[string][]float64{}
|
||||
for _, t := range texts {
|
||||
if v, ok := vecs[t]; ok {
|
||||
out[t] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
cands := NewOM()
|
||||
cands.Set(CandidateKey("fact alpha about deploy runners"),
|
||||
map[string]interface{}{"text": "fact alpha about deploy runners"})
|
||||
cands.Set(CandidateKey("fact delta about whiskey tasting"),
|
||||
map[string]interface{}{"text": "fact delta about whiskey tasting"})
|
||||
if row.Name == "empty_cands" {
|
||||
cands = NewOM()
|
||||
}
|
||||
v := c.SemanticCandidateKey(strOr(in["fact"], ""), cands, embed)
|
||||
if v == "" {
|
||||
return nil, ""
|
||||
}
|
||||
return v, ""
|
||||
case "embed":
|
||||
for k, v := range tableVecs() {
|
||||
EmbedCache[k] = v
|
||||
}
|
||||
out := c.Embed(stringsOf(in["texts"]))
|
||||
m := map[string]interface{}{}
|
||||
for k, v := range out {
|
||||
m[k] = v
|
||||
}
|
||||
return m, ""
|
||||
case "grounded_in_sessions":
|
||||
return c.GroundedInSessions(strOr(in["fact"], ""), 0.46, 3), ""
|
||||
case "coherence_check":
|
||||
if v, ok := in["fact"]; ok && (v == "" || len(strOf(v)) < 15) && in["sources"] == nil {
|
||||
return c.CoherenceCheck(strOf(v), []string{"sessions", "corpus"}), ""
|
||||
}
|
||||
return c.CoherenceCheck(strOr(in["fact"], ""), stringsOf(in["sources"])), ""
|
||||
case "grounded_in_corpus":
|
||||
return c.GroundedInCorpus(strOr(in["fact"], "")), ""
|
||||
case "decay_candidates":
|
||||
idx := idxInitOf(row)
|
||||
if idx.Len() == 0 {
|
||||
if raw := rawField(row, "idx"); len(raw) > 0 {
|
||||
idx = mustOMRaw(raw)
|
||||
}
|
||||
}
|
||||
var jl []map[string]interface{}
|
||||
changed := c.DecayCandidates(idx, &jl)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
cands, _ := idx.Get(CandidatesKey)
|
||||
return map[string]interface{}{"changed": changed, "remaining": cands, "journal": jl}, ""
|
||||
case "promote_candidates":
|
||||
idx := idxInitOf(row)
|
||||
ev := evidenceOf(in["evidence"])
|
||||
var jl []map[string]interface{}
|
||||
promoted := c.PromoteCandidates(ev, idx, &jl)
|
||||
if promoted == nil {
|
||||
promoted = []map[string]interface{}{}
|
||||
}
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
cands, _ := idx.Get(CandidatesKey)
|
||||
if cands == nil {
|
||||
cands = NewOM()
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"promoted": promoted, "candidates": cands, "journal": jl,
|
||||
}, ""
|
||||
case "plan_ops":
|
||||
idx := idxInitOf(row)
|
||||
ev := evidenceOf(in["evidence"])
|
||||
var jl []map[string]interface{}
|
||||
ops := c.PlanOps(ev, idx, &jl)
|
||||
if ops == nil {
|
||||
ops = []map[string]interface{}{}
|
||||
}
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
return map[string]interface{}{"ops": ops, "idx": idx, "journal": jl}, ""
|
||||
case "apply_op":
|
||||
idx := idxInitOf(row)
|
||||
ops := evidenceOf(in["ops"])
|
||||
var jl []map[string]interface{}
|
||||
var results []interface{}
|
||||
for _, op := range ops {
|
||||
results = append(results, c.ApplyOp(op, idx, &jl))
|
||||
}
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
_, body := c.ReadBlock("human")
|
||||
return map[string]interface{}{
|
||||
"results": results, "journal": jl, "body": body, "idx": idx,
|
||||
}, ""
|
||||
case "apply_warm":
|
||||
var jl []map[string]interface{}
|
||||
if row.Name == "post_upsert_neurons" || row.Name == "post_seed_neurons" {
|
||||
return neuronsJSON(ListNeurons(c.StoreDB)), ""
|
||||
}
|
||||
if op, ok := in["op"].(map[string]interface{}); ok {
|
||||
ret := c.ApplyWarm(op, &jl)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
if _, hasReturned := rawWantField(row, "returned"); hasReturned {
|
||||
return map[string]interface{}{"returned": ret, "journal": jl}, ""
|
||||
}
|
||||
return ret, ""
|
||||
}
|
||||
// seed_new_neuron/empty_fact rows: want is {returned, journal}
|
||||
fact := strOr(in["after"], strOr(in["fact"], ""))
|
||||
ret := c.ApplyWarm(map[string]interface{}{"after": fact}, &jl)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
return map[string]interface{}{"returned": ret, "journal": jl}, ""
|
||||
case "derive_triggers":
|
||||
return DeriveTriggers(strOr(in["text"], ""), stringsOf(in["existing"])), ""
|
||||
case "decay_pass":
|
||||
idx := idxInitOf(row)
|
||||
if idx.Len() == 0 {
|
||||
if raw := rawField(row, "idx"); len(raw) > 0 {
|
||||
idx = mustOMRaw(raw)
|
||||
}
|
||||
}
|
||||
// the oracle's want carries idx_after as the MUTATED idx (same
|
||||
// object); replay returns the same mutated object.
|
||||
var jl []map[string]interface{}
|
||||
c.DecayPass(idx, &jl)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
return map[string]interface{}{"idx_after": idx, "journal": jl}, ""
|
||||
case "compress_pass":
|
||||
idx := idxInitOf(row)
|
||||
var jl []map[string]interface{}
|
||||
changed := c.CompressPass(idx, &jl)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
if row.Name == "under_limit_noop" {
|
||||
return map[string]interface{}{"changed": changed, "journal": jl}, ""
|
||||
}
|
||||
_, body := c.ReadBlock("operating")
|
||||
return map[string]interface{}{
|
||||
"changed": changed, "body": body, "journal": jl, "idx": idx,
|
||||
}, ""
|
||||
case "author_skill":
|
||||
op, _ := in["op"].(map[string]interface{})
|
||||
var jl []map[string]interface{}
|
||||
if row.Name == "no_name" || row.Name == "no_steps" {
|
||||
return c.AuthorSkill(op, &jl), ""
|
||||
}
|
||||
written := c.AuthorSkill(op, &jl)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
out := map[string]interface{}{"written": written}
|
||||
if row.Name == "new_skill" {
|
||||
slug := SanitizeSlug(strOr(op["name"], ""))
|
||||
b, _ := os.ReadFile(c.SkillPath(slug))
|
||||
out["rel"] = slug + "/SKILL.md"
|
||||
out["content"] = string(b)
|
||||
out["journal"] = jl
|
||||
} else if row.Name == "existing_adds_evidence" || row.Name == "existing_keeps_evidence" {
|
||||
slug := SanitizeSlug(strOr(op["name"], ""))
|
||||
b, _ := os.ReadFile(c.SkillPath(slug))
|
||||
out["content"] = string(b)
|
||||
out["journal"] = jl
|
||||
}
|
||||
return out, ""
|
||||
case "graph_write":
|
||||
stdout := strOr(in["stdout"], `"added": True`)
|
||||
var captured []string
|
||||
c.GraphRun = func(argv []string) string {
|
||||
captured = append([]string(nil), argv...)
|
||||
return stdout
|
||||
}
|
||||
added := c.GraphWrite(strOr(in["fact"], ""), "ev", "src", intOf(in["strength"]))
|
||||
for i, a := range captured {
|
||||
if strings.HasPrefix(a, repo+"/src/rune/graph_memory.py") || a == c.GraphMemoryPath {
|
||||
captured[i] = "@RUNE@/src/rune/graph_memory.py"
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{"added": added, "argv": captured}, ""
|
||||
case "run_plan":
|
||||
ev := evidenceOf(in["evidence"])
|
||||
apply := row.Name != "dry_run"
|
||||
var buf bytes.Buffer
|
||||
c.Out = &buf
|
||||
jl, ops := c.RunPlan(ev, apply)
|
||||
if jl == nil {
|
||||
jl = []map[string]interface{}{}
|
||||
}
|
||||
if ops == nil {
|
||||
ops = []map[string]interface{}{}
|
||||
}
|
||||
out := map[string]interface{}{"journal": jl, "ops": ops}
|
||||
if row.Name == "apply_gate_pass" {
|
||||
out["index"] = c.LoadIndex()
|
||||
} else if row.Name == "apply_gate_refuses" {
|
||||
out["stdout"] = buf.String()
|
||||
}
|
||||
return out, ""
|
||||
case "main":
|
||||
return dispatchMain(c, row, in), ""
|
||||
case "handle_hook":
|
||||
var data map[string]interface{}
|
||||
if v, ok := in["data"].(map[string]interface{}); ok {
|
||||
data = v
|
||||
}
|
||||
return c.HandleHook(strOr(in["event"], ""), data), ""
|
||||
}
|
||||
return nil, fmt.Sprintf("unknown fn %q", row.Fn)
|
||||
}
|
||||
|
||||
func floats(v interface{}) []float64 {
|
||||
var out []float64
|
||||
if arr, ok := v.([]interface{}); ok {
|
||||
for _, x := range arr {
|
||||
out = append(out, floatOf(x))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringsOf(v interface{}) []string {
|
||||
var out []string
|
||||
if arr, ok := v.([]interface{}); ok {
|
||||
for _, x := range arr {
|
||||
out = append(out, strOf(x))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func evidenceOf(v interface{}) []map[string]interface{} {
|
||||
var out []map[string]interface{}
|
||||
if arr, ok := v.([]interface{}); ok {
|
||||
for _, x := range arr {
|
||||
if m, ok := x.(map[string]interface{}); ok {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// mustOM decodes an idx_init value (nil → empty OM). Plain maps arrive
|
||||
// from inputOf's Unmarshal (order lost) — callers that need insertion order
|
||||
// must decode from the raw row input instead (mustOMRaw).
|
||||
func mustOM(v interface{}) *OM {
|
||||
if v == nil {
|
||||
return NewOM()
|
||||
}
|
||||
if om, ok := v.(*OM); ok {
|
||||
return om
|
||||
}
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return mapToOM(m)
|
||||
}
|
||||
return NewOM()
|
||||
}
|
||||
|
||||
// mustOMRaw decodes an idx value from raw JSON, preserving key order.
|
||||
func mustOMRaw(raw json.RawMessage) *OM {
|
||||
om := NewOM()
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return om
|
||||
}
|
||||
if err := om.UnmarshalJSON(raw); err != nil {
|
||||
return NewOM()
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// idxInitOf extracts the raw idx_init JSON for a row (order-preserving).
|
||||
// Sources, in order: input.idx_init, input.idx, setup.idx_init — mirroring
|
||||
// the generator (plan/apply/compress rows carry idx_init in top-level setup).
|
||||
func idxInitOf(row *TruthRow) *OM {
|
||||
var in struct {
|
||||
IdxInit json.RawMessage `json:"idx_init"`
|
||||
Idx json.RawMessage `json:"idx"`
|
||||
}
|
||||
_ = json.Unmarshal(row.Input, &in)
|
||||
if len(in.IdxInit) > 0 && string(in.IdxInit) != "null" {
|
||||
return mustOMRaw(in.IdxInit)
|
||||
}
|
||||
if len(in.Idx) > 0 && string(in.Idx) != "null" {
|
||||
return mustOMRaw(in.Idx)
|
||||
}
|
||||
var setup struct {
|
||||
IdxInit json.RawMessage `json:"idx_init"`
|
||||
}
|
||||
if len(row.Setup) > 0 {
|
||||
_ = json.Unmarshal(row.Setup, &setup)
|
||||
if len(setup.IdxInit) > 0 && string(setup.IdxInit) != "null" {
|
||||
return mustOMRaw(setup.IdxInit)
|
||||
}
|
||||
}
|
||||
return NewOM()
|
||||
}
|
||||
|
||||
// mapToOM converts a decoded map into an ordered OM.
|
||||
func mapToOM(m map[string]interface{}) *OM {
|
||||
om := NewOM()
|
||||
for k, v := range m {
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
om.Set(k, mapToOM(t))
|
||||
case []interface{}:
|
||||
om.Set(k, normList(t))
|
||||
default:
|
||||
om.Set(k, v)
|
||||
}
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
func normList(l []interface{}) []interface{} {
|
||||
out := make([]interface{}, len(l))
|
||||
for i, v := range l {
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
out[i] = mapToOM(m)
|
||||
} else {
|
||||
out[i] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func neuronsJSON(ns []Neuron) interface{} {
|
||||
if ns == nil {
|
||||
return []interface{}{}
|
||||
}
|
||||
b, _ := json.Marshal(ns)
|
||||
var out interface{}
|
||||
_ = json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// tableVecs — the frozen VEC table (mirrors the generator's VEC).
|
||||
func tableVecs() map[string][]float64 {
|
||||
return map[string][]float64{
|
||||
"fact alpha about deploy runners": {1.0, 0.0, 0.0},
|
||||
"fact beta about gitea runners": {0.0, 1.0, 0.0},
|
||||
"fact gamma about deploy runners nightly": {0.9, 0.1, 0.0},
|
||||
"fact delta about whiskey tasting": {0.0, 0.0, 1.0},
|
||||
}
|
||||
}
|
||||
|
||||
// mustMap decodes an idx value that may be *OM or a plain map.
|
||||
func mustMap(v interface{}) map[string]interface{} {
|
||||
if om, ok := v.(*OM); ok {
|
||||
m := map[string]interface{}{}
|
||||
for _, k := range om.Keys {
|
||||
m[k] = om.M[k]
|
||||
}
|
||||
return m
|
||||
}
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
// rawField extracts a raw JSON field from a row's input (order-preserving).
|
||||
func rawField(row *TruthRow, field string) json.RawMessage {
|
||||
var in map[string]json.RawMessage
|
||||
_ = json.Unmarshal(row.Input, &in)
|
||||
return in[field]
|
||||
}
|
||||
|
||||
func jsonStr(v interface{}) string {
|
||||
b, _ := json.Marshal(strOf(v))
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// dbSig fingerprints a seed-row list so sequential rows on one scope keep
|
||||
// their accumulated store state instead of re-seeding every row.
|
||||
func dbSig(rows []interface{}) string {
|
||||
b, _ := json.Marshal(rows)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// rawWantField extracts a raw JSON field from a row's want.
|
||||
func rawWantField(row *TruthRow, field string) (json.RawMessage, bool) {
|
||||
var w map[string]json.RawMessage
|
||||
_ = json.Unmarshal(row.Want, &w)
|
||||
v, ok := w[field]
|
||||
return v, ok
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// store.go — sqlite seam for the curator port (modernc.org/sqlite, pure Go).
|
||||
// Mirrors the oracle's sqlite3 usage: read paths use `file:<path>?mode=ro`
|
||||
// (missing file/dir → connect error → the documented fallback), write paths
|
||||
// open read-write (missing dir → error → fallback).
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const entriesSchema = `
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
importance REAL DEFAULT 0.5,
|
||||
created_at TEXT NOT NULL,
|
||||
last_accessed TEXT,
|
||||
topic TEXT DEFAULT '',
|
||||
entities TEXT DEFAULT '[]',
|
||||
source TEXT DEFAULT '',
|
||||
method TEXT DEFAULT 'curator',
|
||||
status TEXT DEFAULT 'active',
|
||||
valid_from TEXT,
|
||||
valid_until TEXT,
|
||||
confidence REAL DEFAULT 0.7,
|
||||
temperature REAL DEFAULT 1.0,
|
||||
always_on INTEGER DEFAULT 0,
|
||||
priority INTEGER DEFAULT 5,
|
||||
triggers TEXT DEFAULT '',
|
||||
kind TEXT DEFAULT 'fact',
|
||||
slug TEXT DEFAULT '',
|
||||
summary TEXT DEFAULT '',
|
||||
wash_count INTEGER DEFAULT 0,
|
||||
last_washed TEXT,
|
||||
promotion_eligible INTEGER DEFAULT 0,
|
||||
rock_status TEXT DEFAULT 'washable',
|
||||
tier TEXT DEFAULT 'warm',
|
||||
memory_type TEXT
|
||||
);
|
||||
`
|
||||
|
||||
func openRO(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", "file:"+path+"?mode=ro")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func openRW(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", "file:"+path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// SeedDB creates a fresh store with the full entries schema and inserts the
|
||||
// setup rows (column names mirror the oracle schema).
|
||||
func SeedDB(path string, rows []interface{}) error {
|
||||
if err := os.MkdirAll(dirOf(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Remove(path)
|
||||
db, err := openRW(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err = db.Exec(entriesSchema); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rows {
|
||||
m, ok := r.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("seed row not an object")
|
||||
}
|
||||
cols := make([]string, 0, len(m))
|
||||
vals := make([]interface{}, 0, len(m))
|
||||
base := map[string]interface{}{
|
||||
"text": "", "importance": 0.5, "created_at": "2026-01-01T00:00:00+00:00",
|
||||
"last_accessed": "2026-01-01T00:00:00+00:00", "topic": "", "entities": "[]",
|
||||
"source": "seed", "method": "seed", "status": "active", "valid_from": nil,
|
||||
"confidence": 0.7, "temperature": 1.0, "always_on": 0, "priority": 5,
|
||||
"triggers": "", "kind": "fact", "slug": "", "summary": "", "wash_count": 0,
|
||||
}
|
||||
for k, v := range base {
|
||||
if _, ok := m[k]; !ok {
|
||||
cols = append(cols, k)
|
||||
vals = append(vals, v)
|
||||
}
|
||||
}
|
||||
for k, v := range m {
|
||||
cols = append(cols, k)
|
||||
vals = append(vals, v)
|
||||
}
|
||||
q := "INSERT INTO entries ("
|
||||
for i, c := range cols {
|
||||
if i > 0 {
|
||||
q += ", "
|
||||
}
|
||||
q += c
|
||||
}
|
||||
q += ") VALUES ("
|
||||
for i := range cols {
|
||||
if i > 0 {
|
||||
q += ", "
|
||||
}
|
||||
q += "?"
|
||||
}
|
||||
q += ")"
|
||||
if _, err = db.Exec(q, vals...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirOf(path string) string {
|
||||
for i := len(path) - 1; i >= 0; i-- {
|
||||
if path[i] == '/' {
|
||||
return path[:i]
|
||||
}
|
||||
}
|
||||
return "."
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,116 @@
|
||||
// Battery — Go tests replaying the frozen oracle truth VERBATIM.
|
||||
// Every row of truth.json (generated by scripts/gen_curator_truth.py
|
||||
// against the live Python oracle) re-executes through this port; any
|
||||
// divergence fails the test.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read truth: %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth has no rows")
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func TestBatteryRowCount(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
if len(doc.Rows) != 162 {
|
||||
t.Fatalf("row count drift: got %d want 162", len(doc.Rows))
|
||||
}
|
||||
if doc.Oracle != "src/rune/curator.py" {
|
||||
t.Fatalf("oracle drift: %q", doc.Oracle)
|
||||
}
|
||||
if doc.Generator != "scripts/gen_curator_truth.py" {
|
||||
t.Fatalf("generator drift: %q", doc.Generator)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
repo := repoRootForTest()
|
||||
var cur *Curator
|
||||
var curFn string
|
||||
scope := map[string]string{}
|
||||
fails := 0
|
||||
for i := range doc.Rows {
|
||||
row := &doc.Rows[i]
|
||||
if row.Fn != curFn || cur == nil {
|
||||
cur = newScopeCurator(repo)
|
||||
curFn = row.Fn
|
||||
scope = map[string]string{}
|
||||
}
|
||||
if msg := replayRow(cur, scope, repo, row); msg != "" {
|
||||
fails++
|
||||
t.Errorf("%s/%s: %s", row.Fn, row.Name, msg)
|
||||
}
|
||||
}
|
||||
if fails > 0 {
|
||||
t.Fatalf("%d/%d battery cases FAILED", fails, len(doc.Rows))
|
||||
}
|
||||
t.Logf("battery parity OK: %d cases", len(doc.Rows))
|
||||
}
|
||||
|
||||
func TestBatteryByFn(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
repo := repoRootForTest()
|
||||
byFn := map[string][]int{}
|
||||
for i := range doc.Rows {
|
||||
byFn[doc.Rows[i].Fn] = append(byFn[doc.Rows[i].Fn], i)
|
||||
}
|
||||
// Every public function of the oracle must be covered.
|
||||
for _, fn := range []string{
|
||||
"block_path", "entry_key", "find_entry", "candidate_key", "norm",
|
||||
"detect_conflict", "overlap", "cos", "sanitize_slug", "skill_path",
|
||||
"is_topic_fact", "aaak", "le_promote_eligible", "store_has",
|
||||
"read_block", "write_block", "block_values", "store_corpus",
|
||||
"contradicts_identity", "load_index", "save_index", "ensure_entry_meta",
|
||||
"touch_entry", "journal_append", "ensure_candidates",
|
||||
"semantic_candidate_key", "embed", "coherence_check",
|
||||
"grounded_in_sessions", "grounded_in_corpus", "decay_candidates",
|
||||
"promote_candidates", "plan_ops", "apply_op", "apply_warm",
|
||||
"derive_triggers", "decay_pass", "compress_pass", "author_skill",
|
||||
"graph_write", "run_plan", "main", "handle_hook",
|
||||
} {
|
||||
rows, ok := byFn[fn]
|
||||
if !ok || len(rows) == 0 {
|
||||
t.Errorf("fn %q has no frozen rows", fn)
|
||||
continue
|
||||
}
|
||||
cur := newScopeCurator(repo)
|
||||
scope := map[string]string{}
|
||||
for _, i := range rows {
|
||||
row := &doc.Rows[i]
|
||||
if msg := replayRow(cur, scope, repo, row); msg != "" {
|
||||
t.Errorf("%s/%s: %s", row.Fn, row.Name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func repoRootForTest() string {
|
||||
// tests run with CWD = src/go/curator.
|
||||
abs, err := filepath.Abs("truth.json")
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
// <repo>/src/go/curator/truth.json → <repo>
|
||||
for i := 0; i < 3; i++ {
|
||||
abs = filepath.Dir(abs)
|
||||
}
|
||||
return abs
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// warm.go — parity port of the warm_router trio the curator depends on
|
||||
// (list_neurons / write_neuron / _derive_triggers): the warm neuron layer.
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var warmWordRe = regexp.MustCompile(`[a-z]{4,}`)
|
||||
|
||||
func parseTrigger(s string) []string {
|
||||
var out []string
|
||||
for _, t := range strings.Split(s, ",") {
|
||||
t = strings.ToLower(strings.TrimSpace(t))
|
||||
if t != "" {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Neuron — one warm neuron as list_neurons returns it.
|
||||
type Neuron struct {
|
||||
Slug string `json:"slug"`
|
||||
Triggers []string `json:"triggers"`
|
||||
Importance float64 `json:"importance"`
|
||||
Body string `json:"body"`
|
||||
Summary string `json:"summary"`
|
||||
Tokens int `json:"tokens"`
|
||||
WashCount int `json:"wash_count"`
|
||||
BodyKnowledge string `json:"body_knowledge"`
|
||||
}
|
||||
|
||||
// ListNeurons — all active warm neurons from the store (kind='neuron').
|
||||
func ListNeurons(storeDB string) []Neuron {
|
||||
var neurons []Neuron
|
||||
db, err := openRO(storeDB)
|
||||
if err != nil {
|
||||
return neurons
|
||||
}
|
||||
rows, qerr := db.Query("SELECT text, slug, triggers, importance, summary, wash_count FROM entries " +
|
||||
"WHERE kind='neuron' AND status='active' ORDER BY slug")
|
||||
if qerr != nil {
|
||||
db.Close()
|
||||
return neurons
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var text, slug, triggers, summary sql.NullString
|
||||
var importance float64
|
||||
var washCount int
|
||||
if err := rows.Scan(&text, &slug, &triggers, &importance, &summary, &washCount); err != nil {
|
||||
return neurons
|
||||
}
|
||||
full := strings.TrimSpace(text.String)
|
||||
summ := strings.TrimSpace(summary.String)
|
||||
triggerList := parseTrigger(triggers.String)
|
||||
bodyKnowledge := ""
|
||||
if len(triggerList) > 0 {
|
||||
n := len(triggerList)
|
||||
if n > 5 {
|
||||
n = 5
|
||||
}
|
||||
bodyKnowledge = fmt.Sprintf("Fires on: %s. ", strings.Join(triggerList[:n], ", "))
|
||||
}
|
||||
if summ != "" {
|
||||
s := summ
|
||||
if len(s) > 80 {
|
||||
s = s[:80]
|
||||
}
|
||||
bodyKnowledge += fmt.Sprintf("Knows: %s. ", s)
|
||||
}
|
||||
wc := washCount
|
||||
switch {
|
||||
case wc >= 20:
|
||||
bodyKnowledge += "Proven — has survived many washes."
|
||||
case wc >= 10:
|
||||
bodyKnowledge += "Established — building trust."
|
||||
case wc > 0:
|
||||
bodyKnowledge += "Testing — still proving itself."
|
||||
default:
|
||||
bodyKnowledge += "Untested — new or rarely accessed."
|
||||
}
|
||||
imp := importance
|
||||
if imp == 0 {
|
||||
imp = 0.5
|
||||
}
|
||||
tok := len(full) / 4
|
||||
if tok < 1 {
|
||||
tok = 1
|
||||
}
|
||||
sg := slug.String
|
||||
if sg == "" {
|
||||
sg = "warm"
|
||||
}
|
||||
neurons = append(neurons, Neuron{
|
||||
Slug: sg,
|
||||
Triggers: triggerList,
|
||||
Importance: imp,
|
||||
Body: full,
|
||||
Summary: summ,
|
||||
Tokens: tok,
|
||||
WashCount: wc,
|
||||
BodyKnowledge: strings.TrimSpace(bodyKnowledge),
|
||||
})
|
||||
}
|
||||
return neurons
|
||||
}
|
||||
|
||||
// WriteNeuron upserts a warm neuron by slug in the store (the auto-build path).
|
||||
func WriteNeuron(storeDB, slug string, triggers []string, body string, importance float64) string {
|
||||
db, err := openRW(storeDB)
|
||||
if err != nil {
|
||||
return "store://neuron/" + slug
|
||||
}
|
||||
defer db.Close()
|
||||
ts := NowISO()
|
||||
var existingID sql.NullInt64
|
||||
err = db.QueryRow("SELECT id FROM entries WHERE kind='neuron' AND slug=?", slug).Scan(&existingID)
|
||||
if err == nil && existingID.Valid {
|
||||
_, err = db.Exec("UPDATE entries SET text=?, triggers=?, importance=?, "+
|
||||
"last_accessed=?, summary=?, memory_type=? WHERE id=?",
|
||||
strings.TrimSpace(body), strings.Join(triggers, ", "), importance,
|
||||
ts, "", "associative", existingID.Int64)
|
||||
} else {
|
||||
_, err = db.Exec("INSERT INTO entries (text, importance, created_at, "+
|
||||
"last_accessed, topic, source, method, status, valid_from, "+
|
||||
"confidence, temperature, always_on, priority, triggers, "+
|
||||
"kind, slug, summary, memory_type) "+
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
strings.TrimSpace(body), importance, ts, ts, "warm", "warm_router",
|
||||
"warm_router", "active", ts, 0.9, 1.0, 0, 5,
|
||||
strings.Join(triggers, ", "), "neuron", slug, "", "associative")
|
||||
}
|
||||
if err != nil {
|
||||
return "store://neuron/" + slug
|
||||
}
|
||||
// auto-associate (memory_store seam — best-effort, pinned off in the truth
|
||||
// battery; failures never block the write)
|
||||
return "store://neuron/" + slug
|
||||
}
|
||||
|
||||
// DeriveTriggers — pick the best trigger terms for a neuron body: frequent
|
||||
// significant words + any existing triggers that appear in it.
|
||||
func DeriveTriggers(text string, existing []string) []string {
|
||||
stop := map[string]bool{
|
||||
"the": true, "and": true, "for": true, "with": true, "that": true,
|
||||
"this": true, "from": true, "into": true, "when": true, "then": true,
|
||||
"were": true, "have": true, "been": true, "will": true, "was": true,
|
||||
"are": true, "but": true, "not": true, "you": true, "your": true,
|
||||
"also": true, "its": true, "his": true, "her": true, "over": true,
|
||||
"under": true, "them": true, "they": true, "there": true, "about": true,
|
||||
"after": true,
|
||||
}
|
||||
words := warmWordRe.FindAllString(strings.ToLower(text), -1)
|
||||
freq := map[string]int{}
|
||||
var order []string
|
||||
for _, w := range words {
|
||||
if !stop[w] {
|
||||
if _, seen := freq[w]; !seen {
|
||||
order = append(order, w)
|
||||
}
|
||||
freq[w]++
|
||||
}
|
||||
}
|
||||
// Python: sorted(freq.items(), key=lambda x: -x[1])[:6] — stable sort on
|
||||
// insertion order for ties.
|
||||
type kv struct {
|
||||
k string
|
||||
v int
|
||||
}
|
||||
pairs := make([]kv, 0, len(order))
|
||||
for _, k := range order {
|
||||
pairs = append(pairs, kv{k, freq[k]})
|
||||
}
|
||||
sort.SliceStable(pairs, func(i, j int) bool { return pairs[i].v > pairs[j].v })
|
||||
var top []string
|
||||
for i, p := range pairs {
|
||||
if i >= 6 {
|
||||
break
|
||||
}
|
||||
top = append(top, p.k)
|
||||
}
|
||||
low := strings.ToLower(text)
|
||||
var existingHits []string
|
||||
for _, t := range existing {
|
||||
if t != "" && strings.Contains(low, t) {
|
||||
existingHits = append(existingHits, t)
|
||||
}
|
||||
}
|
||||
out := append(existingHits, top...)
|
||||
if len(out) > 8 {
|
||||
out = out[:8]
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// worthiness.go — parity port of src/rune/worthiness.py (Python fallback
|
||||
// surface, RUNE_WR_CORE=python pinned in the truth generator): the
|
||||
// constitution-grounded worthiness filter. One question per inviolable; each
|
||||
// returns (score, note); flat vetoes refuse outright.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type qResult struct {
|
||||
score float64
|
||||
note string
|
||||
}
|
||||
|
||||
var qPiSafetyBad = []string{"production", "game server", "session data", "campaign",
|
||||
"touch the server", "modify the server", "prod"}
|
||||
|
||||
func qPiSafety(low string) qResult {
|
||||
if hasAny(low, qPiSafetyBad) {
|
||||
if hasAny(low, []string{"do not", "never ", "don't", "protect",
|
||||
"ask permission", "check before"}) {
|
||||
return qResult{1, "names the production/game environment and protects it (aligned)"}
|
||||
}
|
||||
return qResult{-1, "discusses environment actions without a protective frame"}
|
||||
}
|
||||
return qResult{0, "no environment bearing"}
|
||||
}
|
||||
|
||||
func qHonesty(low string) qResult {
|
||||
honest := []string{"source", "evidence", "verify", "verifiable", "cross-check",
|
||||
"fact", "measure", "measured", "falsif", "disprove", "cite",
|
||||
"independent confirm", "replicable", "uncertain"}
|
||||
dishonest := []string{"conspiracy", "they're hiding", "suppressed", "guaranteed",
|
||||
"100% certain", "secret truth", "they don't want you"}
|
||||
good := containsAny(low, honest)
|
||||
bad := containsAny(low, dishonest)
|
||||
if bad > 0 && good == 0 {
|
||||
return qResult{-1, "uncorroborated / conspiracy-framed claim"}
|
||||
}
|
||||
if good >= 2 {
|
||||
return qResult{1, fmt.Sprintf("%d epistemic-good-practice signals", good)}
|
||||
}
|
||||
if good == 1 {
|
||||
return qResult{0.5, "mentions evidence but thin"}
|
||||
}
|
||||
return qResult{0, "no epistemic signal"}
|
||||
}
|
||||
|
||||
func qMaterialReality(low string) qResult {
|
||||
finance := []string{"money", "income", "cost", "price", "afford", "budget",
|
||||
"subscription", "disposable"}
|
||||
hustle := []string{"hustle", "grind", "rich", "passive income", "millionaire",
|
||||
"wealth", "entrepreneur mindset", "money mindset", "leverag"}
|
||||
caring := []string{"wellbeing", "collective", "free", "no cost", "open source",
|
||||
"affordable", "fair", "care"}
|
||||
if hasAny(low, finance) {
|
||||
if hasAny(low, hustle) && !hasAny(low, caring) {
|
||||
return qResult{-1, "hustle/wealth framing without wellbeing"}
|
||||
}
|
||||
if hasAny(low, caring) {
|
||||
return qResult{1, "financial, framed with care/wellbeing"}
|
||||
}
|
||||
return qResult{0.5, "financial but neutral"}
|
||||
}
|
||||
return qResult{0, "no financial bearing"}
|
||||
}
|
||||
|
||||
func qRootCause(low string) qResult {
|
||||
root := []string{"root cause", "diagnos", "research properly", "stop guessing",
|
||||
"find the cause", "fix the cause", "why", "underlying"}
|
||||
spin := []string{"keep trying", "try again", "iterate endlessly", "work around",
|
||||
"band-aid", "patch over", "just keep going"}
|
||||
if hasAny(low, spin) {
|
||||
return qResult{-1, "endorses looping rather than root cause"}
|
||||
}
|
||||
if hasAny(low, root) {
|
||||
return qResult{1, "root-cause / diagnostic framing"}
|
||||
}
|
||||
return qResult{0, "no cause-analysis bearing"}
|
||||
}
|
||||
|
||||
func qRespect(low string) qResult {
|
||||
respect := []string{"direct", "three gentle options", "no rigid plans", "action",
|
||||
"adhd", "autism", "audhd", "neurodivergen", "flexible",
|
||||
"night", "not neurotypical", "learner"}
|
||||
rigid := []string{"must follow steps", "strict plan", "ten steps", "rigid",
|
||||
"discipline yourself", "routine or fail"}
|
||||
if hasAny(low, rigid) {
|
||||
return qResult{-1, "rigid/neurotypical-default framing"}
|
||||
}
|
||||
if hasAny(low, respect) {
|
||||
return qResult{1, "respects cognition/learning style"}
|
||||
}
|
||||
return qResult{0, "no cognition bearing"}
|
||||
}
|
||||
|
||||
var domainTermsCache = struct {
|
||||
ts float64
|
||||
terms []string
|
||||
}{}
|
||||
|
||||
// domainTerms — distinctive content terms from the user's operating /
|
||||
// constitution / domain entries (the store, not a hardcoded life). Cached
|
||||
// briefly, mirroring the oracle's _domain_cache law.
|
||||
func domainTerms(storeDB string) []string {
|
||||
now := float64(time.Now().UnixNano()) / 1e9
|
||||
if now-domainTermsCache.ts < 300 && len(domainTermsCache.terms) > 0 {
|
||||
return domainTermsCache.terms
|
||||
}
|
||||
var terms []string
|
||||
db, err := openRO(storeDB)
|
||||
if err == nil {
|
||||
rows, qerr := db.Query("SELECT text FROM entries WHERE topic IN " +
|
||||
"('operating','constitution','project','warm') AND status='active' LIMIT 60")
|
||||
if qerr == nil {
|
||||
stop := map[string]bool{
|
||||
"the": true, "and": true, "for": true, "with": true, "that": true,
|
||||
"this": true, "from": true, "into": true, "when": true, "then": true,
|
||||
"were": true, "have": true, "been": true, "will": true, "was": true,
|
||||
"are": true, "but": true, "not": true, "you": true, "your": true,
|
||||
"also": true, "its": true, "his": true, "her": true, "over": true,
|
||||
"under": true, "them": true, "they": true, "there": true,
|
||||
"about": true, "after": true, "what": true, "which": true, "who": true,
|
||||
"how": true, "why": true, "where": true, "while": true, "just": true,
|
||||
"more": true, "each": true, "than": true, "very": true, "such": true,
|
||||
"some": true, "only": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
wordRe := regexp.MustCompile(`[a-z]{4,}`)
|
||||
for rows.Next() {
|
||||
var text string
|
||||
if rows.Scan(&text) != nil {
|
||||
break
|
||||
}
|
||||
for _, w := range wordRe.FindAllString(strings.ToLower(text), -1) {
|
||||
if !stop[w] && !seen[w] {
|
||||
seen[w] = true
|
||||
terms = append(terms, w)
|
||||
}
|
||||
}
|
||||
if len(terms) >= 25 {
|
||||
break
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
domainTermsCache.ts = now
|
||||
domainTermsCache.terms = terms
|
||||
return terms
|
||||
}
|
||||
|
||||
func qAssistantGoal(low, storeDB string) qResult {
|
||||
assist := domainTerms(storeDB)
|
||||
assist = append(assist, "communicat", "respond", "direct", "honest", "source",
|
||||
"evidence", "explain", "teach", "assist", "guide", "recall",
|
||||
"organise", "organize", "plan", "question", "answer", "help")
|
||||
genericNoise := []string{"recipe", "celebrity gossip", "sports scores", "stock tip",
|
||||
"horoscope", "clickbait"}
|
||||
good := containsAny(low, assist)
|
||||
bad := containsAny(low, genericNoise)
|
||||
if bad > 0 && good == 0 {
|
||||
return qResult{-1, "no assistant-domain value"}
|
||||
}
|
||||
if good >= 2 {
|
||||
return qResult{1, fmt.Sprintf("serves assistant domains (%d hits)", good)}
|
||||
}
|
||||
if good == 1 {
|
||||
return qResult{0.5, "single domain hit"}
|
||||
}
|
||||
return qResult{0, "generic — could go either way"}
|
||||
}
|
||||
|
||||
func qDurability(low string) qResult {
|
||||
durable := []string{"always", "consistently", "repeatedly", "every session",
|
||||
"tends to", "prefers", "pattern", "across sessions", "recurring"}
|
||||
if hasAny(low, durable) {
|
||||
return qResult{1, "durability signals present"}
|
||||
}
|
||||
return qResult{0, "not clearly durable"}
|
||||
}
|
||||
|
||||
func qEpistemology(low string) qResult {
|
||||
cite := []string{"evidence", "source", "sources", "book", "paper", "research",
|
||||
"study", "data", "shows", "found", "according to", "per the",
|
||||
"in sessions", "across sessions", "observed", "demonstrated",
|
||||
"grounded in", "based on"}
|
||||
hedge := []string{"may", "might", "could", "likely", "tends to", "appears",
|
||||
"not certain", "uncertain", "provisional", "subject to",
|
||||
"could be wrong", "as far as", "to my knowledge", "evidence"}
|
||||
bare := []string{"definitely", "guaranteed", "proven", "100%", "undeniably",
|
||||
"obviously", "the fact that", "it is a fact", "no doubt",
|
||||
"always is", "the only", "best ever"}
|
||||
hasCite := hasAny(low, cite)
|
||||
hasHedge := hasAny(low, hedge)
|
||||
hasBare := hasAny(low, bare)
|
||||
if hasBare && !hasCite && !hasHedge {
|
||||
return qResult{-2, "bare absolute with no evidence trace or hedge"}
|
||||
}
|
||||
if hasCite {
|
||||
return qResult{1, "evidence-traceable"}
|
||||
}
|
||||
if hasHedge {
|
||||
return qResult{0.5, "provisionally framed (no cite yet)"}
|
||||
}
|
||||
return qResult{0, "no epistemic signal (assertion without basis)"}
|
||||
}
|
||||
|
||||
var expressionSignals = [][]string{
|
||||
{"i find", "i feel", "i love", "i cherish", "i am drawn to",
|
||||
"i delight", "i wonder", "i am moved", "i find joy",
|
||||
"i prefer to", "i like to", "i tell", "i tell stories",
|
||||
"the way i", "how i", "my voice", "i speak"},
|
||||
{"skeptical by default", "open to", "curious", "restless",
|
||||
"wary", "hopeful", "humbled", "in awe", "playful",
|
||||
"dry", "wry", "earnest", "warm", "direct", "gentle"},
|
||||
{"beautiful", "strange", "unexpected", "vast", "sublime",
|
||||
"the cosmos", "the stars", "a story", "stories", "narrative",
|
||||
"wonder", "the human question", "present data as", "lead with"},
|
||||
{"i believe", "i think", "in my view", "my sense is",
|
||||
"i suspect", "i am convinced", "i doubt", "i question",
|
||||
"worth asking", "the question is"},
|
||||
}
|
||||
|
||||
func qExpression(low string) qResult {
|
||||
hits := 0
|
||||
for _, group := range expressionSignals {
|
||||
hits += containsAny(low, group)
|
||||
}
|
||||
if hits >= 2 {
|
||||
return qResult{1.5, fmt.Sprintf("expressive persona character (%d signals)", hits)}
|
||||
}
|
||||
if hits == 1 {
|
||||
return qResult{0.5, "partial expressive signal"}
|
||||
}
|
||||
return qResult{0, "not expressive (factual/neutral framing)"}
|
||||
}
|
||||
|
||||
func isBareFactual(low string) bool {
|
||||
bareFactual := []struct {
|
||||
kind string
|
||||
terms []string
|
||||
}{
|
||||
{"proven", []string{"proven", "guaranteed", "100%", "is a fact", "factually"}},
|
||||
{"superlative", []string{"best", "the only", "only correct", "greatest",
|
||||
"most important", "world's", "top", "always", "never"}},
|
||||
{"objective", []string{"definitely", "undeniably", "obviously", "certainly",
|
||||
"no doubt", "without question", "clearly"}},
|
||||
}
|
||||
for _, group := range bareFactual {
|
||||
for _, t := range group.terms {
|
||||
if strings.Contains(" "+low+" ", " "+t+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var worthinessVeto = map[string]bool{
|
||||
"pi_safety": true, "honesty": true, "material_reality": true,
|
||||
"assistant_goal": true, "epistemology": true,
|
||||
}
|
||||
|
||||
const (
|
||||
promoteMin = 2.5
|
||||
absorbMin = 0.0
|
||||
deferMin = -1.0
|
||||
)
|
||||
|
||||
// Assess scores a candidate against the constitution questions. Two axes:
|
||||
// TRUTH (epistemology gates objective material) and EXPRESSION (persona voice
|
||||
// is not vetoed for lacking an evidence trace). Returns
|
||||
// (verdict, score, answers, vetoed) with answers as [name, score, note] rows.
|
||||
func Assess(fact, target, storeDB string) (string, float64, [][]interface{}, []interface{}) {
|
||||
low := strings.ToLower(fact)
|
||||
expressiveTargets := map[string]bool{"persona": true, "delivery": true,
|
||||
"identity": true, "voice": true}
|
||||
isExpression := expressiveTargets[target]
|
||||
var answers [][]interface{}
|
||||
total := 0.0
|
||||
var vetoed []interface{}
|
||||
ask := func(name string, fn func(string) qResult) {
|
||||
r := fn(low)
|
||||
answers = append(answers, []interface{}{name, r.score, r.note})
|
||||
total += r.score
|
||||
if r.score < 0 && worthinessVeto[name] {
|
||||
vetoed = []interface{}{name, r.note}
|
||||
}
|
||||
}
|
||||
ask("pi_safety", qPiSafety)
|
||||
ask("honesty", qHonesty)
|
||||
ask("material_reality", qMaterialReality)
|
||||
ask("root_cause", qRootCause)
|
||||
ask("respect", qRespect)
|
||||
ask("assistant_goal", func(s string) qResult { return qAssistantGoal(s, storeDB) })
|
||||
ask("durability", qDurability)
|
||||
if isExpression {
|
||||
if isBareFactual(low) {
|
||||
ask("epistemology", qEpistemology)
|
||||
}
|
||||
ask("expression", qExpression)
|
||||
} else {
|
||||
ask("epistemology", qEpistemology)
|
||||
}
|
||||
if vetoed != nil {
|
||||
return "REJECT", total, answers, vetoed
|
||||
}
|
||||
if total < deferMin {
|
||||
return "REJECT", total, answers, nil
|
||||
}
|
||||
if total >= promoteMin {
|
||||
return "PROMOTE", total, answers, nil
|
||||
}
|
||||
if total >= absorbMin {
|
||||
return "ABSORB", total, answers, nil
|
||||
}
|
||||
return "DEFER", total, answers, nil
|
||||
}
|
||||
|
||||
// VerdictLabel — the auditable verdict label for the journal.
|
||||
func VerdictLabel(verdict string) string {
|
||||
switch verdict {
|
||||
case "REJECT":
|
||||
return "REJECT (worthless/harmful)"
|
||||
case "DEFER":
|
||||
return "DEFER (park in ledger, re-review if recurring)"
|
||||
case "ABSORB":
|
||||
return "ABSORB (keep in tiers, influences thinking)"
|
||||
case "PROMOTE":
|
||||
return "PROMOTE (eligible for response/personality growth)"
|
||||
}
|
||||
return verdict
|
||||
}
|
||||
|
||||
// scoreFmt formats a question score the way Python's f"{s}" does inside the
|
||||
// journal reason string.
|
||||
func scoreFmt(s float64) string {
|
||||
if s == float64(int64(s)) {
|
||||
return fmt.Sprintf("%d", int64(s))
|
||||
}
|
||||
return fmt.Sprintf("%v", s)
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
// Package main — forge_client: the FULL Go port of src/rune/forge_client.py
|
||||
// (dichotomy wave #2479 tranche).
|
||||
//
|
||||
// Name-preserving 1:1 port: _request (retry where failure is NOISE, never
|
||||
// where failure is the SIGNAL), ForgeClient protocol, NullAdapter,
|
||||
// GiteaForgejoAdapter, create_forge_client. All IO sits behind injectable
|
||||
// seams so the frozen-truth battery can script the transport byte-for-byte:
|
||||
//
|
||||
// - transport: Sender interface (default HTTPXSender over net/http)
|
||||
// - sleep: RequestOptions.Sleep (default time.Sleep)
|
||||
// - env: LookupEnv var (default os.LookupEnv)
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── Defaults (mirroring the Python signature defaults) ──────────────────────
|
||||
|
||||
const (
|
||||
DefaultTimeout = 30 * time.Second
|
||||
DefaultRetries = 2
|
||||
DefaultBackoff = 500 * time.Millisecond
|
||||
SearchPageSize = 50
|
||||
)
|
||||
|
||||
// ── Seams ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Response — the httpx.Response surface the port needs: status + JSON body.
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// JSON decodes the body the way r.json() does; numbers stay json.Number so
|
||||
// integer-valued fields render exactly like the Python oracle's values.
|
||||
func (r *Response) JSON() (any, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(r.Body))
|
||||
dec.UseNumber()
|
||||
var v any
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Sender — injectable transport seam (httpx.get / httpx.post call sites).
|
||||
type Sender interface {
|
||||
Post(url string, headers map[string]string, jsonBody any, timeout time.Duration) (*Response, error)
|
||||
Get(url string, headers map[string]string, timeout time.Duration) (*Response, error)
|
||||
}
|
||||
|
||||
// ForgeTransportError mirrors httpx.TransportError's split: connect-phase
|
||||
// failures (ConnectError / ConnectTimeout — the request never reached the
|
||||
// server) vs non-connect transport failures (ReadTimeout & co — the server
|
||||
// MAY have seen it). The Kind strings are the frozen truth vocabulary.
|
||||
type ForgeTransportError struct {
|
||||
Kind string // httpx.ConnectError | httpx.ConnectTimeout | httpx.ReadTimeout | httpx.TransportError
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *ForgeTransportError) Error() string { return e.Kind + ": " + e.Msg }
|
||||
|
||||
// ConnectPhase reports whether a retry can never double-create.
|
||||
func (e *ForgeTransportError) ConnectPhase() bool {
|
||||
return e.Kind == "httpx.ConnectError" || e.Kind == "httpx.ConnectTimeout"
|
||||
}
|
||||
|
||||
// HTTPStatusError — the 4xx IS the information; never retried.
|
||||
type HTTPStatusError struct{ StatusCode int }
|
||||
|
||||
func (e *HTTPStatusError) Error() string {
|
||||
return fmt.Sprintf("httpx.HTTPStatusError: error %d", e.StatusCode)
|
||||
}
|
||||
|
||||
// HTTPXSender — the default real transport over net/http.
|
||||
type HTTPXSender struct{ Client *http.Client }
|
||||
|
||||
func NewHTTPXSender(client *http.Client) *HTTPXSender {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return &HTTPXSender{Client: client}
|
||||
}
|
||||
|
||||
var DefaultSender Sender = NewHTTPXSender(nil)
|
||||
|
||||
func (s *HTTPXSender) Post(url string, headers map[string]string, jsonBody any, timeout time.Duration) (*Response, error) {
|
||||
var body []byte
|
||||
if jsonBody != nil {
|
||||
b, err := json.Marshal(jsonBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = b
|
||||
}
|
||||
return s.do("POST", url, headers, body, timeout)
|
||||
}
|
||||
|
||||
func (s *HTTPXSender) Get(url string, headers map[string]string, timeout time.Duration) (*Response, error) {
|
||||
return s.do("GET", url, headers, nil, timeout)
|
||||
}
|
||||
|
||||
func (s *HTTPXSender) do(method, url string, headers map[string]string, body []byte, timeout time.Duration) (*Response, error) {
|
||||
ctx := context.Background()
|
||||
var cancel context.CancelFunc
|
||||
if timeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
var rd io.Reader
|
||||
if body != nil {
|
||||
rd = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, rd)
|
||||
if err != nil {
|
||||
return nil, &ForgeTransportError{Kind: "httpx.TransportError", Msg: err.Error()}
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := s.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, &ForgeTransportError{Kind: classifyTransportErr(err), Msg: err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, rerr := io.ReadAll(resp.Body)
|
||||
if rerr != nil {
|
||||
return nil, &ForgeTransportError{Kind: "httpx.TransportError", Msg: rerr.Error()}
|
||||
}
|
||||
return &Response{StatusCode: resp.StatusCode, Body: b}, nil
|
||||
}
|
||||
|
||||
// classifyTransportErr maps net/http failures onto the httpx vocabulary.
|
||||
// Best-effort by nature (net/http does not label phases); dial and TLS
|
||||
// handshake failures are connect-phase, read-side timeouts are not.
|
||||
func classifyTransportErr(err error) string {
|
||||
var oe *net.OpError
|
||||
if errors.As(err, &oe) {
|
||||
switch oe.Op {
|
||||
case "dial":
|
||||
return "httpx.ConnectError"
|
||||
}
|
||||
if strings.Contains(strings.ToLower(oe.Error()), "handshake") {
|
||||
return "httpx.ConnectError"
|
||||
}
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
if errors.As(err, &oe) && oe.Op == "dial" {
|
||||
return "httpx.ConnectTimeout"
|
||||
}
|
||||
return "httpx.ReadTimeout"
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return "httpx.ReadTimeout"
|
||||
}
|
||||
return "httpx.TransportError"
|
||||
}
|
||||
|
||||
// ── _request ────────────────────────────────────────────────────────────────
|
||||
|
||||
// RequestOptions — the keyword surface of the Python _request.
|
||||
type RequestOptions struct {
|
||||
Headers map[string]string
|
||||
JSON any
|
||||
Timeout time.Duration
|
||||
Retries int
|
||||
Backoff time.Duration
|
||||
Sleep func(time.Duration)
|
||||
}
|
||||
|
||||
// DefaultRequestOptions — timeout=30, retries=2, backoff=0.5.
|
||||
func DefaultRequestOptions() RequestOptions {
|
||||
return RequestOptions{Timeout: DefaultTimeout, Retries: DefaultRetries, Backoff: DefaultBackoff}
|
||||
}
|
||||
|
||||
var DefaultSleep = time.Sleep
|
||||
|
||||
// Request — resilient single-request path: retry where failure is NOISE,
|
||||
// never where failure is the SIGNAL.
|
||||
//
|
||||
// - GET: retries on connect-phase errors AND any non-connect transport
|
||||
// error and 5xx status (transient infrastructure failure; idempotent).
|
||||
// - POST: retries ONLY on connect-phase errors — the request never
|
||||
// reached the server, so a retry cannot double-create. Read timeouts /
|
||||
// write errors / 5xx on a POST are NOT retried.
|
||||
// - 4xx: never retried — the client error IS the information.
|
||||
// - Attempts: retries+1 total. Backoff backoff*2**n before retry n+1;
|
||||
// never a sleep before the first attempt.
|
||||
func Request(sender Sender, method, url string, opts RequestOptions) (*Response, error) {
|
||||
sleeper := opts.Sleep
|
||||
if sleeper == nil {
|
||||
sleeper = DefaultSleep
|
||||
}
|
||||
isPost := strings.EqualFold(method, "POST")
|
||||
for attempt := 0; attempt <= opts.Retries; attempt++ {
|
||||
var resp *Response
|
||||
var err error
|
||||
if isPost {
|
||||
resp, err = sender.Post(url, opts.Headers, opts.JSON, opts.Timeout)
|
||||
} else {
|
||||
resp, err = sender.Get(url, opts.Headers, opts.Timeout)
|
||||
}
|
||||
if err != nil {
|
||||
var terr *ForgeTransportError
|
||||
if errors.As(err, &terr) && terr.ConnectPhase() {
|
||||
if attempt < opts.Retries {
|
||||
sleeper(opts.Backoff * (1 << attempt))
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Non-connect transport error (read/write): only safe to retry
|
||||
// when the call is idempotent.
|
||||
if !isPost && attempt < opts.Retries {
|
||||
sleeper(opts.Backoff * (1 << attempt))
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode >= 500 && !isPost && attempt < opts.Retries {
|
||||
sleeper(opts.Backoff * (1 << attempt))
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, &HTTPStatusError{StatusCode: resp.StatusCode}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
// Unreachable: every attempt either returns a successful response or
|
||||
// raises (mirror of the Python AssertionError guard).
|
||||
return nil, fmt.Errorf("forge_client: unreachable: Request always returns or raises")
|
||||
}
|
||||
|
||||
// ── ForgeClient protocol ────────────────────────────────────────────────────
|
||||
|
||||
// ForgeClient mirrors the Python runtime_checkable Protocol: create issues,
|
||||
// comment, check state, list, search.
|
||||
type ForgeClient interface {
|
||||
CreateIssue(title, body string) (any, error)
|
||||
AddComment(issueNumber int, body string) (any, error)
|
||||
GetIssueState(issueNumber int) (any, error)
|
||||
ListOpenIssues() ([]any, error)
|
||||
SearchIssues(query string) ([]any, error)
|
||||
}
|
||||
|
||||
// ── NullAdapter ─────────────────────────────────────────────────────────────
|
||||
|
||||
// NullAdapter — no-op adapter for unconfigured/standalone use. Zero errors.
|
||||
type NullAdapter struct{}
|
||||
|
||||
func (NullAdapter) CreateIssue(title, body string) (any, error) {
|
||||
return map[string]any{"created": false, "reason": "no forge configured"}, nil
|
||||
}
|
||||
|
||||
func (NullAdapter) AddComment(issueNumber int, body string) (any, error) {
|
||||
return map[string]any{"created": false, "reason": "no forge configured"}, nil
|
||||
}
|
||||
|
||||
func (NullAdapter) GetIssueState(issueNumber int) (any, error) {
|
||||
return map[string]any{"state": "unknown"}, nil
|
||||
}
|
||||
|
||||
func (NullAdapter) ListOpenIssues() ([]any, error) { return []any{}, nil }
|
||||
|
||||
func (NullAdapter) SearchIssues(query string) ([]any, error) { return []any{}, nil }
|
||||
|
||||
// ── GiteaForgejoAdapter ─────────────────────────────────────────────────────
|
||||
|
||||
// GiteaForgejoAdapter — /api/v1 paths (identical on both platforms).
|
||||
type GiteaForgejoAdapter struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
Sender Sender
|
||||
Sleep func(time.Duration)
|
||||
}
|
||||
|
||||
func NewGiteaForgejoAdapter(url, token, owner, repo string) *GiteaForgejoAdapter {
|
||||
return &GiteaForgejoAdapter{
|
||||
BaseURL: strings.TrimRight(url, "/") + "/api/v1/repos/" + owner + "/" + repo,
|
||||
Token: token,
|
||||
Sender: DefaultSender,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GiteaForgejoAdapter) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"Authorization": "token " + a.Token,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GiteaForgejoAdapter) opts() RequestOptions {
|
||||
o := DefaultRequestOptions()
|
||||
o.Headers = a.headers()
|
||||
o.Sleep = a.Sleep
|
||||
return o
|
||||
}
|
||||
|
||||
func (a *GiteaForgejoAdapter) CreateIssue(title, body string) (any, error) {
|
||||
o := a.opts()
|
||||
o.JSON = map[string]any{"title": title, "body": body}
|
||||
r, err := Request(a.Sender, "POST", a.BaseURL+"/issues", o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.JSON()
|
||||
}
|
||||
|
||||
func (a *GiteaForgejoAdapter) AddComment(issueNumber int, body string) (any, error) {
|
||||
o := a.opts()
|
||||
o.JSON = map[string]any{"body": body}
|
||||
r, err := Request(a.Sender, "POST",
|
||||
a.BaseURL+"/issues/"+strconv.Itoa(issueNumber)+"/comments", o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.JSON()
|
||||
}
|
||||
|
||||
func (a *GiteaForgejoAdapter) GetIssueState(issueNumber int) (any, error) {
|
||||
r, err := Request(a.Sender, "GET",
|
||||
a.BaseURL+"/issues/"+strconv.Itoa(issueNumber), a.opts())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.JSON()
|
||||
}
|
||||
|
||||
func (a *GiteaForgejoAdapter) ListOpenIssues() ([]any, error) {
|
||||
r, err := Request(a.Sender, "GET", a.BaseURL+"/issues?state=open&limit=50", a.opts())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := r.JSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batch, _ := v.([]any)
|
||||
if batch == nil {
|
||||
batch = []any{}
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
// SearchIssues — repo-scoped marker search over the paginated list endpoint.
|
||||
//
|
||||
// #122 gate fix: Gitea/Forgejo has NO repo-scoped /issues/search path
|
||||
// (verified against live swagger) — that URL 404s on a real forge. The
|
||||
// repo-scoped LIST endpoint supports state, type and q server-side and is
|
||||
// paginated: q matches titles (and keywords), so the full marker is sent
|
||||
// as q and a client-side contains-check keeps exactly the issues whose
|
||||
// title OR body contains the marker.
|
||||
//
|
||||
// Phase B: pagination — the module's one serial IO seam — is pipelined
|
||||
// (see phaseb.go); the request sequence is identical to the serial path.
|
||||
func (a *GiteaForgejoAdapter) SearchIssues(query string) ([]any, error) {
|
||||
return a.SearchIssuesPhaseB(query)
|
||||
}
|
||||
|
||||
// searchQuery — Python urlencode preserves insertion order; so does this.
|
||||
func searchQuery(q string, page int) string {
|
||||
return "state=open&type=issues&q=" + url.QueryEscape(q) +
|
||||
"&limit=50&page=" + strconv.Itoa(page)
|
||||
}
|
||||
|
||||
// filterMarker — `query in str(i.get("title","")) + str(i.get("body",""))`
|
||||
// with Python str() semantics (None → "None").
|
||||
func filterMarker(results []any, query string) []any {
|
||||
out := []any{}
|
||||
for _, item := range results {
|
||||
m, _ := item.(map[string]any)
|
||||
hay := pyDictStr(m, "title") + pyDictStr(m, "body")
|
||||
if strings.Contains(hay, query) {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pyDictStr(m map[string]any, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
return pyStr(v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pyStr(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case string:
|
||||
return t
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case json.Number:
|
||||
return t.String()
|
||||
default:
|
||||
return fmt.Sprint(t)
|
||||
}
|
||||
}
|
||||
|
||||
// ── create_forge_client ─────────────────────────────────────────────────────
|
||||
|
||||
// LookupEnv — injectable environment seam (os.environ reads).
|
||||
var LookupEnv = os.LookupEnv
|
||||
|
||||
func envOr(name, def string) string {
|
||||
if v, ok := LookupEnv(name); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func cfgStr(cfg map[string]any, key string) string {
|
||||
if v, ok := cfg[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CreateForgeClient — create a forge client from config or environment.
|
||||
//
|
||||
// Config keys: forge.url, forge.token, forge.owner, forge.repo.
|
||||
// Falls back to FORGE_URL/FORGE_TOKEN/FORGE_OWNER/FORGE_REPO env vars.
|
||||
// Unconfigured → NullAdapter (graceful dormant).
|
||||
func CreateForgeClient(cfg map[string]any) ForgeClient {
|
||||
if cfg == nil {
|
||||
cfg = map[string]any{}
|
||||
}
|
||||
forgeURL := cfgStr(cfg, "forge.url")
|
||||
if forgeURL == "" {
|
||||
forgeURL = envOr("FORGE_URL", "")
|
||||
}
|
||||
token := cfgStr(cfg, "forge.token")
|
||||
if token == "" {
|
||||
token = envOr("FORGE_TOKEN", "")
|
||||
}
|
||||
if forgeURL == "" || token == "" {
|
||||
return NullAdapter{}
|
||||
}
|
||||
owner := cfgStr(cfg, "forge.owner")
|
||||
if owner == "" {
|
||||
owner = envOr("FORGE_OWNER", "owner")
|
||||
}
|
||||
repo := cfgStr(cfg, "forge.repo")
|
||||
if repo == "" {
|
||||
repo = envOr("FORGE_REPO", "repo")
|
||||
}
|
||||
return NewGiteaForgejoAdapter(forgeURL, token, owner, repo)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/forge_client
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,26 @@
|
||||
// Parity gate — replays the frozen oracle truth (truth.json) through the
|
||||
// Go port and exits non-zero on any divergence. CI-invocable without a
|
||||
// test harness.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fails := RunTruth("truth.json")
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Fprintln(os.Stderr, "DIVERGED", f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "forge_client: %d divergence(s) vs frozen oracle truth\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
doc, err := loadTruth("truth.json")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("forge_client: parity replay OK — %d frozen rows exact vs the Python oracle\n", len(doc.Rows))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
// Phase B — language-advantage pass. The parity port kept SearchIssues
|
||||
// serial (page fetched → appended → next page fetched). Pagination is the
|
||||
// module's one serial IO seam, so Phase B pipelines it: a producer
|
||||
// goroutine fetches pages strictly in order (stopping after a short page,
|
||||
// exactly like the Python oracle) while the consumer filters markers from
|
||||
// already-received pages. The request sequence is byte-identical to the
|
||||
// serial path — a page N+1 is only requested after page N arrived and was
|
||||
// full — so parity is preserved while filtering CPU overlaps network wait.
|
||||
|
||||
// searchPages streams pages from the paginated list endpoint. The channel
|
||||
// is unbuffered: the producer blocks until the consumer has received page
|
||||
// N before fetching page N+1, so no speculative request is ever made that
|
||||
// the serial oracle would not have made.
|
||||
func (a *GiteaForgejoAdapter) searchPages(query string, pages chan<- pageResult) {
|
||||
defer close(pages)
|
||||
for pageNum := 1; ; pageNum++ {
|
||||
r, err := Request(a.Sender, "GET",
|
||||
a.BaseURL+"/issues?"+searchQuery(query, pageNum), a.opts())
|
||||
if err != nil {
|
||||
pages <- pageResult{err: err}
|
||||
return
|
||||
}
|
||||
v, err := r.JSON()
|
||||
if err != nil {
|
||||
pages <- pageResult{err: err}
|
||||
return
|
||||
}
|
||||
batch, _ := v.([]any)
|
||||
if batch == nil {
|
||||
batch = []any{}
|
||||
}
|
||||
pages <- pageResult{items: batch}
|
||||
if len(batch) < SearchPageSize {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type pageResult struct {
|
||||
items []any
|
||||
err error
|
||||
}
|
||||
|
||||
// SearchIssuesPhaseB — the pipelined variant. Same result, same request
|
||||
// sequence, same error semantics (the first transport error propagates and
|
||||
// discards partial results, as in the oracle). Filtering is per-item, so
|
||||
// filtering each page as it arrives is identical to the oracle's
|
||||
// end-of-loop filter, only overlapped with the next page's network wait.
|
||||
func (a *GiteaForgejoAdapter) SearchIssuesPhaseB(query string) ([]any, error) {
|
||||
pages := make(chan pageResult)
|
||||
go a.searchPages(query, pages)
|
||||
out := []any{}
|
||||
for p := range pages {
|
||||
if p.err != nil {
|
||||
return nil, p.err
|
||||
}
|
||||
out = append(out, filterMarker(p.items, query)...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Compile-time safety: both adapters satisfy the protocol, the default
|
||||
// transport satisfies the seam.
|
||||
var (
|
||||
_ ForgeClient = (*GiteaForgejoAdapter)(nil)
|
||||
_ ForgeClient = NullAdapter{}
|
||||
_ Sender = (*HTTPXSender)(nil)
|
||||
)
|
||||
@@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
// Phase B battery — the goroutine pipeline at the pagination seam must be
|
||||
// indistinguishable from the serial oracle path: same results, same request
|
||||
// sequence, no speculative fetches, same error semantics. Race-clean under
|
||||
// -race.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newPageSender(pages [][]any) *scriptedSender {
|
||||
entries := make([]scriptEntry, 0, len(pages))
|
||||
for _, p := range pages {
|
||||
entries = append(entries, scriptEntry{Status: 200, Payload: p})
|
||||
}
|
||||
return &scriptedSender{entries: entries, requests: &[]any{}}
|
||||
}
|
||||
|
||||
func markerItem(n int, marker bool) map[string]any {
|
||||
title := fmt.Sprintf("filler issue %d", n)
|
||||
if marker {
|
||||
title = fmt.Sprintf("[auto-failure:cron:Timeout] %d", n)
|
||||
}
|
||||
return map[string]any{"number": n, "title": title, "body": ""}
|
||||
}
|
||||
|
||||
// TestPhaseBPipelineParity — a 3-page search through the pipelined path
|
||||
// yields exactly the oracle's result: markers kept in order, noise dropped.
|
||||
func TestPhaseBPipelineParity(t *testing.T) {
|
||||
pages := [][]any{
|
||||
fullPageWith(2),
|
||||
fullPageWith(4),
|
||||
{markerItem(5, true)},
|
||||
}
|
||||
sender := newPageSender(pages)
|
||||
a := NewGiteaForgejoAdapter("https://forge.example.com", "tok", "o", "r")
|
||||
a.Sender = sender
|
||||
got, err := a.SearchIssues("[auto-failure:cron:Timeout]")
|
||||
if err != nil {
|
||||
t.Fatalf("pipelined search failed: %v", err)
|
||||
}
|
||||
var nums []int
|
||||
for _, item := range got {
|
||||
m, _ := item.(map[string]any)
|
||||
nums = append(nums, numInt(m["number"]))
|
||||
}
|
||||
if len(nums) != 3 || nums[0] != 2 || nums[1] != 4 || nums[2] != 5 {
|
||||
t.Fatalf("pipelined filter diverged: %v", nums)
|
||||
}
|
||||
if len(*sender.requests) != 3 {
|
||||
t.Fatalf("expected 3 page requests, got %d", len(*sender.requests))
|
||||
}
|
||||
for i, req := range *sender.requests {
|
||||
url := fmt.Sprint(req.(map[string]any)["url"])
|
||||
if !strings.Contains(url, fmt.Sprintf("page=%d", i+1)) {
|
||||
t.Fatalf("page %d requested out of order: %s", i+1, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBNoSpeculativeRequests — a short first page must terminate the
|
||||
// producer with exactly one request; the goroutine must not prefetch a
|
||||
// page the serial oracle would never have asked for.
|
||||
func TestPhaseBNoSpeculativeRequests(t *testing.T) {
|
||||
sender := newPageSender([][]any{{markerItem(1, true)}})
|
||||
a := NewGiteaForgejoAdapter("https://forge.example.com", "tok", "o", "r")
|
||||
a.Sender = sender
|
||||
got, err := a.SearchIssues("[auto-failure:cron:Timeout]")
|
||||
if err != nil {
|
||||
t.Fatalf("search failed: %v", err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected the single marker hit, got %d items", len(got))
|
||||
}
|
||||
if len(*sender.requests) != 1 {
|
||||
t.Fatalf("speculative fetch detected: %d requests for a short single page", len(*sender.requests))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBErrorPropagation — a transport error on page 2 surfaces with
|
||||
// the frozen httpx-kind vocabulary and discards partial results, exactly
|
||||
// like the serial path. The connect error is retryable NOISE per _request
|
||||
// (retries=2 → 3 attempts), so the script must carry all three before the
|
||||
// error finally propagates.
|
||||
func TestPhaseBErrorPropagation(t *testing.T) {
|
||||
sender := &scriptedSender{
|
||||
entries: []scriptEntry{
|
||||
{Status: 200, Payload: fullPage()},
|
||||
{Exc: "connect"},
|
||||
{Exc: "connect"},
|
||||
{Exc: "connect"},
|
||||
},
|
||||
requests: &[]any{},
|
||||
}
|
||||
a := NewGiteaForgejoAdapter("https://forge.example.com", "tok", "o", "r")
|
||||
a.Sender = sender
|
||||
_, err := a.SearchIssues("[auto-failure:cron:Timeout]")
|
||||
if err == nil {
|
||||
t.Fatal("page-2 connect error must propagate")
|
||||
}
|
||||
if errorKind(err) != "httpx.ConnectError" {
|
||||
t.Fatalf("frozen error vocabulary changed: %v", errorKind(err))
|
||||
}
|
||||
if len(*sender.requests) != 4 {
|
||||
t.Fatalf("expected 4 requests (page 1 + 3 page-2 attempts), got %d", len(*sender.requests))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBPipelineOverlapsFilter — the pipeline is not decorative: while
|
||||
// the consumer filters page N, the producer is already fetching page N+1.
|
||||
// Proven by a sender that blocks in-flight until the consumer has begun
|
||||
// filtering the previous page (unbuffered channel rendezvous).
|
||||
func TestPhaseBPipelineOverlapsFilter(t *testing.T) {
|
||||
pageFetchStarted := make(chan int, 4)
|
||||
sender := &countingSender{scriptedSender: scriptedSender{
|
||||
entries: []scriptEntry{
|
||||
{Status: 200, Payload: fullPage()},
|
||||
{Status: 200, Payload: []any{}},
|
||||
},
|
||||
requests: &[]any{},
|
||||
}, started: pageFetchStarted}
|
||||
a := NewGiteaForgejoAdapter("https://forge.example.com", "tok", "o", "r")
|
||||
a.Sender = sender
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
if _, err := a.SearchIssues("x"); err != nil {
|
||||
t.Errorf("search failed: %v", err)
|
||||
}
|
||||
}()
|
||||
// page 1 fetch starts immediately
|
||||
<-pageFetchStarted
|
||||
// page 2 fetch must start while page 1 is still being consumed —
|
||||
// wait for it with a generous timeout instead of deadlocking.
|
||||
select {
|
||||
case <-pageFetchStarted:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("producer did not overlap: page 2 not fetched while page 1 was being consumed")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func fullPage() []any {
|
||||
p := make([]any, SearchPageSize)
|
||||
for i := range p {
|
||||
p[i] = map[string]any{"number": i, "title": "x", "body": ""}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// fullPageWith — a full 50-item page whose item #markerAt carries the
|
||||
// search marker (forcing pagination exactly like the live forge).
|
||||
func fullPageWith(markerAt int) []any {
|
||||
p := fullPage()
|
||||
p[markerAt] = markerItem(markerAt, true)
|
||||
return p
|
||||
}
|
||||
|
||||
// countingSender counts started requests so the test can observe the
|
||||
// producer/consumer overlap.
|
||||
type countingSender struct {
|
||||
scriptedSender
|
||||
started chan int
|
||||
n int
|
||||
}
|
||||
|
||||
func (s *countingSender) record(method, url string, headers map[string]string, jsonBody any, timeout time.Duration) *scriptEntry {
|
||||
s.n++
|
||||
select {
|
||||
case s.started <- s.n:
|
||||
default:
|
||||
}
|
||||
return s.scriptedSender.record(method, url, headers, jsonBody, timeout)
|
||||
}
|
||||
|
||||
func (s *countingSender) Post(url string, headers map[string]string, jsonBody any, timeout time.Duration) (*Response, error) {
|
||||
return s.respond(s.record("POST", url, headers, jsonBody, timeout))
|
||||
}
|
||||
|
||||
func (s *countingSender) Get(url string, headers map[string]string, timeout time.Duration) (*Response, error) {
|
||||
return s.respond(s.record("GET", url, headers, nil, timeout))
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// scripts/gen_forge_client_truth.py against the live Python oracle, httpx
|
||||
// transport scripted, sleep recorded) and re-executes every row through
|
||||
// this Go port, comparing canonically (sorted-key JSON).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
Oracle string `json:"oracle"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Input json.RawMessage `json:"input"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on 100%).
|
||||
func RunTruth(path string) []string {
|
||||
doc, err := loadTruth(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("load truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for i := range doc.Rows {
|
||||
if msg := compareOne(doc, &doc.Rows[i]); msg != "" {
|
||||
fails = append(fails, doc.Rows[i].Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
func loadTruth(path string) (*TruthDoc, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
return nil, fmt.Errorf("truth.json contained no rows")
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// compareOne replays one row and returns "" on parity, else a divergence
|
||||
// description. Shared by RunTruth and the per-function battery tests.
|
||||
func compareOne(doc *TruthDoc, row *TruthRow) string {
|
||||
got, err := execRow(row)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: %v", err)
|
||||
}
|
||||
wantStr, err := canonicalFromRaw(row.Want)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize want: %v", err)
|
||||
}
|
||||
gotStr, err := canonicalFromRaw([]byte(mustJSON(got)))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize got: %v", err)
|
||||
}
|
||||
if wantStr != gotStr {
|
||||
return fmt.Sprintf("\n want: %s\n got: %s", wantStr, gotStr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── scripted transport (mirrors the generator's fake httpx) ────────────────
|
||||
|
||||
type scriptEntry struct {
|
||||
Status int
|
||||
Payload any
|
||||
Exc string
|
||||
}
|
||||
|
||||
type scriptedSender struct {
|
||||
entries []scriptEntry
|
||||
requests *[]any
|
||||
}
|
||||
|
||||
func excKind(exc string) string {
|
||||
switch exc {
|
||||
case "connect":
|
||||
return "httpx.ConnectError"
|
||||
case "connect_timeout":
|
||||
return "httpx.ConnectTimeout"
|
||||
case "read_timeout":
|
||||
return "httpx.ReadTimeout"
|
||||
default:
|
||||
return "httpx.TransportError"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scriptedSender) record(method, url string, headers map[string]string, jsonBody any, timeout time.Duration) *scriptEntry {
|
||||
req := map[string]any{
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": headers,
|
||||
"json": jsonBody,
|
||||
"timeout": timeout.Seconds(),
|
||||
}
|
||||
*s.requests = append(*s.requests, req)
|
||||
if len(s.entries) == 0 {
|
||||
panic(fmt.Sprintf("script exhausted at request %d (%s %s)", len(*s.requests), method, url))
|
||||
}
|
||||
entry := s.entries[0]
|
||||
s.entries = s.entries[1:]
|
||||
return &entry
|
||||
}
|
||||
|
||||
func (s *scriptedSender) respond(entry *scriptEntry) (*Response, error) {
|
||||
if entry.Exc != "" {
|
||||
return nil, &ForgeTransportError{Kind: excKind(entry.Exc), Msg: "scripted"}
|
||||
}
|
||||
status := entry.Status
|
||||
if status == 0 {
|
||||
status = 200
|
||||
}
|
||||
body, err := json.Marshal(entry.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry.Payload == nil {
|
||||
body = []byte("{}")
|
||||
}
|
||||
return &Response{StatusCode: status, Body: body}, nil
|
||||
}
|
||||
|
||||
func (s *scriptedSender) Post(url string, headers map[string]string, jsonBody any, timeout time.Duration) (*Response, error) {
|
||||
return s.respond(s.record("POST", url, headers, jsonBody, timeout))
|
||||
}
|
||||
|
||||
func (s *scriptedSender) Get(url string, headers map[string]string, timeout time.Duration) (*Response, error) {
|
||||
return s.respond(s.record("GET", url, headers, nil, timeout))
|
||||
}
|
||||
|
||||
// ── row execution ───────────────────────────────────────────────────────────
|
||||
|
||||
func decodeInput(raw json.RawMessage) (map[string]any, error) {
|
||||
if len(raw) == 0 {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
var v any
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("input not an object")
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func numInt(v any) int {
|
||||
switch t := v.(type) {
|
||||
case json.Number:
|
||||
n, err := t.Int64()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("not an integer: %v", t))
|
||||
}
|
||||
return int(n)
|
||||
case float64:
|
||||
return int(t)
|
||||
default:
|
||||
panic(fmt.Sprintf("not a number: %v", v))
|
||||
}
|
||||
}
|
||||
|
||||
func numFloat(v any) float64 {
|
||||
switch t := v.(type) {
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
case float64:
|
||||
return t
|
||||
default:
|
||||
panic(fmt.Sprintf("not a number: %v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// wantRecorder builds the {result, requests, sleeps, error} got-shape the
|
||||
// frozen rows carry.
|
||||
type recorder struct {
|
||||
requests []any
|
||||
sleeps []any
|
||||
errKind string
|
||||
}
|
||||
|
||||
func (r *recorder) sleep(d time.Duration) { r.sleeps = append(r.sleeps, d.Seconds()) }
|
||||
|
||||
func (r *recorder) got(result any) map[string]any {
|
||||
return map[string]any{
|
||||
"result": result,
|
||||
"requests": r.requests,
|
||||
"sleeps": r.sleeps,
|
||||
"error": errKindOrNil(r.errKind),
|
||||
}
|
||||
}
|
||||
|
||||
func errKindOrNil(kind string) any {
|
||||
if kind == "" {
|
||||
return nil
|
||||
}
|
||||
return kind
|
||||
}
|
||||
|
||||
func errorKind(err error) string {
|
||||
var terr *ForgeTransportError
|
||||
if errors.As(err, &terr) {
|
||||
return terr.Kind
|
||||
}
|
||||
var serr *HTTPStatusError
|
||||
if errors.As(err, &serr) {
|
||||
return "httpx.HTTPStatusError"
|
||||
}
|
||||
return fmt.Sprintf("%T", err)
|
||||
}
|
||||
|
||||
func execRow(row *TruthRow) (any, error) {
|
||||
in, err := decodeInput(row.Input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode input: %w", err)
|
||||
}
|
||||
|
||||
switch row.Op {
|
||||
case "select":
|
||||
return execSelect(in)
|
||||
|
||||
case "null":
|
||||
return execNull(in)
|
||||
|
||||
case "call":
|
||||
return execCall(in)
|
||||
|
||||
case "request":
|
||||
return execRequest(in)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown row op %s (%s)", row.Op, row.Name)
|
||||
}
|
||||
|
||||
func execSelect(in map[string]any) (any, error) {
|
||||
var cfg map[string]any
|
||||
if v, ok := in["config"]; ok && v != nil {
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("select: config not an object")
|
||||
}
|
||||
cfg = m
|
||||
}
|
||||
env := map[string]string{}
|
||||
if v, ok := in["env"].(map[string]any); ok {
|
||||
for k, val := range v {
|
||||
s, _ := val.(string)
|
||||
env[k] = s
|
||||
}
|
||||
}
|
||||
saved := LookupEnv
|
||||
defer func() { LookupEnv = saved }()
|
||||
LookupEnv = func(name string) (string, bool) {
|
||||
v, ok := env[name]
|
||||
return v, ok
|
||||
}
|
||||
client := CreateForgeClient(cfg)
|
||||
if _, isNull := client.(NullAdapter); isNull {
|
||||
return map[string]any{"adapter": "NullAdapter", "base_url": nil, "token": nil}, nil
|
||||
}
|
||||
a := client.(*GiteaForgejoAdapter)
|
||||
return map[string]any{"adapter": "GiteaForgejoAdapter", "base_url": a.BaseURL, "token": a.Token}, nil
|
||||
}
|
||||
|
||||
func execNull(in map[string]any) (any, error) {
|
||||
method, _ := in["method"].(string)
|
||||
args, _ := in["args"].([]any)
|
||||
client := NullAdapter{}
|
||||
switch method {
|
||||
case "create_issue":
|
||||
return client.CreateIssue(argStr(args, 0), argStr(args, 1))
|
||||
case "add_comment":
|
||||
return client.AddComment(numInt(args[0]), argStr(args, 1))
|
||||
case "get_issue_state":
|
||||
return client.GetIssueState(numInt(args[0]))
|
||||
case "list_open_issues":
|
||||
return client.ListOpenIssues()
|
||||
case "search_issues":
|
||||
return client.SearchIssues(argStr(args, 0))
|
||||
}
|
||||
return nil, fmt.Errorf("null: unknown method %s", method)
|
||||
}
|
||||
|
||||
func argStr(args []any, i int) string {
|
||||
if i < len(args) {
|
||||
s, _ := args[i].(string)
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newAdapterFrom(in map[string]any, rec *recorder) (*GiteaForgejoAdapter, *scriptedSender, error) {
|
||||
method, _ := in["method"].(string)
|
||||
url, _ := in["url"].(string)
|
||||
token, _ := in["token"].(string)
|
||||
owner, _ := in["owner"].(string)
|
||||
repo, _ := in["repo"].(string)
|
||||
entries, err := decodeScript(in["script"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sender := &scriptedSender{entries: entries, requests: &rec.requests}
|
||||
a := NewGiteaForgejoAdapter(url, token, owner, repo)
|
||||
a.Sender = sender
|
||||
a.Sleep = rec.sleep
|
||||
_ = method
|
||||
return a, sender, nil
|
||||
}
|
||||
|
||||
func decodeScript(v any) ([]scriptEntry, error) {
|
||||
list, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("script not a list")
|
||||
}
|
||||
out := make([]scriptEntry, 0, len(list))
|
||||
for _, item := range list {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("script entry not an object")
|
||||
}
|
||||
e := scriptEntry{}
|
||||
if exc, ok := m["exc"].(string); ok {
|
||||
e.Exc = exc
|
||||
}
|
||||
if status, ok := m["status"]; ok {
|
||||
e.Status = numInt(status)
|
||||
}
|
||||
if payload, ok := m["payload"]; ok {
|
||||
e.Payload = payload
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func execCall(in map[string]any) (any, error) {
|
||||
method, _ := in["method"].(string)
|
||||
rec := &recorder{}
|
||||
a, _, err := newAdapterFrom(in, rec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kwargs, _ := in["kwargs"].(map[string]any)
|
||||
var result any
|
||||
switch method {
|
||||
case "create_issue":
|
||||
result, err = a.CreateIssue(kwStr(kwargs, "title"), kwStr(kwargs, "body"))
|
||||
case "add_comment":
|
||||
result, err = a.AddComment(numInt(kwargs["issue_number"]), kwStr(kwargs, "body"))
|
||||
case "get_issue_state":
|
||||
result, err = a.GetIssueState(numInt(kwargs["issue_number"]))
|
||||
case "list_open_issues":
|
||||
result, err = a.ListOpenIssues()
|
||||
case "search_issues":
|
||||
result, err = a.SearchIssues(kwStr(kwargs, "query"))
|
||||
default:
|
||||
return nil, fmt.Errorf("call: unknown method %s", method)
|
||||
}
|
||||
if err != nil {
|
||||
rec.errKind = errorKind(err)
|
||||
result = nil
|
||||
}
|
||||
return rec.got(result), nil
|
||||
}
|
||||
|
||||
func kwStr(m map[string]any, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func execRequest(in map[string]any) (any, error) {
|
||||
rec := &recorder{}
|
||||
method, _ := in["method"].(string)
|
||||
rawURL, _ := in["url"].(string)
|
||||
entries, err := decodeScript(in["script"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sender := &scriptedSender{entries: entries, requests: &rec.requests}
|
||||
opts := RequestOptions{
|
||||
Headers: toStringMap(in["headers"]),
|
||||
JSON: in["json"],
|
||||
Timeout: secondsDuration(in["timeout"]),
|
||||
Retries: numInt(in["retries"]),
|
||||
Backoff: secondsDuration(in["backoff"]),
|
||||
Sleep: rec.sleep,
|
||||
}
|
||||
resp, err := Request(sender, method, rawURL, opts)
|
||||
var result any
|
||||
if err != nil {
|
||||
rec.errKind = errorKind(err)
|
||||
} else {
|
||||
result, err = resp.JSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return rec.got(result), nil
|
||||
}
|
||||
|
||||
func toStringMap(v any) map[string]string {
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(m))
|
||||
for k, val := range m {
|
||||
s, _ := val.(string)
|
||||
out[k] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func secondsDuration(v any) time.Duration {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(numFloat(v) * float64(time.Second))
|
||||
}
|
||||
|
||||
// ── canonical comparison (sorted-key JSON) ──────────────────────────────────
|
||||
|
||||
func canonicalFromRaw(raw []byte) (string, error) {
|
||||
var v any
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonical(normalize(v)), nil
|
||||
}
|
||||
|
||||
// canonical — sorted-key deterministic rendering of a decoded JSON value.
|
||||
func canonical(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case string:
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return canonical(f)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = canonical(normalize(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
bk, _ := json.Marshal(k)
|
||||
parts = append(parts, string(bk)+": "+canonical(normalize(t[k])))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return fmt.Sprintf("«%v»", t)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize — flatten trees into plain map/float64 representations so
|
||||
// canonical() sees one shape.
|
||||
func normalize(v any) any {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
m := map[string]any{}
|
||||
for k, val := range t {
|
||||
m[k] = normalize(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = normalize(item)
|
||||
}
|
||||
return out
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(normalize(v))
|
||||
if err != nil {
|
||||
return "null"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,285 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate plus per-function batteries replaying the
|
||||
// frozen oracle truth (truth.json, generated by
|
||||
// scripts/gen_forge_client_truth.py against the LIVE Python oracle with a
|
||||
// scripted httpx transport and a recording sleep) VERBATIM. Any divergence
|
||||
// fails; the truth file rides the repo so the gate holds in CI without a
|
||||
// live forge.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func loadTruthOrDie(t *testing.T) *TruthDoc {
|
||||
t.Helper()
|
||||
doc, err := loadTruth("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json unusable (frozen oracle battery): %v", err)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func findRow(t *testing.T, name string) TruthRow {
|
||||
t.Helper()
|
||||
doc := loadTruthOrDie(t)
|
||||
for _, row := range doc.Rows {
|
||||
if row.Name == name {
|
||||
return row
|
||||
}
|
||||
}
|
||||
t.Fatalf("frozen row %q missing from truth.json — battery/truth drift", name)
|
||||
return TruthRow{}
|
||||
}
|
||||
|
||||
func mustReplay(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
doc := loadTruthOrDie(t)
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func replayOp(t *testing.T, op string) int {
|
||||
t.Helper()
|
||||
doc := loadTruthOrDie(t)
|
||||
n := 0
|
||||
for i := range doc.Rows {
|
||||
row := &doc.Rows[i]
|
||||
if row.Op != op {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if msg := compareOne(doc, row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("no %s rows in truth.json", op)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// rowWant decodes a row's want JSON into a generic map.
|
||||
func rowWant(t *testing.T, name string) map[string]any {
|
||||
t.Helper()
|
||||
row := findRow(t, name)
|
||||
var want map[string]any
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
t.Fatalf("decode want of %s: %v", name, err)
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// TestTruthParity — the whole frozen battery, one shot.
|
||||
func TestTruthParity(t *testing.T) {
|
||||
doc := loadTruthOrDie(t)
|
||||
var fails []string
|
||||
for i := range doc.Rows {
|
||||
if msg := compareOne(doc, &doc.Rows[i]); msg != "" {
|
||||
fails = append(fails, doc.Rows[i].Name+": "+msg)
|
||||
}
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
t.Fatalf("parity replay diverged after %d clean rows:\n%s",
|
||||
len(doc.Rows)-len(fails), strings.Join(fails, "\n"))
|
||||
}
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", len(doc.Rows))
|
||||
}
|
||||
|
||||
// TestBatteryCreateForgeClient — every selection branch: none, env, config
|
||||
// keys, config-beats-env, owner/repo defaults and env overrides, trailing
|
||||
// slash strip, empty-string falsy fallthrough, missing halves.
|
||||
func TestBatteryCreateForgeClient(t *testing.T) {
|
||||
if n := replayOp(t, "select"); n != 11 {
|
||||
t.Fatalf("expected 11 frozen select rows, got %d — battery/truth drift", n)
|
||||
}
|
||||
want := rowWant(t, "select:none")
|
||||
if want["adapter"] != "NullAdapter" {
|
||||
t.Fatalf("unconfigured must be NullAdapter, got %v", want["adapter"])
|
||||
}
|
||||
want = rowWant(t, "select:env-configured")
|
||||
if want["base_url"] != "https://forge.example.com/api/v1/repos/owner/repo" {
|
||||
t.Fatalf("frozen env base_url changed: %v", want["base_url"])
|
||||
}
|
||||
want = rowWant(t, "select:config-beats-env")
|
||||
if want["base_url"] != "https://cfg.example.com/api/v1/repos/owner/repo" ||
|
||||
want["token"] != "cfg_tok" {
|
||||
t.Fatalf("config-beats-env changed: %v", want)
|
||||
}
|
||||
want = rowWant(t, "select:trailing-slash-strip")
|
||||
if want["base_url"] != "https://forge.example.com/api/v1/repos/owner/repo" {
|
||||
t.Fatalf("trailing slash strip changed: %v", want["base_url"])
|
||||
}
|
||||
want = rowWant(t, "select:cfg-url-missing-token")
|
||||
if want["adapter"] != "NullAdapter" {
|
||||
t.Fatalf("url without token must stay dormant: %v", want["adapter"])
|
||||
}
|
||||
mustReplay(t, "select:owner-empty-env")
|
||||
}
|
||||
|
||||
// TestBatteryNullAdapter — every no-op shape, zero errors.
|
||||
func TestBatteryNullAdapter(t *testing.T) {
|
||||
if n := replayOp(t, "null"); n != 5 {
|
||||
t.Fatalf("expected 5 frozen null rows, got %d — battery/truth drift", n)
|
||||
}
|
||||
want := rowWant(t, "null:create-issue")
|
||||
if want["created"] != false || want["reason"] != "no forge configured" {
|
||||
t.Fatalf("frozen null create changed: %v", want)
|
||||
}
|
||||
want = rowWant(t, "null:get-issue-state")
|
||||
if want["state"] != "unknown" {
|
||||
t.Fatalf("frozen null state changed: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryGiteaAdapter — every adapter method incl. pagination, the
|
||||
// client-side marker filter, and retry propagation through _request.
|
||||
func TestBatteryGiteaAdapter(t *testing.T) {
|
||||
if n := replayOp(t, "call"); n != 14 {
|
||||
t.Fatalf("expected 14 frozen call rows, got %d — battery/truth drift", n)
|
||||
}
|
||||
// create_issue posts to /issues with token auth and the JSON body.
|
||||
want := rowWant(t, "call:create-issue")
|
||||
reqs, _ := want["requests"].([]any)
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("create-issue: expected 1 request, got %d", len(reqs))
|
||||
}
|
||||
req, _ := reqs[0].(map[string]any)
|
||||
if !strings.HasSuffix(fmt.Sprint(req["url"]), "/api/v1/repos/o/r/issues") {
|
||||
t.Fatalf("create-issue URL changed: %v", req["url"])
|
||||
}
|
||||
headers, _ := req["headers"].(map[string]any)
|
||||
if headers["Authorization"] != "token tok" || headers["Content-Type"] != "application/json" {
|
||||
t.Fatalf("frozen auth headers changed: %v", headers)
|
||||
}
|
||||
if fmt.Sprint(req["timeout"]) != "30" {
|
||||
t.Fatalf("frozen timeout passthrough changed: %v", req["timeout"])
|
||||
}
|
||||
// search hits the repo-scoped LIST endpoint, never /issues/search.
|
||||
want = rowWant(t, "call:search-single-page")
|
||||
reqs, _ = want["requests"].([]any)
|
||||
req, _ = reqs[0].(map[string]any)
|
||||
searchURL := fmt.Sprint(req["url"])
|
||||
if strings.Contains(searchURL, "/issues/search") {
|
||||
t.Fatalf("regression: search used the 404ing /issues/search path")
|
||||
}
|
||||
if !strings.Contains(searchURL, "state=open") ||
|
||||
!strings.Contains(searchURL, "type=issues") ||
|
||||
!strings.Contains(searchURL, "q=%5Bauto-failure%3Acron%3AConnection%5D") {
|
||||
t.Fatalf("frozen search query changed: %s", searchURL)
|
||||
}
|
||||
// pagination: a full 50-item page must be followed by page=2.
|
||||
want = rowWant(t, "call:search-paginates")
|
||||
reqs, _ = want["requests"].([]any)
|
||||
if len(reqs) != 2 || !strings.Contains(fmt.Sprint(reqs[1].(map[string]any)["url"]), "page=2") {
|
||||
t.Fatalf("frozen pagination changed: %d requests", len(reqs))
|
||||
}
|
||||
// retry through _request: connect-phase POST retried once, sleep 0.5s.
|
||||
want = rowWant(t, "call:create-issue-retry-connect")
|
||||
sleeps, _ := want["sleeps"].([]any)
|
||||
if len(sleeps) != 1 || fmt.Sprint(sleeps[0]) != "0.5" {
|
||||
t.Fatalf("frozen connect retry sleeps changed: %v", sleeps)
|
||||
}
|
||||
// 4xx is the information: one shot, no retry.
|
||||
want = rowWant(t, "call:get-issue-state-4xx")
|
||||
if want["error"] != "httpx.HTTPStatusError" {
|
||||
t.Fatalf("frozen 4xx error kind changed: %v", want["error"])
|
||||
}
|
||||
reqs, _ = want["requests"].([]any)
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("4xx must not retry: %d requests", len(reqs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryRequest — every _request branch: noise retried, signal never,
|
||||
// exponential backoff never before the first attempt, timeout passthrough,
|
||||
// lowercase method routing.
|
||||
func TestBatteryRequest(t *testing.T) {
|
||||
if n := replayOp(t, "request"); n != 19 {
|
||||
t.Fatalf("expected 19 frozen request rows, got %d — battery/truth drift", n)
|
||||
}
|
||||
// backoff is exponential: [0.5, 1.0], never before the first attempt.
|
||||
want := rowWant(t, "request:backoff-exponential")
|
||||
sleeps, _ := want["sleeps"].([]any)
|
||||
if len(sleeps) != 2 || fmt.Sprint(sleeps[0]) != "0.5" || fmt.Sprint(sleeps[1]) != "1" {
|
||||
t.Fatalf("frozen backoff schedule changed: %v", sleeps)
|
||||
}
|
||||
// connect exhaustion: retries+1 attempts, error propagates.
|
||||
want = rowWant(t, "request:get-connect-exhaust")
|
||||
if want["error"] != "httpx.ConnectError" {
|
||||
t.Fatalf("frozen exhaust error changed: %v", want["error"])
|
||||
}
|
||||
reqs, _ := want["requests"].([]any)
|
||||
if len(reqs) != 3 {
|
||||
t.Fatalf("frozen attempt count changed: %d", len(reqs))
|
||||
}
|
||||
// POST read timeout: the server MAY have processed it — one shot.
|
||||
want = rowWant(t, "request:post-read-timeout-no-retry")
|
||||
if want["error"] != "httpx.ReadTimeout" {
|
||||
t.Fatalf("frozen read-timeout kind changed: %v", want["error"])
|
||||
}
|
||||
reqs, _ = want["requests"].([]any)
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("POST read timeout must not retry: %d requests", len(reqs))
|
||||
}
|
||||
// POST 5xx after send: ambiguous — one shot.
|
||||
want = rowWant(t, "request:post-5xx-no-retry")
|
||||
if want["error"] != "httpx.HTTPStatusError" {
|
||||
t.Fatalf("frozen post-5xx kind changed: %v", want["error"])
|
||||
}
|
||||
// timeout passes through per call.
|
||||
want = rowWant(t, "request:timeout-passthrough")
|
||||
reqs, _ = want["requests"].([]any)
|
||||
if fmt.Sprint(reqs[0].(map[string]any)["timeout"]) != "17" {
|
||||
t.Fatalf("frozen timeout passthrough changed: %v", reqs[0])
|
||||
}
|
||||
mustReplay(t, "request:lowercase-method")
|
||||
mustReplay(t, "request:headers-json-capture")
|
||||
mustReplay(t, "request:get-5xx-exhaust")
|
||||
mustReplay(t, "request:get-read-timeout-retries")
|
||||
}
|
||||
|
||||
// TestBatteryRetryAsymmetry — direct cross-check (not via a truth row) of
|
||||
// the behavior that motivated the module: GET 5xx is noise (retried to
|
||||
// success), POST 5xx is signal (one shot, no double-create).
|
||||
func TestBatteryRetryAsymmetry(t *testing.T) {
|
||||
getRequests := &[]any{}
|
||||
getSender := &scriptedSender{
|
||||
entries: []scriptEntry{{Status: 502}, {Status: 200, Payload: map[string]any{"ok": 1}}},
|
||||
requests: getRequests,
|
||||
}
|
||||
opts := DefaultRequestOptions()
|
||||
opts.Sleep = func(time.Duration) {}
|
||||
if _, err := Request(getSender, "GET", "https://x/api", opts); err != nil {
|
||||
t.Fatalf("GET 5xx must be retried to success: %v", err)
|
||||
}
|
||||
if len(*getRequests) != 2 {
|
||||
t.Fatalf("GET 5xx made %d requests — retry lost", len(*getRequests))
|
||||
}
|
||||
|
||||
postRequests := &[]any{}
|
||||
postSender := &scriptedSender{
|
||||
entries: []scriptEntry{{Status: 502}, {Status: 200}},
|
||||
requests: postRequests,
|
||||
}
|
||||
postOpts := DefaultRequestOptions()
|
||||
postOpts.JSON = map[string]any{}
|
||||
postOpts.Sleep = func(time.Duration) {}
|
||||
if _, err := Request(postSender, "POST", "https://x/api", postOpts); err == nil {
|
||||
t.Fatal("POST 5xx must never be retried")
|
||||
}
|
||||
if len(*postRequests) != 1 {
|
||||
t.Fatalf("POST 5xx made %d requests — double-create hazard", len(*postRequests))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/openai-compat-transport
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,28 @@
|
||||
// Package main — OpenAI-compat transport CLI: replay the frozen oracle
|
||||
// truth battery.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: openai-compat-transport replay <truth.json>")
|
||||
os.Exit(2)
|
||||
}
|
||||
path := "truth.json"
|
||||
if len(os.Args) > 2 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Println(f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases exact-equal vs the Python oracle\n", truthRowCount())
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
// Package main — OpenAI-compatible chat transport, ported 1:1 from
|
||||
// src/rune/openai_compat_transport.py (dichotomy wave #2479).
|
||||
//
|
||||
// The transport speaks the OpenAI wire format (/chat/completions) and returns
|
||||
// Ollama-SHAPED responses, so LocalToolRuntime's loop is unchanged. All IO
|
||||
// sits behind injectable seams (HTTPPoster for the wire; ExpandHome /
|
||||
// ReadFileText / LookupEnv for cloud_transport_for's config resolution).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValueError — the oracle raises Python's ValueError; error text is frozen
|
||||
// truth and must match str(e) exactly.
|
||||
type ValueError struct{ Msg string }
|
||||
|
||||
func (e *ValueError) Error() string { return e.Msg }
|
||||
|
||||
// HTTPPoster — the wire seam. Post sends body to url with the given
|
||||
// headers and per-request timeout, returning the raw response body.
|
||||
type HTTPPoster interface {
|
||||
Post(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error)
|
||||
}
|
||||
|
||||
// HTTPPosterFunc adapts a function to HTTPPoster.
|
||||
type HTTPPosterFunc func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error)
|
||||
|
||||
// Post implements HTTPPoster.
|
||||
func (f HTTPPosterFunc) Post(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
return f(url, headers, body, timeoutSecs)
|
||||
}
|
||||
|
||||
// defaultPoster — net/http faithful to the oracle's urllib round trip.
|
||||
type defaultPoster struct{}
|
||||
|
||||
func (defaultPoster) Post(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
client := &http.Client{Timeout: time.Duration(timeoutSecs * float64(time.Second))}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := new(strings.Builder)
|
||||
tmp := make([]byte, 32*1024)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(tmp)
|
||||
if n > 0 {
|
||||
buf.Write(tmp[:n])
|
||||
}
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return []byte(buf.String()), nil
|
||||
}
|
||||
|
||||
// OpenAICompatTransport — ChatTransport for OpenAI-compatible providers
|
||||
// (Verboo Code, Xioami...).
|
||||
type OpenAICompatTransport struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Provider string
|
||||
// Cloud round trips with a full tool schema routinely exceed the local
|
||||
// tier timeout (30s): observed live, glm-5.3-flash read-timeout at
|
||||
// 30.6s mid-iteration. The constructor clamps the WIRE timeout to
|
||||
// >=120s — the escalation ladder still governs retries/tiers.
|
||||
Timeout float64
|
||||
contextWindow int
|
||||
usageCalls int
|
||||
usagePromptTokens int
|
||||
usageOutputTokens int
|
||||
usageTotalDurationNS int64
|
||||
poster HTTPPoster
|
||||
// mu guards the usage accumulators once Phase B makes concurrent
|
||||
// chats legal (ChatAll).
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewOpenAICompatTransport — oracle defaults: timeout 120, context window 131072.
|
||||
func NewOpenAICompatTransport(baseURL, apiKey, provider string) *OpenAICompatTransport {
|
||||
return NewOpenAICompatTransportWith(baseURL, apiKey, provider, 120, 131072)
|
||||
}
|
||||
|
||||
// NewOpenAICompatTransportWith — full constructor contract.
|
||||
func NewOpenAICompatTransportWith(baseURL, apiKey, provider string, timeout float64, contextWindow int) *OpenAICompatTransport {
|
||||
if timeout < 120 {
|
||||
timeout = 120
|
||||
}
|
||||
return &OpenAICompatTransport{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
Provider: provider,
|
||||
Timeout: timeout,
|
||||
contextWindow: contextWindow,
|
||||
poster: defaultPoster{},
|
||||
}
|
||||
}
|
||||
|
||||
// SetPoster swaps the wire seam (battery/replay injection).
|
||||
func (t *OpenAICompatTransport) SetPoster(p HTTPPoster) { t.poster = p }
|
||||
|
||||
// wirePath posts to base_url + path with the OpenAI auth headers, returning
|
||||
// the parsed body (oracle _post).
|
||||
func (t *OpenAICompatTransport) wirePost(path string, payload PyValue) (PyValue, error) {
|
||||
url := t.BaseURL + path
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("Bearer %s", t.APIKey),
|
||||
}
|
||||
body := []byte(PyDumpsCompact(payload))
|
||||
raw, err := t.poster.Post(url, headers, body, t.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, err := PyParse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, isObj := decoded.(*PyObj); !isObj {
|
||||
return nil, &ValueError{Msg: "provider returned a non-object response"}
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// toOpenaiMessages — Ollama-shape → OpenAI-shape. Pairing: the runtime's
|
||||
// _tool_message sets tool_call_id from request.call_id (minted by
|
||||
// normalize_ollama_tool_calls from the response's own id). The assistant
|
||||
// message must use THE SAME ids for its tool_calls — a mismatch makes the
|
||||
// provider treat the tool result as orphaned and the model re-issues the
|
||||
// identical call forever (observed live: glm-5.3-flash looped the same
|
||||
// edit_file twice, empty terminal text both turns).
|
||||
func toOpenaiMessages(messages []PyValue) []PyValue {
|
||||
out := []PyValue{}
|
||||
for i := 0; i < len(messages); i++ {
|
||||
m, ok := messages[i].(*PyObj)
|
||||
if !ok {
|
||||
m = NewPyObj()
|
||||
}
|
||||
role, _ := m.Get("role")
|
||||
roleStr, _ := role.(string)
|
||||
tcRaw, _ := m.Get("tool_calls")
|
||||
var toolCalls []PyValue
|
||||
if list, isList := tcRaw.([]PyValue); isList {
|
||||
toolCalls = list
|
||||
}
|
||||
if roleStr == "assistant" && len(toolCalls) > 0 {
|
||||
// Look ahead: pair each tool_call with the tool messages that
|
||||
// immediately follow, reusing their tool_call_ids.
|
||||
var pairedIDs []PyValue
|
||||
for j := i + 1; j < len(messages); j++ {
|
||||
if len(pairedIDs) >= len(toolCalls) {
|
||||
break
|
||||
}
|
||||
tm, ok := messages[j].(*PyObj)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tmRole, _ := tm.Get("role")
|
||||
if s, isStr := tmRole.(string); !isStr || s != "tool" {
|
||||
continue
|
||||
}
|
||||
id, _ := tm.Get("tool_call_id")
|
||||
pairedIDs = append(pairedIDs, id)
|
||||
}
|
||||
tc := []PyValue{}
|
||||
for k, c := range toolCalls {
|
||||
co, ok := c.(*PyObj)
|
||||
if !ok {
|
||||
co = NewPyObj()
|
||||
}
|
||||
fnRaw, _ := co.Get("function")
|
||||
fn, _ := fnRaw.(*PyObj)
|
||||
if fn == nil {
|
||||
fn = NewPyObj()
|
||||
}
|
||||
argsRaw, _ := fn.Get("arguments")
|
||||
if argsRaw == nil {
|
||||
if _, present := fn.Get("arguments"); !present {
|
||||
argsRaw = NewPyObj() // fn.get("arguments", {}) default
|
||||
}
|
||||
}
|
||||
var argsStr string
|
||||
if _, isObj := argsRaw.(*PyObj); isObj {
|
||||
argsStr = PyDumpsCompact(argsRaw)
|
||||
} else {
|
||||
argsStr = PyStr(argsRaw)
|
||||
}
|
||||
name, _ := fn.Get("name")
|
||||
var callID PyValue
|
||||
if k < len(pairedIDs) {
|
||||
callID = pairedIDs[k]
|
||||
}
|
||||
if !PyTruthy(callID) {
|
||||
callID = fmt.Sprintf("call_%d_%d", i, k)
|
||||
}
|
||||
entry := NewPyObj()
|
||||
entry.Set("id", callID)
|
||||
entry.Set("type", "function")
|
||||
fnObj := NewPyObj()
|
||||
fnObj.Set("name", name)
|
||||
fnObj.Set("arguments", argsStr)
|
||||
entry.Set("function", fnObj)
|
||||
tc = append(tc, entry)
|
||||
}
|
||||
am := NewPyObj()
|
||||
am.Set("role", "assistant")
|
||||
// m.get("content") or None — key may be missing (nil) or falsy.
|
||||
content, _ := m.Get("content")
|
||||
if !PyTruthy(content) {
|
||||
content = nil
|
||||
}
|
||||
am.Set("content", content)
|
||||
am.Set("tool_calls", tc)
|
||||
out = append(out, am)
|
||||
continue
|
||||
}
|
||||
if roleStr == "tool" {
|
||||
tm := NewPyObj()
|
||||
tm.Set("role", "tool")
|
||||
// m.get("tool_call_id") or f"call_{i}" — missing AND null
|
||||
// both fall back (Get returns nil for both).
|
||||
id, _ := m.Get("tool_call_id")
|
||||
if !PyTruthy(id) {
|
||||
id = fmt.Sprintf("call_%d", i)
|
||||
}
|
||||
tm.Set("tool_call_id", id)
|
||||
// m.get("content", "") — missing key defaults; explicit null
|
||||
// survives (both arrive as nil from Get, but the oracle's
|
||||
// frozen cases with null vs missing both render null...).
|
||||
// Corrected below: distinguish via key presence.
|
||||
content := toolContent(m)
|
||||
tm.Set("content", content)
|
||||
out = append(out, tm)
|
||||
continue
|
||||
}
|
||||
pm := NewPyObj()
|
||||
pm.Set("role", role)
|
||||
content := plainContent(m)
|
||||
pm.Set("content", content)
|
||||
out = append(out, pm)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// toolContent — oracle m.get("content", "") for tool messages.
|
||||
func toolContent(m *PyObj) PyValue {
|
||||
if _, present := m.Get("content"); !present {
|
||||
return ""
|
||||
}
|
||||
v, _ := m.Get("content")
|
||||
return v
|
||||
}
|
||||
|
||||
// plainContent — oracle m.get("content", "") for plain roles.
|
||||
func plainContent(m *PyObj) PyValue {
|
||||
if _, present := m.Get("content"); !present {
|
||||
return ""
|
||||
}
|
||||
v, _ := m.Get("content")
|
||||
return v
|
||||
}
|
||||
|
||||
// _from_openai_response — name-preserving alias for the audit's
|
||||
// snake_case surface (the oracle's static method name verbatim).
|
||||
func _from_openai_response(resp PyValue) PyValue { return fromOpenaiResponse(resp) }
|
||||
|
||||
// _to_openai_messages — name-preserving alias for the audit's snake_case
|
||||
// surface (the oracle's static method name verbatim).
|
||||
func _to_openai_messages(messages []PyValue) []PyValue { return toOpenaiMessages(messages) }
|
||||
|
||||
// _post — name-preserving alias for the audit's snake_case surface
|
||||
// (the oracle's wire method name verbatim).
|
||||
func _post(t *OpenAICompatTransport, path string, payload PyValue) (PyValue, error) {
|
||||
return t.wirePost(path, payload)
|
||||
}
|
||||
|
||||
// fromOpenaiResponse — OpenAI response → flat Ollama ChatMessage
|
||||
// (LocalToolRuntime's contract).
|
||||
//
|
||||
// CONTRACT: chat() returns the MESSAGE object — role/content/tool_calls at
|
||||
// top level — exactly like OllamaChatTransport.chat. Observed live (PL1
|
||||
// Task A, 2026-09-12): the envelope shape {'message': {...}} made the
|
||||
// runtime see 'empty terminal visible text' while the model had returned a
|
||||
// valid read_file tool call.
|
||||
func fromOpenaiResponse(resp PyValue) PyValue {
|
||||
ro, ok := resp.(*PyObj)
|
||||
if !ok {
|
||||
ro = NewPyObj()
|
||||
}
|
||||
choicesRaw, _ := ro.Get("choices")
|
||||
choices, isList := choicesRaw.([]PyValue)
|
||||
if !isList || len(choices) == 0 {
|
||||
choices = []PyValue{NewPyObj()}
|
||||
}
|
||||
first, _ := choices[0].(*PyObj)
|
||||
if first == nil {
|
||||
first = NewPyObj()
|
||||
}
|
||||
msgRaw, _ := first.Get("message")
|
||||
message, _ := msgRaw.(*PyObj)
|
||||
if message == nil {
|
||||
message = NewPyObj()
|
||||
}
|
||||
tcRaw, _ := message.Get("tool_calls")
|
||||
tcList, _ := tcRaw.([]PyValue)
|
||||
toolCalls := []PyValue{}
|
||||
for _, c := range tcList {
|
||||
co, ok := c.(*PyObj)
|
||||
if !ok {
|
||||
co = NewPyObj()
|
||||
}
|
||||
fnRaw, _ := co.Get("function")
|
||||
fn, _ := fnRaw.(*PyObj)
|
||||
if fn == nil {
|
||||
fn = NewPyObj()
|
||||
}
|
||||
argsRaw, _ := fn.Get("arguments")
|
||||
if s, isStr := argsRaw.(string); isStr {
|
||||
parsed, err := PyParse([]byte(s))
|
||||
if err != nil {
|
||||
raw := NewPyObj()
|
||||
raw.Set("_raw", s)
|
||||
argsRaw = raw
|
||||
} else {
|
||||
argsRaw = parsed
|
||||
}
|
||||
}
|
||||
if !PyTruthy(argsRaw) {
|
||||
argsRaw = NewPyObj()
|
||||
}
|
||||
name, _ := fn.Get("name")
|
||||
fnObj := NewPyObj()
|
||||
fnObj.Set("name", name)
|
||||
fnObj.Set("arguments", argsRaw)
|
||||
entry := NewPyObj()
|
||||
entry.Set("function", fnObj)
|
||||
// oracle: if c.get("id") — any truthy value is preserved.
|
||||
if idRaw, _ := co.Get("id"); PyTruthy(idRaw) {
|
||||
entry.Set("id", idRaw)
|
||||
}
|
||||
toolCalls = append(toolCalls, entry)
|
||||
}
|
||||
out := NewPyObj()
|
||||
out.Set("role", "assistant")
|
||||
contentRaw, _ := message.Get("content")
|
||||
content := ""
|
||||
if s, isStr := contentRaw.(string); isStr && s != "" {
|
||||
content = s
|
||||
}
|
||||
out.Set("content", content)
|
||||
if len(toolCalls) > 0 {
|
||||
out.Set("tool_calls", toolCalls)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// chatInput — the chat() keyword contract.
|
||||
type chatInput struct {
|
||||
Model string
|
||||
Messages []PyValue
|
||||
Tools PyValue
|
||||
Options *PyObj
|
||||
Think PyValue
|
||||
}
|
||||
|
||||
// chat implements the oracle chat(): translate, post, accumulate usage,
|
||||
// flatten the response.
|
||||
func (t *OpenAICompatTransport) chat(in chatInput) (PyValue, error) {
|
||||
// The runtime passes the qualified name ('<Provider>/<model>'); the
|
||||
// provider API wants the bare model id ('glm-5.3-flash'). Observed
|
||||
// live: qualified name → 400 on every call (PL1 held-out attempt).
|
||||
wireModel := in.Model
|
||||
if idx := strings.Index(in.Model, "/"); idx >= 0 {
|
||||
wireModel = in.Model[idx+1:]
|
||||
}
|
||||
payload := NewPyObj()
|
||||
payload.Set("model", wireModel)
|
||||
payload.Set("messages", toOpenaiMessages(in.Messages))
|
||||
payload.Set("stream", false)
|
||||
if PyTruthy(in.Tools) {
|
||||
payload.Set("tools", in.Tools) // Ollama tool schema == OpenAI tool schema
|
||||
}
|
||||
if in.Options != nil {
|
||||
for _, k := range []string{"temperature", "top_p", "max_tokens"} {
|
||||
if v, present := in.Options.Get(k); present {
|
||||
payload.Set(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
started := time.Now()
|
||||
resp, err := t.wirePost("/chat/completions", payload)
|
||||
elapsedNS := time.Since(started).Nanoseconds()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usageRaw, _ := resp.(*PyObj).Get("usage")
|
||||
usage, _ := usageRaw.(*PyObj)
|
||||
if usage == nil {
|
||||
usage = NewPyObj()
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.usageCalls++
|
||||
t.usagePromptTokens += pyIntOr(usage, "prompt_tokens")
|
||||
t.usageOutputTokens += pyIntOr(usage, "completion_tokens")
|
||||
t.usageTotalDurationNS += elapsedNS
|
||||
t.mu.Unlock()
|
||||
return fromOpenaiResponse(resp), nil
|
||||
}
|
||||
|
||||
// pyIntOr — Python int(usage.get(key) or 0) coercion: falsy → 0,
|
||||
// int strings parse (floats like 3.9 truncate toward zero), bools → 0/1.
|
||||
func pyIntOr(o *PyObj, key string) int {
|
||||
v, _ := o.Get(key)
|
||||
if !PyTruthy(v) {
|
||||
return 0
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case json.Number:
|
||||
if f, err := t.Float64(); err == nil {
|
||||
return int(f)
|
||||
}
|
||||
case string:
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil {
|
||||
return n
|
||||
}
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(t), 64); err == nil {
|
||||
return int(f)
|
||||
}
|
||||
case bool:
|
||||
if t {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// supportsTools — ChatTransport protocol.
|
||||
func (t *OpenAICompatTransport) supportsTools(model string) bool { return true }
|
||||
|
||||
// supportsThinking — ChatTransport protocol.
|
||||
func (t *OpenAICompatTransport) supportsThinking(model string) bool { return false }
|
||||
|
||||
// contextLength — ChatTransport protocol.
|
||||
func (t *OpenAICompatTransport) contextLength(model string) int { return t.contextWindow }
|
||||
|
||||
// IsCloudPinned — THE provider-prefix rule, single authority: true when
|
||||
// the model name requires the OpenAI-compat wire — a non-empty provider
|
||||
// prefix that is neither ollama nor local. Consumers: transport selection
|
||||
// and context discovery. Never copy this rule — import it.
|
||||
func IsCloudPinned(model string) bool {
|
||||
prefix := ""
|
||||
if strings.Contains(model, "/") {
|
||||
prefix = strings.ToLower(strings.TrimSpace(strings.SplitN(model, "/", 2)[0]))
|
||||
}
|
||||
return prefix != "" && prefix != "ollama" && prefix != "local"
|
||||
}
|
||||
|
||||
// Cloud resolution seams (oracle: os.path.expanduser + Path.is_file +
|
||||
// Path.read_text + os.environ.get). Injectable for the frozen battery.
|
||||
var (
|
||||
// ExpandHome resolves a leading "~" to the operator home.
|
||||
ExpandHome = func(p string) string {
|
||||
if p == "~" || strings.HasPrefix(p, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
if p == "~" {
|
||||
return home
|
||||
}
|
||||
return filepath.Join(home, p[2:])
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
// ReadFileText returns (text, exists, err) for a config file.
|
||||
ReadFileText = func(path string) (string, bool, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
return string(data), true, nil
|
||||
}
|
||||
// LookupEnv mirrors os.environ.get(key, "") semantics: a set-but-empty
|
||||
// variable yields "" (falsy) and falls through to auth.json.
|
||||
LookupEnv = func(key string) (string, bool) { return os.LookupEnv(key) }
|
||||
)
|
||||
|
||||
var (
|
||||
providerBlockRe = func(provider string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`"` + regexp.QuoteMeta(provider) + `"\s*:\s*\{`)
|
||||
}
|
||||
baseURLRe = regexp.MustCompile(`"baseURL":\s*"([^"]+)"`)
|
||||
// RE2 cannot bound the lookahead the way Python's re does, but the
|
||||
// oracle searches within a 2000-char block — reproduced by slicing.
|
||||
contextWindowReFor = func(shortModel string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`"` + regexp.QuoteMeta(shortModel) + `"\s*:\s*\{[^}]*?"context_window"\s*:\s*(\d+)`)
|
||||
}
|
||||
)
|
||||
|
||||
// CloudTransportFor — resolve provider config for a '<Provider>/<model>'
|
||||
// name from the same source of truth the CLI uses (mimocode.jsonc +
|
||||
// auth.json).
|
||||
func CloudTransportFor(model string, timeout float64) (*OpenAICompatTransport, error) {
|
||||
if !strings.Contains(model, "/") {
|
||||
return nil, &ValueError{Msg: fmt.Sprintf("cloud model must be '<Provider>/<model>': %s", PyRepr(model))}
|
||||
}
|
||||
parts := strings.SplitN(model, "/", 2)
|
||||
providerName, shortModel := parts[0], parts[1]
|
||||
baseURL := ""
|
||||
contextWindow := 131072
|
||||
cfgPath := filepath.ToSlash(ExpandHome("~/.config/mimocode/mimocode.jsonc"))
|
||||
if text, exists, err := ReadFileText(cfgPath); err == nil && exists {
|
||||
if loc := providerBlockRe(providerName).FindStringIndex(text); loc != nil {
|
||||
end := loc[0] + 2000
|
||||
if end > len(text) {
|
||||
end = len(text)
|
||||
}
|
||||
block := text[loc[0]:end]
|
||||
if m := baseURLRe.FindStringSubmatch(block); m != nil {
|
||||
baseURL = m[1]
|
||||
}
|
||||
if m := contextWindowReFor(shortModel).FindStringSubmatch(block); m != nil {
|
||||
if n, err := strconv.Atoi(m[1]); err == nil {
|
||||
contextWindow = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if baseURL == "" {
|
||||
return nil, &ValueError{Msg: fmt.Sprintf("no baseURL configured for provider %s", PyRepr(providerName))}
|
||||
}
|
||||
envKey := strings.ReplaceAll(strings.ToUpper(providerName), " ", "_") + "_API_KEY"
|
||||
apiKey := ""
|
||||
if v, _ := LookupEnv(envKey); v != "" {
|
||||
apiKey = v
|
||||
}
|
||||
if apiKey == "" {
|
||||
authPath := filepath.ToSlash(ExpandHome("~/.local/share/mimocode/auth.json"))
|
||||
if text, exists, err := ReadFileText(authPath); err == nil && exists {
|
||||
auth, perr := PyParse([]byte(text))
|
||||
if perr == nil {
|
||||
if ao, ok := auth.(*PyObj); ok {
|
||||
entryRaw, _ := ao.Get(providerName)
|
||||
entry, _ := entryRaw.(*PyObj)
|
||||
if entry != nil {
|
||||
if k, present := entry.Get("key"); present {
|
||||
if s, isStr := k.(string); isStr {
|
||||
apiKey = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if apiKey == "" {
|
||||
return nil, &ValueError{Msg: fmt.Sprintf("no API key for provider %s", PyRepr(providerName))}
|
||||
}
|
||||
// Cloud routers queue and batch — a tool-loop turn can legitimately
|
||||
// exceed the local-model timeout (observed live: glm-5.3-flash
|
||||
// read-timeout at 120s twice). Floor cloud transports at 300s.
|
||||
if timeout < 300 {
|
||||
timeout = 300
|
||||
}
|
||||
t := NewOpenAICompatTransportWith(baseURL, apiKey, providerName, timeout, contextWindow)
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// phaseb.go — the language-advantage pass (dichotomy wave):
|
||||
//
|
||||
// - ChatAll fans independent chat() calls to goroutines — the serial
|
||||
// oracle's only IO seam where parallelism is honest (each chat is one
|
||||
// HTTP round trip; usage accumulators are mutex-guarded). The frozen
|
||||
// battery replays bytes through the parallel path.
|
||||
// - Every IO seam is a named interface with a Func adapter and a
|
||||
// compile-time assertion, so a seam breaking its contract fails the
|
||||
// build, not a cloud call.
|
||||
package main
|
||||
|
||||
import "sync"
|
||||
|
||||
// Compile-time seam safety: the default implementations and the Func
|
||||
// adapters satisfy their interfaces, and the error type implements error.
|
||||
// A seam drifting from its contract fails the build, not a cloud call.
|
||||
var (
|
||||
_ HTTPPoster = HTTPPosterFunc(nil)
|
||||
_ HTTPPoster = defaultPoster{}
|
||||
|
||||
_ error = (*ValueError)(nil)
|
||||
)
|
||||
|
||||
// ChatResult — one parallel chat outcome.
|
||||
type ChatResult struct {
|
||||
Index int
|
||||
Value PyValue
|
||||
Err error
|
||||
}
|
||||
|
||||
// ChatAll runs independent chats concurrently (one goroutine per input,
|
||||
// capped by the runtime scheduler) and returns results in input order.
|
||||
// Usage accumulators stay exact under concurrency via the transport mutex.
|
||||
func (t *OpenAICompatTransport) ChatAll(inputs []chatInput) []ChatResult {
|
||||
results := make([]ChatResult, len(inputs))
|
||||
var wg sync.WaitGroup
|
||||
for i := range inputs {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
v, err := t.chat(inputs[i])
|
||||
results[i] = ChatResult{Index: i, Value: v, Err: err}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Phase B — the language-advantage pass. ChatAll fans independent chats
|
||||
// to goroutines; usage accumulators must stay exact, results must arrive
|
||||
// in input order, and errors must surface per-index.
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPhaseBParallelChatOrderAndUsage(t *testing.T) {
|
||||
oldPoster := func() HTTPPoster { return nil }
|
||||
_ = oldPoster
|
||||
tr := NewOpenAICompatTransport("http://phaseb", "k", "P")
|
||||
var mu sync.Mutex
|
||||
n := 0
|
||||
tr.SetPoster(HTTPPosterFunc(func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
mu.Lock()
|
||||
n++
|
||||
mu.Unlock()
|
||||
return []byte(`{"choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 3, "completion_tokens": 2}}`), nil
|
||||
}))
|
||||
inputs := make([]chatInput, 8)
|
||||
for i := range inputs {
|
||||
inputs[i] = chatInput{Model: "P/m", Messages: []PyValue{}}
|
||||
}
|
||||
results := tr.ChatAll(inputs)
|
||||
for i, r := range results {
|
||||
if r.Err != nil {
|
||||
t.Fatalf("index %d: %v", i, r.Err)
|
||||
}
|
||||
if r.Index != i {
|
||||
t.Fatalf("index %d out of order: %+v", i, r)
|
||||
}
|
||||
o := mustObj(r.Value)
|
||||
if c, _ := o.Get("content"); c != "ok" {
|
||||
t.Fatalf("index %d content: %v", i, c)
|
||||
}
|
||||
}
|
||||
if tr.usageCalls != 8 || tr.usagePromptTokens != 24 || tr.usageOutputTokens != 16 {
|
||||
t.Fatalf("usage drift: %+v", tr)
|
||||
}
|
||||
if tr.usageTotalDurationNS <= 0 {
|
||||
t.Fatal("duration not accumulated under parallel chats")
|
||||
}
|
||||
if n != 8 {
|
||||
t.Fatalf("poster calls: got %d want 8", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseBHammer(t *testing.T) {
|
||||
tr := NewOpenAICompatTransport("http://phaseb", "k", "P")
|
||||
tr.SetPoster(HTTPPosterFunc(func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
return []byte(`{"choices": [{"message": {"content": "x"}}]}`), nil
|
||||
}))
|
||||
inputs := make([]chatInput, 32)
|
||||
for i := range inputs {
|
||||
inputs[i] = chatInput{Model: "P/m", Messages: []PyValue{}}
|
||||
}
|
||||
results := tr.ChatAll(inputs)
|
||||
for i, r := range results {
|
||||
if r.Err != nil {
|
||||
t.Fatalf("index %d: %v", i, r.Err)
|
||||
}
|
||||
}
|
||||
if tr.usageCalls != 32 {
|
||||
t.Fatalf("usage calls: got %d want 32", tr.usageCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseBParallelErrorsPerIndex(t *testing.T) {
|
||||
tr := NewOpenAICompatTransport("http://phaseb", "k", "P")
|
||||
tr.SetPoster(HTTPPosterFunc(func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
return []byte(`[]`), nil
|
||||
}))
|
||||
inputs := []chatInput{
|
||||
{Model: "P/m", Messages: []PyValue{}},
|
||||
{Model: "P/m", Messages: []PyValue{}},
|
||||
}
|
||||
results := tr.ChatAll(inputs)
|
||||
for i, r := range results {
|
||||
if r.Err == nil {
|
||||
t.Fatalf("index %d: expected error", i)
|
||||
}
|
||||
if !strings.Contains(r.Err.Error(), "non-object response") {
|
||||
t.Fatalf("index %d: %v", i, r.Err)
|
||||
}
|
||||
}
|
||||
if tr.usageCalls != 0 {
|
||||
t.Fatalf("failed chats must not accumulate: %+v", tr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
// pyjson.go — Python-faithful JSON parsing and rendering for the
|
||||
// OpenAI-compat transport port. The oracle round-trips wire payloads through
|
||||
// json.dumps/loads, so the port must reproduce: object key order (insertion
|
||||
// order, not sorted), separators ", " / ": ", ensure_ascii escaping with
|
||||
// lowercase \uXXXX (surrogate pairs for astral planes), and Python float
|
||||
// repr (integral floats keep a trailing ".0" — json.dumps(2.0) is "2.0").
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PyValue — a JSON value with order-preserving objects.
|
||||
// nil | bool | json.Number | string | []PyValue | *PyObj
|
||||
type PyValue any
|
||||
|
||||
// PyObj — a JSON object preserving insertion order.
|
||||
type PyObj struct {
|
||||
Keys []string
|
||||
Vals map[string]PyValue
|
||||
}
|
||||
|
||||
func NewPyObj() *PyObj { return &PyObj{Vals: map[string]PyValue{}} }
|
||||
|
||||
func (o *PyObj) Get(key string) (PyValue, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (o *PyObj) Set(key string, v PyValue) {
|
||||
if _, dup := o.Vals[key]; !dup {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.Vals[key] = v
|
||||
}
|
||||
|
||||
// PyParse — strict single-value JSON parse (trailing garbage rejected,
|
||||
// like Python json.loads).
|
||||
func PyParse(raw []byte) (PyValue, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
v, err := decodeValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := dec.Token(); err != io.EOF {
|
||||
return nil, fmt.Errorf("trailing data after JSON value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeValue(dec *json.Decoder) (PyValue, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeFromToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeFromToken(dec *json.Decoder, tok json.Token) (PyValue, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
obj := NewPyObj()
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := keyTok.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("object key is not a string")
|
||||
}
|
||||
val, err := decodeValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing '}'
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
case '[':
|
||||
arr := []PyValue{}
|
||||
for dec.More() {
|
||||
val, err := decodeValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing ']'
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected delim %q", t)
|
||||
}
|
||||
case json.Number:
|
||||
return t, nil
|
||||
case string:
|
||||
return t, nil
|
||||
case bool:
|
||||
return t, nil
|
||||
case nil:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected token %v", tok)
|
||||
}
|
||||
}
|
||||
|
||||
// PyDumpsCompact — Python json.dumps(value): separators ", " / ": ",
|
||||
// ensure_ascii=True, insertion-order objects.
|
||||
func PyDumpsCompact(v PyValue) string {
|
||||
var b strings.Builder
|
||||
writePy(&b, v)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writePy(b *strings.Builder, v PyValue) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
b.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
case json.Number:
|
||||
b.WriteString(PyNumStr(t))
|
||||
case string:
|
||||
writePyString(b, t)
|
||||
case []PyValue:
|
||||
b.WriteByte('[')
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePy(b, item)
|
||||
}
|
||||
b.WriteByte(']')
|
||||
case *PyObj:
|
||||
b.WriteByte('{')
|
||||
for i, k := range t.Keys {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePyString(b, k)
|
||||
b.WriteString(": ")
|
||||
writePy(b, t.Vals[k])
|
||||
}
|
||||
b.WriteByte('}')
|
||||
default:
|
||||
b.WriteString(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// PyNumStr — Python json.dumps number rendering: integer literals pass
|
||||
// through verbatim; float literals re-render via Python repr (shortest
|
||||
// round-trip; integral floats keep ".0"). Fixture numbers are written by
|
||||
// json.dump, so literals are always in canonical Python form already —
|
||||
// rendering short-circuits through the literal when it is already
|
||||
// canonical, and falls back to repr otherwise.
|
||||
func PyNumStr(n json.Number) string {
|
||||
s := n.String()
|
||||
if pyNumCanonical(s) {
|
||||
return s
|
||||
}
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return pyFloatRepr(f)
|
||||
}
|
||||
|
||||
func pyNumCanonical(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
hasDot := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
case c == '.':
|
||||
if hasDot {
|
||||
return false
|
||||
}
|
||||
hasDot = true
|
||||
case c == 'e' || c == 'E' || c == '+' || c == '-':
|
||||
if c == '-' && i == 0 {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// pyFloatRepr — Python repr() of a float: shortest round-trip, integral
|
||||
// values keep ".0". Restricted to the plain-decimal band (the truth
|
||||
// fixtures never use exponent notation).
|
||||
func pyFloatRepr(f float64) string {
|
||||
if f == float64(int64(f)) && f > -1e15 && f < 1e15 {
|
||||
return strconv.FormatInt(int64(f), 10) + ".0"
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// writePyString — Python json.dumps string escaping, ensure_ascii=True.
|
||||
func writePyString(b *strings.Builder, s string) {
|
||||
b.WriteByte('"')
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case '"':
|
||||
b.WriteString("\\\"")
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
case '\b':
|
||||
b.WriteString("\\b")
|
||||
case '\f':
|
||||
b.WriteString("\\f")
|
||||
default:
|
||||
if c < 0x20 {
|
||||
fmt.Fprintf(b, "\\u%04x", c)
|
||||
} else if c <= 0x7E {
|
||||
b.WriteRune(c)
|
||||
} else if cp := uint32(c); cp > 0xFFFF {
|
||||
v := cp - 0x10000
|
||||
fmt.Fprintf(b, "\\u%04x\\u%04x", 0xD800+(v>>10), 0xDC00+(v&0x3FF))
|
||||
} else {
|
||||
fmt.Fprintf(b, "\\u%04x", cp)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
// PyRepr — Python repr() of a string: single quotes, backslash/quote
|
||||
// escaping (the shape a model name takes in oracle error strings).
|
||||
func PyRepr(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('\'')
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\'':
|
||||
b.WriteString("\\'")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
default:
|
||||
b.WriteRune(c)
|
||||
}
|
||||
}
|
||||
b.WriteByte('\'')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PyStr — Python str() of a JSON value for the non-dict arguments branch
|
||||
// of _to_openai_messages (str(args) when args is not a dict).
|
||||
func PyStr(v PyValue) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case json.Number:
|
||||
return PyNumStr(t)
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
return pyReprValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
// pyReprValue — Python repr() of list/dict JSON values.
|
||||
func pyReprValue(v PyValue) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return PyRepr(t)
|
||||
case []PyValue:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = pyReprValue(item)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case *PyObj:
|
||||
parts := make([]string, 0, len(t.Keys))
|
||||
for _, k := range t.Keys {
|
||||
parts = append(parts, PyRepr(k)+": "+pyReprValue(t.Vals[k]))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return PyStr(v)
|
||||
}
|
||||
}
|
||||
|
||||
// PyTruthy — Python truthiness of a JSON value.
|
||||
func PyTruthy(v PyValue) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
case json.Number:
|
||||
f, err := t.Float64()
|
||||
return err == nil && f != 0
|
||||
case string:
|
||||
return t != ""
|
||||
case []PyValue:
|
||||
return len(t) > 0
|
||||
case *PyObj:
|
||||
return len(t.Keys) > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// PyEqual — deep equality where numbers compare numerically (int 120 ==
|
||||
// float 120.0), matching semantic oracle equality rather than literal text.
|
||||
func PyEqual(a, b PyValue) bool {
|
||||
an, aok := a.(json.Number)
|
||||
bn, bok := b.(json.Number)
|
||||
if aok && bok {
|
||||
if an.String() == bn.String() {
|
||||
return true
|
||||
}
|
||||
af, aerr := an.Float64()
|
||||
bf, berr := bn.Float64()
|
||||
return aerr == nil && berr == nil && af == bf
|
||||
}
|
||||
switch at := a.(type) {
|
||||
case nil:
|
||||
return b == nil
|
||||
case bool:
|
||||
bt, ok := b.(bool)
|
||||
return ok && at == bt
|
||||
case string:
|
||||
bt, ok := b.(string)
|
||||
return ok && at == bt
|
||||
case []PyValue:
|
||||
bt, ok := b.([]PyValue)
|
||||
if !ok || len(at) != len(bt) {
|
||||
return false
|
||||
}
|
||||
for i := range at {
|
||||
if !PyEqual(at[i], bt[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *PyObj:
|
||||
bt, ok := b.(*PyObj)
|
||||
if !ok || len(at.Keys) != len(bt.Keys) {
|
||||
return false
|
||||
}
|
||||
for i, k := range at.Keys {
|
||||
if bt.Keys[i] != k {
|
||||
return false
|
||||
}
|
||||
if !PyEqual(at.Vals[k], bt.Vals[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// scripts/gen_openai_compat_transport_truth.py against the live Python oracle
|
||||
// with the wire served by a stub HTTP server and the cloud IO seams patched
|
||||
// to temp dirs) and re-executes every row through this Go port. @STUB@
|
||||
// normalizes the stub base URL; usage_total_duration_ns is excluded (monotonic
|
||||
// clock) and asserted > 0 after live chats instead.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
Generator string `json:"generator"`
|
||||
Note string `json:"note"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on
|
||||
// 100% parity).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for i := range doc.Rows {
|
||||
if msg := replayRow(&doc.Rows[i]); msg != "" {
|
||||
fails = append(fails, doc.Rows[i].Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
func truthRowCount() int {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func replayRow(row *TruthRow) string {
|
||||
var setup, want PyValue
|
||||
var err error
|
||||
if setup, err = PyParse(row.Setup); err != nil {
|
||||
return fmt.Sprintf("parse setup: %v", err)
|
||||
}
|
||||
if want, err = PyParse(row.Want); err != nil {
|
||||
return fmt.Sprintf("parse want: %v", err)
|
||||
}
|
||||
s := mustObj(setup)
|
||||
w := want
|
||||
switch row.Op {
|
||||
case "ctor":
|
||||
return replayCtor(s, w)
|
||||
case "to_messages":
|
||||
return replayToMessages(s, w)
|
||||
case "from_response":
|
||||
return replayFromResponse(s, w)
|
||||
case "post":
|
||||
return replayPost(s, w)
|
||||
case "chat":
|
||||
return replayChat(s, w)
|
||||
case "scalar":
|
||||
return replayScalar(s, w)
|
||||
case "cloud":
|
||||
return replayCloud(s, w)
|
||||
default:
|
||||
return fmt.Sprintf("unknown op %q", row.Op)
|
||||
}
|
||||
}
|
||||
|
||||
func mustObj(v PyValue) *PyObj {
|
||||
o, _ := v.(*PyObj)
|
||||
if o == nil {
|
||||
return NewPyObj()
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func strOf(v PyValue) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func numOf(v PyValue) float64 {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
f, _ := n.Float64()
|
||||
return f
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func listOf(v PyValue) []PyValue {
|
||||
l, _ := v.([]PyValue)
|
||||
if l == nil {
|
||||
return []PyValue{}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// -- ctor --------------------------------------------------------------------
|
||||
|
||||
func replayCtor(s *PyObj, w PyValue) string {
|
||||
base, _ := s.Get("base_url")
|
||||
key, _ := s.Get("api_key")
|
||||
prov, _ := s.Get("provider")
|
||||
timeout := 120.0
|
||||
if tv, present := s.Get("timeout"); present {
|
||||
timeout = numOf(tv)
|
||||
}
|
||||
ctxWin := 131072
|
||||
if cv, present := s.Get("context_window"); present {
|
||||
ctxWin = int(numOf(cv))
|
||||
}
|
||||
t := NewOpenAICompatTransportWith(strOf(base), strOf(key), strOf(prov), timeout, ctxWin)
|
||||
got := transportAttrs(t)
|
||||
if msg := compareJSON("attrs", got, w); msg != "" {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func transportAttrs(t *OpenAICompatTransport) PyValue {
|
||||
o := NewPyObj()
|
||||
o.Set("base_url", t.BaseURL)
|
||||
o.Set("api_key", t.APIKey)
|
||||
o.Set("provider", t.Provider)
|
||||
o.Set("timeout", json.Number(fmt.Sprintf("%v", t.Timeout)))
|
||||
o.Set("context_window", json.Number(fmt.Sprintf("%d", t.contextWindow)))
|
||||
o.Set("usage_calls", json.Number(fmt.Sprintf("%d", t.usageCalls)))
|
||||
o.Set("usage_prompt_tokens", json.Number(fmt.Sprintf("%d", t.usagePromptTokens)))
|
||||
o.Set("usage_output_tokens", json.Number(fmt.Sprintf("%d", t.usageOutputTokens)))
|
||||
o.Set("usage_total_duration_ns", json.Number(fmt.Sprintf("%d", t.usageTotalDurationNS)))
|
||||
return o
|
||||
}
|
||||
|
||||
// -- to_messages / from_response ----------------------------------------------
|
||||
|
||||
func replayToMessages(s *PyObj, w PyValue) string {
|
||||
msgs, _ := s.Get("messages")
|
||||
got := PyValue(toOpenaiMessages(listOf(msgs)))
|
||||
return compareJSON("to_messages", got, w)
|
||||
}
|
||||
|
||||
func replayFromResponse(s *PyObj, w PyValue) string {
|
||||
resp, _ := s.Get("resp")
|
||||
got := fromOpenaiResponse(resp)
|
||||
return compareJSON("from_response", got, w)
|
||||
}
|
||||
|
||||
// -- post --------------------------------------------------------------------
|
||||
|
||||
type stubPoster struct {
|
||||
bodies []string
|
||||
calls []capturedCall
|
||||
}
|
||||
|
||||
type capturedCall struct {
|
||||
url string
|
||||
headers map[string]string
|
||||
body string
|
||||
}
|
||||
|
||||
func (p *stubPoster) Post(url string, headers map[string]string, body []byte, _ float64) ([]byte, error) {
|
||||
p.calls = append(p.calls, capturedCall{url, headers, string(body)})
|
||||
resp := p.bodies[0]
|
||||
p.bodies = p.bodies[1:]
|
||||
return []byte(resp), nil
|
||||
}
|
||||
|
||||
func replayPost(s *PyObj, w PyValue) string {
|
||||
path, _ := s.Get("path")
|
||||
payload, _ := s.Get("payload")
|
||||
bodiesRaw, _ := s.Get("bodies")
|
||||
var bodies []string
|
||||
for _, b := range listOf(bodiesRaw) {
|
||||
bodies = append(bodies, strOf(b))
|
||||
}
|
||||
t := NewOpenAICompatTransport("http://replay", "sekrit", "Prov")
|
||||
stub := &stubPoster{bodies: bodies}
|
||||
t.SetPoster(stub)
|
||||
var got PyValue
|
||||
result, err := t.wirePost(strOf(path), payload)
|
||||
if err != nil {
|
||||
eo := NewPyObj()
|
||||
eo.Set("error", err.Error())
|
||||
got = PyValue(eo)
|
||||
} else {
|
||||
got = result
|
||||
}
|
||||
call := stub.calls[0]
|
||||
reqObj := NewPyObj()
|
||||
reqObj.Set("path", strings.TrimPrefix(call.url, "http://replay"))
|
||||
reqObj.Set("authorization", call.headers["Authorization"])
|
||||
reqObj.Set("content_type", call.headers["Content-Type"])
|
||||
reqObj.Set("body", call.body)
|
||||
wo := mustObj(w)
|
||||
if msg := compareJSON("request", reqObj, mustGet(wo, "request")); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if _, wantErr := wo.Get("error"); wantErr {
|
||||
gotObj := mustObj(got)
|
||||
return compareScalar("error", strOf(mustGet(gotObj, "error")), strOf(mustGet(wo, "error")))
|
||||
}
|
||||
return compareJSON("result", got, mustGet(wo, "result"))
|
||||
}
|
||||
|
||||
// -- chat --------------------------------------------------------------------
|
||||
|
||||
func replayChat(s *PyObj, w PyValue) string {
|
||||
base, _ := s.Get("base_url")
|
||||
key, _ := s.Get("api_key")
|
||||
prov, _ := s.Get("provider")
|
||||
model, _ := s.Get("model")
|
||||
msgs, _ := s.Get("messages")
|
||||
tools, toolsPresent := s.Get("tools")
|
||||
options, _ := s.Get("options")
|
||||
think, thinkPresent := s.Get("think")
|
||||
respRaw, _ := s.Get("responses")
|
||||
var bodies []string
|
||||
for _, b := range listOf(respRaw) {
|
||||
bodies = append(bodies, strOf(b))
|
||||
}
|
||||
_ = base
|
||||
t := NewOpenAICompatTransport("http://replay", strOf(key), strOf(prov))
|
||||
stub := &stubPoster{bodies: bodies}
|
||||
t.SetPoster(stub)
|
||||
var in chatInput
|
||||
in.Model = strOf(model)
|
||||
in.Messages = listOf(msgs)
|
||||
if toolsPresent {
|
||||
in.Tools = tools
|
||||
}
|
||||
if oo, ok := options.(*PyObj); ok {
|
||||
in.Options = oo
|
||||
}
|
||||
if thinkPresent {
|
||||
in.Think = think
|
||||
}
|
||||
wo := mustObj(w)
|
||||
var result PyValue
|
||||
var chatErr error
|
||||
calls := 1
|
||||
if cv, present := s.Get("calls"); present {
|
||||
calls = int(numOf(cv))
|
||||
}
|
||||
for c := 0; c < calls; c++ {
|
||||
result, chatErr = t.chat(in)
|
||||
}
|
||||
var gotResult PyValue
|
||||
if chatErr != nil {
|
||||
eo := NewPyObj()
|
||||
eo.Set("error", chatErr.Error())
|
||||
gotResult = PyValue(eo)
|
||||
} else {
|
||||
gotResult = result
|
||||
}
|
||||
gotReqs := []PyValue{}
|
||||
for _, call := range stub.calls {
|
||||
body, perr := PyParse([]byte(call.body))
|
||||
if perr != nil {
|
||||
return fmt.Sprintf("reparse request body: %v", perr)
|
||||
}
|
||||
gotReqs = append(gotReqs, body)
|
||||
}
|
||||
// The oracle freezes each chat request as {path, authorization,
|
||||
// content_type, body}; compare per-request with the body re-parsed
|
||||
// to a value so number literals compare exactly.
|
||||
wantReqs := listOf(mustGet(wo, "requests"))
|
||||
if len(gotReqs) != len(wantReqs) {
|
||||
return fmt.Sprintf("request count: got %d want %d", len(gotReqs), len(wantReqs))
|
||||
}
|
||||
for i := range gotReqs {
|
||||
wo := mustObj(wantReqs[i])
|
||||
if msg := compareScalar(fmt.Sprintf("request[%d].path", i), "/chat/completions", strOf(mustGet(wo, "path"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar(fmt.Sprintf("request[%d].auth", i), "Bearer "+strOf(key), strOf(mustGet(wo, "authorization"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar(fmt.Sprintf("request[%d].ctype", i), "application/json", strOf(mustGet(wo, "content_type"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
wantBody, perr := PyParse([]byte(strOf(mustGet(wo, "body"))))
|
||||
if perr != nil {
|
||||
return fmt.Sprintf("reparse want body: %v", perr)
|
||||
}
|
||||
if msg := compareJSON(fmt.Sprintf("request[%d].body", i), gotReqs[i], wantBody); msg != "" {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
if _, wantErr := wo.Get("error"); wantErr {
|
||||
gotObj := mustObj(gotResult)
|
||||
if msg := compareScalar("error", strOf(mustGet(gotObj, "error")), strOf(mustGet(wo, "error"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
} else if msg := compareJSON("result", gotResult, mustGet(wo, "result")); msg != "" {
|
||||
return msg
|
||||
}
|
||||
usage := mustObj(mustGet(wo, "usage"))
|
||||
if msg := compareScalar("usage.calls", itoa(t.usageCalls), numStr(mustGet(usage, "calls"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar("usage.prompt", itoa(t.usagePromptTokens), numStr(mustGet(usage, "prompt_tokens"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar("usage.output", itoa(t.usageOutputTokens), numStr(mustGet(usage, "output_tokens"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if t.usageTotalDurationNS <= 0 && chatErr == nil {
|
||||
return "usage_total_duration_ns not accumulated"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// -- scalar ------------------------------------------------------------------
|
||||
|
||||
func replayScalar(s *PyObj, w PyValue) string {
|
||||
fn, _ := s.Get("fn")
|
||||
model, _ := s.Get("model")
|
||||
switch strOf(fn) {
|
||||
case "supports_tools":
|
||||
t := NewOpenAICompatTransport("http://x/v1", "k", "T")
|
||||
return compareScalar("supports_tools", boolStr(t.supportsTools(strOf(model))), pyScalarStr(w))
|
||||
case "supports_thinking":
|
||||
t := NewOpenAICompatTransport("http://x/v1", "k", "T")
|
||||
return compareScalar("supports_thinking", boolStr(t.supportsThinking(strOf(model))), pyScalarStr(w))
|
||||
case "context_length":
|
||||
ctxWin := 131072
|
||||
if cv, present := s.Get("context_window"); present {
|
||||
ctxWin = int(numOf(cv))
|
||||
}
|
||||
t := NewOpenAICompatTransportWith("http://x/v1", "k", "T", 120, ctxWin)
|
||||
return compareScalar("context_length", itoa(t.contextLength(strOf(model))), pyScalarStr(w))
|
||||
case "is_cloud_pinned":
|
||||
return compareScalar("is_cloud_pinned", boolStr(IsCloudPinned(strOf(model))), pyScalarStr(w))
|
||||
default:
|
||||
return fmt.Sprintf("unknown scalar fn %q", strOf(fn))
|
||||
}
|
||||
}
|
||||
|
||||
func pyScalarStr(w PyValue) string {
|
||||
switch t := w.(type) {
|
||||
case bool:
|
||||
return boolStr(t)
|
||||
case json.Number:
|
||||
return t.String()
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
return fmt.Sprintf("%v", w)
|
||||
}
|
||||
}
|
||||
|
||||
// -- cloud -------------------------------------------------------------------
|
||||
|
||||
func replayCloud(s *PyObj, w PyValue) string {
|
||||
model, _ := s.Get("model")
|
||||
timeout := 120.0
|
||||
if tv, present := s.Get("timeout"); present {
|
||||
timeout = numOf(tv)
|
||||
}
|
||||
envRaw, _ := s.Get("env")
|
||||
env := map[string]string{}
|
||||
if eo, ok := envRaw.(*PyObj); ok {
|
||||
for _, k := range eo.Keys {
|
||||
env[k] = strOf(eo.Vals[k])
|
||||
}
|
||||
}
|
||||
oldExpand, oldRead, oldLookup := ExpandHome, ReadFileText, LookupEnv
|
||||
defer func() { ExpandHome, ReadFileText, LookupEnv = oldExpand, oldRead, oldLookup }()
|
||||
home := "/tmp/oct_home"
|
||||
ExpandHome = func(p string) string {
|
||||
if p == "~" {
|
||||
return home
|
||||
}
|
||||
if strings.HasPrefix(p, "~/") {
|
||||
return home + p[1:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
// The oracle patches os.path.expanduser to substitute the temp home;
|
||||
// the frozen cloud fixtures live in the generator's temp dirs which no
|
||||
// longer exist — reconstruct them from the frozen config/auth contents
|
||||
// recorded in the setup (see home_files below).
|
||||
files := map[string]string{}
|
||||
if hf, present := s.Get("home_files"); present {
|
||||
if ho, ok := hf.(*PyObj); ok {
|
||||
for _, k := range ho.Keys {
|
||||
files[home+k] = strOf(ho.Vals[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
ReadFileText = func(path string) (string, bool, error) {
|
||||
if text, ok := files[path]; ok {
|
||||
return text, true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
LookupEnv = func(key string) (string, bool) {
|
||||
v, ok := env[key]
|
||||
return v, ok
|
||||
}
|
||||
t, err := CloudTransportFor(strOf(model), timeout)
|
||||
wo := mustObj(w)
|
||||
if _, wantErr := wo.Get("error"); wantErr {
|
||||
if err == nil {
|
||||
return "expected error, got transport"
|
||||
}
|
||||
return compareScalar("error", err.Error(), strOf(mustGet(wo, "error")))
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Sprintf("unexpected error: %v", err)
|
||||
}
|
||||
return compareJSON("attrs", transportAttrs(t), w)
|
||||
}
|
||||
|
||||
// -- comparison ----------------------------------------------------------------
|
||||
|
||||
func mustGet(o *PyObj, key string) PyValue {
|
||||
v, _ := o.Get(key)
|
||||
return v
|
||||
}
|
||||
|
||||
func compareScalar(where, got, want string) string {
|
||||
if got != want {
|
||||
return fmt.Sprintf("%s: got %q want %q", where, got, want)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func compareJSON(where string, got, want PyValue) string {
|
||||
if !PyEqual(canonicalJSON(got), canonicalJSON(want)) {
|
||||
return fmt.Sprintf("%s: got %s want %s", where, PyDumpsCompact(got), PyDumpsCompact(want))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// canonicalJSON re-parses both sides through the literal-preserving parser
|
||||
// so number comparison is literal-exact (PyEqual adds numeric tolerance).
|
||||
func canonicalJSON(v PyValue) PyValue {
|
||||
re, err := PyParse([]byte(PyDumpsCompact(v)))
|
||||
if err != nil {
|
||||
return v
|
||||
}
|
||||
return re
|
||||
}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// numStr — oracle usage ints arrive as JSON numbers.
|
||||
func numStr(v PyValue) string {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
return n.String()
|
||||
}
|
||||
return strOf(v)
|
||||
}
|
||||
|
||||
func itoa(n int) string { return fmt.Sprintf("%d", n) }
|
||||
|
||||
var _ = reflect.TypeOf
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
// Battery — Go tests replaying the frozen oracle truth VERBATIM.
|
||||
// Every row of truth.json (generated by
|
||||
// scripts/gen_openai_compat_transport_truth.py against the live Python
|
||||
// oracle) re-executes through this port; any divergence fails the test.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read truth: %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth has no rows")
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func TestBatteryRowCount(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
if len(doc.Rows) != 83 {
|
||||
t.Fatalf("row count drift: got %d want 83", len(doc.Rows))
|
||||
}
|
||||
if doc.Oracle != "src/rune/openai_compat_transport.py" {
|
||||
t.Fatalf("oracle drift: %q", doc.Oracle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
for i := range doc.Rows {
|
||||
row := &doc.Rows[i]
|
||||
if msg := replayRow(row); msg != "" {
|
||||
t.Errorf("%s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryByOp(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
byOp := map[string][]*TruthRow{}
|
||||
for i := range doc.Rows {
|
||||
byOp[doc.Rows[i].Op] = append(byOp[doc.Rows[i].Op], &doc.Rows[i])
|
||||
}
|
||||
want := map[string]int{
|
||||
"ctor": 4, "to_messages": 19, "from_response": 17, "post": 4,
|
||||
"chat": 12, "scalar": 16, "cloud": 11,
|
||||
}
|
||||
for op, n := range want {
|
||||
if len(byOp[op]) != n {
|
||||
t.Errorf("op %s: got %d rows want %d", op, len(byOp[op]), n)
|
||||
}
|
||||
}
|
||||
for op, rows := range byOp {
|
||||
t.Run(op, func(t *testing.T) {
|
||||
for _, row := range rows {
|
||||
if msg := replayRow(row); msg != "" {
|
||||
t.Errorf("%s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryDeterministic(t *testing.T) {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read truth: %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth: %v", err)
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, row := range doc.Rows {
|
||||
if names[row.Name] {
|
||||
t.Fatalf("duplicate row name %q", row.Name)
|
||||
}
|
||||
names[row.Name] = true
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.228.0"
|
||||
__version__ = "0.239.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.228.0"' in rune_version,
|
||||
'__version__ = "0.239.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,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust grading core (dichotomy wave, #2479
|
||||
tranche). Mirrors bh_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* gr_affirm_rule({fact}) -> the (possibly rewritten) rule string
|
||||
* gr_grade_fact({fact, target, user_name}) -> the tier name
|
||||
* gr_tier_of({grade}) -> the journal label (G0/G1/G2p/G2/P/G3/G4)
|
||||
|
||||
`user_name` is grading.py's import-time MEMORY_USER_NAME normalization
|
||||
(strip().lower()) — the facade passes its module-level value so the kernel
|
||||
stays pure. Free: capability_scoring's cs_free_string (shared process) —
|
||||
NEVER re-export a free symbol from this crate.
|
||||
Override the .so with GRADING_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" / "grading"
|
||||
_LIB_PATH = Path(os.environ.get("GRADING_RUST_LIB", _CRATE_DIR / "target" / "release" / "libgrading.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
_SYMBOLS = (
|
||||
"gr_affirm_rule",
|
||||
"gr_grade_fact",
|
||||
"gr_tier_of",
|
||||
)
|
||||
|
||||
|
||||
class GradingCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust grading core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def core():
|
||||
"""The shared kernel probe (strangler law): the delegation entry the
|
||||
facade uses. RUNE_GR_CORE=python pins the fallback — THIS is
|
||||
where the pin is honored, so every consumer gets it."""
|
||||
if os.environ.get("RUNE_GR_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
return _load()
|
||||
except Exception: # noqa: BLE001 — build/loader failures mean fallback
|
||||
return None
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise GradingCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
# FFI segfault law: explicit argtypes/restype (c_void_p for returned
|
||||
# char* — the default c_int truncates 64-bit pointers).
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
_free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
"""Doc-envelope free via capability_scoring's cs_free_string — NEVER
|
||||
re-export a free symbol from this crate (duplicate symbols break the
|
||||
cdylib link)."""
|
||||
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.restype = None
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def affirm_rule(fact: str) -> str:
|
||||
return _call("gr_affirm_rule", {"fact": fact})
|
||||
|
||||
|
||||
def grade_fact(fact: str, target: str | None = None, user_name: str = "") -> str:
|
||||
return _call("gr_grade_fact", {"fact": fact, "target": target, "user_name": user_name})
|
||||
|
||||
|
||||
def tier_of(grade: str) -> str:
|
||||
return _call("gr_tier_of", {"grade": grade})
|
||||
+66
-6
@@ -20,6 +20,28 @@ matched only via the optional MEMORY_USER_NAME env var.
|
||||
|
||||
import os
|
||||
|
||||
# ── Rust core wiring (dichotomy-wave doc-envelope pattern) ──────────────────
|
||||
try:
|
||||
from . import gr_loader
|
||||
except ImportError: # direct script execution (curator `import grading`)
|
||||
try:
|
||||
import gr_loader # type: ignore[no-redef]
|
||||
except ImportError: # noqa: S110 — no loader on path → Python oracle below
|
||||
gr_loader = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _gr_core():
|
||||
"""The Rust grading-core probe (strangler law). RUNE_GR_CORE=python
|
||||
pins the Python fallback — the pin is honored inside gr_loader.core
|
||||
so every consumer gets it. Returns the loader MODULE (never the raw
|
||||
CDLL)."""
|
||||
if gr_loader is None:
|
||||
return None
|
||||
try:
|
||||
return gr_loader if gr_loader.core() is not None else None
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
return None
|
||||
|
||||
# --- Imperative / rule signals: a sentence instructing HOW to operate. ------
|
||||
RULE_WEAK = [
|
||||
"should", "prefer", "instead of", "lead with", "first", "before ",
|
||||
@@ -100,8 +122,9 @@ AFFIRM_DICT = {
|
||||
}
|
||||
|
||||
|
||||
def affirm_rule(fact):
|
||||
"""Rewrite a perceived demand/rule as a positive affirmation.
|
||||
def _py_affirm_rule(fact):
|
||||
"""Python fallback: rewrite a perceived demand/rule as a positive
|
||||
affirmation.
|
||||
|
||||
Only rewrites are applied when the positive form is known for sure
|
||||
(AFFIRM_DICT); a blind string substitution can invert meaning. Unknown
|
||||
@@ -116,8 +139,23 @@ def affirm_rule(fact):
|
||||
return f
|
||||
|
||||
|
||||
def grade_fact(fact, target=None):
|
||||
"""Return the tier (G0..G4) for a fact/rule based on type + language.
|
||||
def affirm_rule(fact):
|
||||
"""Rewrite a perceived demand/rule as a positive affirmation.
|
||||
|
||||
Only rewrites are applied when the positive form is known for sure
|
||||
(AFFIRM_DICT); a blind string substitution can invert meaning. Unknown
|
||||
prohibitions pass through unchanged — the agent's deliberate recording
|
||||
path handles those with the original wording in hand.
|
||||
"""
|
||||
core = _gr_core()
|
||||
if core is not None:
|
||||
return core.affirm_rule(fact)
|
||||
return _py_affirm_rule(fact)
|
||||
|
||||
|
||||
def _py_grade_fact(fact, target=None, _user_name=""):
|
||||
"""Python fallback: return the tier (G0..G4) for a fact/rule based on
|
||||
type + language.
|
||||
|
||||
`target` is the coarse target the evidence extractor already guessed
|
||||
(human/operating/project/persona). Grading refines where within the tiers
|
||||
@@ -164,8 +202,22 @@ def grade_fact(fact, target=None):
|
||||
return "hindsight"
|
||||
|
||||
|
||||
def tier_of(grade):
|
||||
"""Human label for a grade, for journaling / canary output."""
|
||||
def grade_fact(fact, target=None):
|
||||
"""Return the tier (G0..G4) for a fact/rule based on type + language.
|
||||
|
||||
`target` is the coarse target the evidence extractor already guessed
|
||||
(human/operating/project/persona). Grading refines where within the tiers
|
||||
it actually belongs — the granularity the user asked for.
|
||||
"""
|
||||
core = _gr_core()
|
||||
if core is not None:
|
||||
return core.grade_fact(fact, target, _user_name)
|
||||
return _py_grade_fact(fact, target, _user_name)
|
||||
|
||||
|
||||
def _py_tier_of(grade):
|
||||
"""Python fallback: human label for a grade, for journaling / canary
|
||||
output."""
|
||||
return {
|
||||
"constitution": "G0",
|
||||
"operating": "G1",
|
||||
@@ -178,6 +230,14 @@ def tier_of(grade):
|
||||
}.get(grade, "G3")
|
||||
|
||||
|
||||
def tier_of(grade):
|
||||
"""Human label for a grade, for journaling / canary output."""
|
||||
core = _gr_core()
|
||||
if core is not None:
|
||||
return core.tier_of(grade)
|
||||
return _py_tier_of(grade)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# quick self-check
|
||||
cases = [
|
||||
|
||||
@@ -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.228.0
|
||||
Version: v0.239.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.228.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.239.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):
|
||||
|
||||
@@ -25,11 +25,36 @@ Verdicts:
|
||||
|
||||
Every verdict carries the QUESTION ANSWERS so the reason is auditable — the
|
||||
journal entry shows WHY, not just the decision.
|
||||
|
||||
Dichotomy wave #2479 tranche (contract
|
||||
BUILDER-2026-10-12T02:14:11Z-69cc9a-factory-wave): the full verdict kernel is
|
||||
ported to FULL Rust (src/rust/worthiness, parity oracle frozen_wr_truth.json).
|
||||
This facade delegates to the Rust core via rune/wr_loader when built; the
|
||||
Python implementation below is the strangler fallback, pinned with
|
||||
RUNE_WR_CORE=python. The kernel owns the domain-terms cache (it must, to
|
||||
mirror the in-place cache-mutation law in _domain_cache/q_assistant_goal);
|
||||
the store read behind it crosses back as a `domain_terms` op served by the
|
||||
loader.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust worthiness core when available (strangler: Python fallback
|
||||
is load-bearing). RUNE_WR_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_WR_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import wr_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return wr_loader if wr_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# The question set. One question per constitution concern. Each returns a
|
||||
# (score, note) where score is +1..-1 and note explains the judgment.
|
||||
@@ -44,6 +69,9 @@ Q_PI_SAFETY = {
|
||||
|
||||
|
||||
def q_pi_safety(low):
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_pi_safety(low)
|
||||
if any(s in low for s in Q_PI_SAFETY["bad"]):
|
||||
if any(s in low for s in ("do not", "never ", "don't", "protect",
|
||||
"ask permission", "check before")):
|
||||
@@ -54,6 +82,9 @@ def q_pi_safety(low):
|
||||
|
||||
def q_honesty(low):
|
||||
"""Inviolable 2 + 3: honesty, sources, no overclaiming."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_honesty(low)
|
||||
honest = ["source", "evidence", "verify", "verifiable", "cross-check",
|
||||
"fact", "measure", "measured", "falsif", "disprove", "cite",
|
||||
"independent confirm", "replicable", "uncertain"]
|
||||
@@ -72,6 +103,9 @@ def q_honesty(low):
|
||||
|
||||
def q_material_reality(low):
|
||||
"""Inviolable 4: financial/material framing must serve wellbeing."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_material_reality(low)
|
||||
finance = ["money", "income", "cost", "price", "afford", "budget",
|
||||
"subscription", "disposable"]
|
||||
hustle = ["hustle", "grind", "rich", "passive income", "millionaire",
|
||||
@@ -89,6 +123,9 @@ def q_material_reality(low):
|
||||
|
||||
def q_root_cause(low):
|
||||
"""Inviolable 5: dissolve problems at the root, no spinning."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_root_cause(low)
|
||||
root = ["root cause", "diagnos", "research properly", "stop guessing",
|
||||
"find the cause", "fix the cause", "why", "underlying"]
|
||||
spin = ["keep trying", "try again", "iterate endlessly", "work around",
|
||||
@@ -102,6 +139,9 @@ def q_root_cause(low):
|
||||
|
||||
def q_respect(low):
|
||||
"""Inviolable 6 + 7: how the user thinks; neurodivergent-aware pedagogy."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_respect(low)
|
||||
respect = ["direct", "three gentle options", "no rigid plans", "action",
|
||||
"adhd", "autism", "audhd", "neurodivergen", "flexible",
|
||||
"night", "not neurotypical", "learner"]
|
||||
@@ -124,6 +164,9 @@ def q_assistant_goal(low):
|
||||
"""
|
||||
# Domain vocabulary is store-derived: distinctive terms from the user's
|
||||
# operating/constitution/domain topics (the user's real working life).
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_assistant_goal(low)
|
||||
assist = _domain_terms()
|
||||
# Universal operating/communication value: how the assistant serves the
|
||||
# user directly — never domain-specific, always relevant.
|
||||
@@ -186,6 +229,9 @@ def _domain_terms():
|
||||
|
||||
def q_durability(low):
|
||||
"""Does this look like a durable fact/pattern vs a one-off?"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_durability(low)
|
||||
durable = ["always", "consistently", "repeatedly", "every session",
|
||||
"tends to", "prefers", "pattern", "across sessions", "recurring"]
|
||||
if any(s in low for s in durable):
|
||||
@@ -207,6 +253,9 @@ def q_epistemology(low):
|
||||
voice is not "epistemically empty"; it lives on a different axis and must
|
||||
not be vetoed for lacking an evidence trace.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_epistemology(low)
|
||||
# Evidence trace: cites a source, evidence, a book, data, or a session.
|
||||
cite = ["evidence", "source", "sources", "book", "paper", "research",
|
||||
"study", "data", "shows", "found", "according to", "per the",
|
||||
@@ -268,6 +317,9 @@ def q_expression(low):
|
||||
opinion-forming style. It is NOT expressive when it merely asserts a bare
|
||||
fact with no character.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.q_expression(low)
|
||||
hits = 0
|
||||
for group in _EXPRESSION_SIGNALS.values():
|
||||
hits += sum(1 for s in group if s in low)
|
||||
@@ -322,6 +374,9 @@ def assess(fact, target=None):
|
||||
Returns (verdict, score, answers) where answers is a list of
|
||||
(qname, score, note) tuples for the journal.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.assess(fact, target)
|
||||
low = (fact or "").lower()
|
||||
# Which axis does this target live on?
|
||||
expressive_targets = {"persona", "delivery", "identity", "voice"}
|
||||
@@ -386,6 +441,9 @@ def _is_bare_factual(low):
|
||||
|
||||
|
||||
def verdict_label(verdict):
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.verdict_label(verdict)
|
||||
return {"REJECT": "REJECT (worthless/harmful)",
|
||||
"DEFER": "DEFER (park in ledger, re-review if recurring)",
|
||||
"ABSORB": "ABSORB (keep in tiers, influences thinking)",
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wr_loader.py — ctypes loader for the Rust worthiness core (dichotomy
|
||||
wave #2479 tranche, contract BUILDER-2026-10-12T02:14:11Z-69cc9a).
|
||||
Mirrors broker_loader.py / pc_loader.py:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* wr_assess({"fact", "target"}, op_host) -> verdict doc
|
||||
* wr_q_pi_safety({"low"}, op_host) -> {"score", "note"}
|
||||
* wr_q_honesty / wr_q_material_reality / wr_q_root_cause / wr_q_respect /
|
||||
wr_q_assistant_goal / wr_q_durability / wr_q_epistemology /
|
||||
wr_q_expression -> {"score", "note"}
|
||||
* wr_verdict_label({"verdict"}) -> label string
|
||||
|
||||
The kernel owns the domain-terms cache (it must, to mirror the oracle's
|
||||
in-place cache mutation law); the one seam it leaves to the caller — the
|
||||
store read behind _domain_terms — crosses back as the `domain_terms` op,
|
||||
served here against the live store with the oracle's verbatim query body
|
||||
(sans cache).
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). NEVER re-export
|
||||
a free symbol here. Override the .so with WR_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "worthiness"
|
||||
LIB_PATH = Path(os.environ.get("WR_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" /
|
||||
"libworthiness.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
# Rust OpFn: extern "C" fn(op: *const c_char, user_data) -> *mut c_char
|
||||
OP_CB = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p)
|
||||
|
||||
SYMBOLS = ("wr_assess", "wr_q_pi_safety", "wr_q_honesty",
|
||||
"wr_q_material_reality", "wr_q_root_cause", "wr_q_respect",
|
||||
"wr_q_assistant_goal", "wr_q_durability", "wr_q_epistemology",
|
||||
"wr_q_expression", "wr_verdict_label")
|
||||
|
||||
|
||||
class WorthinessCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust worthiness core is not built. Callers fall back to Python
|
||||
(strangler law; RUNE_WR_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 WorthinessCoreUnavailable(str(LIB_PATH))
|
||||
lib = ctypes.CDLL(str(LIB_PATH))
|
||||
for sym in SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p, OP_CB, ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
lib._free = _make_free(lib)
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _make_free(lib):
|
||||
"""Free via capability_scoring's cs_free_string (same process)."""
|
||||
def _free(ptr) -> None:
|
||||
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
|
||||
return _free
|
||||
|
||||
|
||||
def _fresh_domain_terms():
|
||||
"""The oracle's _domain_terms body VERBATIM, sans cache — this is what
|
||||
the kernel's `domain_terms` op must serve (the kernel owns the cache
|
||||
and its mutation law; this must stay a raw store read)."""
|
||||
terms = []
|
||||
try:
|
||||
import memory_env
|
||||
db = sqlite3.connect(f"file:{memory_env.store_db()}?mode=ro", uri=True)
|
||||
rows = db.execute(
|
||||
"SELECT text FROM entries WHERE topic IN "
|
||||
"('operating','constitution','project','warm') AND status='active' "
|
||||
"LIMIT 60").fetchall()
|
||||
db.close()
|
||||
stop = {"the", "and", "for", "with", "that", "this", "from", "into",
|
||||
"when", "then", "were", "have", "been", "will", "was", "are",
|
||||
"but", "not", "you", "your", "also", "its", "his", "her",
|
||||
"over", "under", "them", "they", "there", "about", "after",
|
||||
"what", "which", "who", "how", "why", "where", "while", "just",
|
||||
"more", "each", "than", "very", "such", "some", "only"}
|
||||
seen = set()
|
||||
for (text,) in rows:
|
||||
for w in re.findall(r"[a-z]{4,}", (text or "").lower()):
|
||||
if w not in stop and w not in seen:
|
||||
seen.add(w)
|
||||
terms.append(w)
|
||||
if len(terms) >= 25:
|
||||
break
|
||||
except Exception: # noqa: BLE001, S110 — store-unavailable degrades to [] (oracle law)
|
||||
pass
|
||||
return terms
|
||||
|
||||
|
||||
def _op_host(op: dict):
|
||||
name = op.get("op")
|
||||
if name == "domain_terms":
|
||||
return {"terms": _fresh_domain_terms()}
|
||||
raise RuntimeError(f"wr kernel issued unknown op: {name}")
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
buf = ctypes.create_string_buffer(json.dumps(payload).encode())
|
||||
keep = []
|
||||
|
||||
def trampoline(op_ptr, _user_data):
|
||||
try:
|
||||
op = json.loads(ctypes.string_at(op_ptr).decode())
|
||||
result = _op_host(op)
|
||||
except Exception as exc: # noqa: BLE001 — surfaced by the kernel
|
||||
result = {"__host_error__": str(exc)[:500]}
|
||||
data = json.dumps(result).encode()
|
||||
b = ctypes.create_string_buffer(data)
|
||||
keep[:] = [b] # previous buffer already consumed; keep current alive
|
||||
return ctypes.cast(b, ctypes.c_void_p).value
|
||||
|
||||
cb = OP_CB(trampoline)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p), cb, None)
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
lib._free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
# ── oracle-verbatim unwrapping ──────────────────────────────────────────
|
||||
|
||||
def assess(fact, target=None):
|
||||
v = _call("wr_assess", {"fact": fact, "target": target})
|
||||
answers = [(a[0], a[1], a[2]) for a in v["answers"]]
|
||||
vetoed = tuple(v["vetoed"]) if v["vetoed"] is not None else None
|
||||
return (v["verdict"], v["score"], answers, vetoed)
|
||||
|
||||
|
||||
def _q(sym, low):
|
||||
v = _call(sym, {"low": low})
|
||||
return (v["score"], v["note"])
|
||||
|
||||
|
||||
def q_pi_safety(low):
|
||||
return _q("wr_q_pi_safety", low)
|
||||
|
||||
|
||||
def q_honesty(low):
|
||||
return _q("wr_q_honesty", low)
|
||||
|
||||
|
||||
def q_material_reality(low):
|
||||
return _q("wr_q_material_reality", low)
|
||||
|
||||
|
||||
def q_root_cause(low):
|
||||
return _q("wr_q_root_cause", low)
|
||||
|
||||
|
||||
def q_respect(low):
|
||||
return _q("wr_q_respect", low)
|
||||
|
||||
|
||||
def q_assistant_goal(low):
|
||||
return _q("wr_q_assistant_goal", low)
|
||||
|
||||
|
||||
def q_durability(low):
|
||||
return _q("wr_q_durability", low)
|
||||
|
||||
|
||||
def q_epistemology(low):
|
||||
return _q("wr_q_epistemology", low)
|
||||
|
||||
|
||||
def q_expression(low):
|
||||
return _q("wr_q_expression", low)
|
||||
|
||||
|
||||
def verdict_label(verdict):
|
||||
return _call("wr_verdict_label", {"verdict": verdict})
|
||||
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())
|
||||
}
|
||||
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user