Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e6bc4b3c7 | ||
|
|
31e87e612b | ||
|
|
dc20b9b295 | ||
|
|
797bb63f3c | ||
|
|
04583cd26a | ||
|
|
1494c1a5db | ||
|
|
bcc839e78b | ||
|
|
d980d486a7 | ||
|
|
4f3ef0f76b | ||
|
|
3c70f3bd14 | ||
|
|
626359e693 | ||
|
|
845cbfb3d0 | ||
|
|
37feb10d37 | ||
|
|
18052d5e1a | ||
|
|
1af27dc5d4 | ||
|
|
4f3d1f7fc1 | ||
|
|
2d31356417 | ||
|
|
25f7cb6db4 | ||
|
|
9ba51ba5e8 | ||
|
|
78a89eff6d | ||
|
|
cb70224f52 | ||
|
|
02e0c649da | ||
|
|
5844426bdd | ||
|
|
0946141074 | ||
|
|
cc921f72c9 | ||
|
|
7194f69a6d | ||
|
|
1f7a266497 | ||
|
|
6786b4a922 | ||
|
|
2f4c44ee15 | ||
|
|
25e1915977 | ||
|
|
b205ea9452 | ||
|
|
b704f87549 | ||
|
|
53b788f3c6 | ||
|
|
480e25c750 | ||
|
|
5cea323f8b | ||
|
|
92107319d2 | ||
|
|
36ac3de022 | ||
|
|
e2a9bdd878 | ||
|
|
ad4f32aca5 | ||
|
|
fa72a60d95 | ||
|
|
b20a74badb | ||
|
|
f9df935605 | ||
|
|
132cf1e4e1 | ||
|
|
670d55ff62 | ||
|
|
7d049cc42c | ||
|
|
e49eefe926 | ||
|
|
cd4c0beb2f | ||
|
|
9436e6ca26 | ||
|
|
bb7251e1aa | ||
|
|
0cd50f46ed | ||
|
|
2e5ba22249 | ||
|
|
7fbbdeba71 | ||
|
|
f129a6a469 | ||
|
|
72f70aa47a | ||
|
|
61de7c5b74 | ||
|
|
f83396eecd | ||
|
|
8289d896e1 | ||
|
|
5f6c23ff20 | ||
|
|
5723de5b17 | ||
|
|
501ca712b0 | ||
|
|
181954fcf3 | ||
|
|
8e7166b848 | ||
|
|
9561bd02f3 | ||
|
|
9f1c593a8a | ||
|
|
7330a4f628 | ||
|
|
1e80b88a34 | ||
|
|
4e1eee7d9e | ||
|
|
8626d33ffa | ||
|
|
916c6c478a | ||
|
|
7d270f2098 | ||
|
|
f7d51c4834 | ||
|
|
431ca18ea2 | ||
|
|
c9a5f39935 | ||
|
|
374487b24f | ||
|
|
31fb68b58d | ||
|
|
59b98c7576 | ||
|
|
891b5ab1ce | ||
|
|
56c6f02fdb | ||
|
|
115b4cb258 | ||
|
|
53736c20f7 | ||
|
|
54df8e77fb | ||
|
|
aa3e1e3113 | ||
|
|
f11006c457 | ||
|
|
b8b1ba66ad |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.209.0**
|
||||
**Current release: v0.229.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,14 +2,14 @@
|
||||
|
||||
_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: 28%** (686/2480 runtime functions matched)
|
||||
**Function-level completion: 32%** (838/2627 runtime functions matched)
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 15 | 4264 | 80%+ of functions ported |
|
||||
| PARTIAL | 83 | 41656 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 119 | 26856 | no name-preserving implementation |
|
||||
| Total runtime | 217 | 72776 | |
|
||||
| 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 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -17,50 +17,36 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
|---|---|---|---|
|
||||
| rune_tui | 4170 | 159 | TUI-LAST |
|
||||
| curator | 1838 | 43 | GO |
|
||||
| abstraction | 656 | 17 | GO |
|
||||
| canary_audit_ledger | 541 | 2 | TEST-INFRA |
|
||||
| tui_validator | 514 | 13 | TUI-LAST |
|
||||
| construction_pattern | 496 | 12 | RUST |
|
||||
| model_routing | 432 | 13 | GO |
|
||||
| worthiness | 411 | 13 | unclassified |
|
||||
| token_optimizer | 410 | 22 | RUST |
|
||||
| behavior | 408 | 9 | RUST |
|
||||
| capability_ledger | 405 | 18 | RUST |
|
||||
| chat_bubble | 401 | 12 | TUI-LAST |
|
||||
| prompt_sync | 379 | 14 | RUST |
|
||||
| drift-ledger | 355 | 14 | unclassified |
|
||||
| programme_continuation | 349 | 11 | RUST |
|
||||
| pc_loader | 337 | 22 | unclassified |
|
||||
| canary_rune_serve | 329 | 13 | TEST-INFRA |
|
||||
| canary_p1_07 | 324 | 10 | TEST-INFRA |
|
||||
| message_queue | 316 | 13 | GO |
|
||||
| drift_observability | 315 | 8 | RUST |
|
||||
| build_basis | 314 | 12 | RUST |
|
||||
| canary_p0_07 | 310 | 10 | TEST-INFRA |
|
||||
| attention_capsule | 296 | 7 | RUST |
|
||||
| canary_p1_01 | 294 | 12 | TEST-INFRA |
|
||||
| drift_cache | 292 | 15 | RUST |
|
||||
| outcome_ledger | 286 | 7 | RUST |
|
||||
| time_awareness | 286 | 14 | RUST |
|
||||
| canary_p1_06 | 280 | 7 | TEST-INFRA |
|
||||
| auto_context | 273 | 8 | GO |
|
||||
| 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 |
|
||||
| toolchain | 236 | 5 | RUST |
|
||||
| task_gate | 235 | 8 | RUST |
|
||||
| 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 |
|
||||
| evidence_probe | 213 | 9 | GO |
|
||||
| canary_p0_04 | 207 | 8 | TEST-INFRA |
|
||||
| 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 |
|
||||
@@ -73,16 +59,24 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| inference_trace | 164 | 6 | RUST |
|
||||
| ms_loader | 163 | 16 | RUST |
|
||||
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
|
||||
| dc_loader | 158 | 13 | unclassified |
|
||||
| mse_loader | 152 | 12 | unclassified |
|
||||
| bb_loader | 151 | 14 | unclassified |
|
||||
| forge | 146 | 11 | GO |
|
||||
| rune_graphic | 145 | 5 | TUI-LAST |
|
||||
| spine_loader | 144 | 13 | RUST |
|
||||
| ac_loader | 140 | 11 | unclassified |
|
||||
| project_registry | 139 | 11 | RUST |
|
||||
| canary_rune_entry | 137 | 3 | TEST-INFRA |
|
||||
| do_loader | 137 | 8 | unclassified |
|
||||
| ps_loader | 136 | 13 | unclassified |
|
||||
| verification_layer | 136 | 11 | RUST |
|
||||
| tui_backlog | 125 | 5 | TUI-LAST |
|
||||
| state_checkpoint | 123 | 8 | unclassified |
|
||||
| cp_loader | 122 | 9 | unclassified |
|
||||
| drift_resume | 118 | 3 | RUST |
|
||||
| tl_loader | 118 | 9 | unclassified |
|
||||
| bh_loader | 117 | 9 | unclassified |
|
||||
| canary_conversation_state | 115 | 2 | TEST-INFRA |
|
||||
| contracts_loader | 112 | 8 | unclassified |
|
||||
| canary_query_memory | 111 | 2 | TEST-INFRA |
|
||||
@@ -100,9 +94,9 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| broker_loader | 86 | 5 | unclassified |
|
||||
| canary_schema_migration | 86 | 1 | TEST-INFRA |
|
||||
| ss_loader | 86 | 7 | unclassified |
|
||||
| to_loader | 85 | 4 | unclassified |
|
||||
| mems_loader | 83 | 5 | RUST |
|
||||
| cm_loader | 82 | 4 | unclassified |
|
||||
| cp_loader | 77 | 4 | unclassified |
|
||||
| dl_loader | 77 | 5 | unclassified |
|
||||
| lc_loader | 75 | 5 | unclassified |
|
||||
| cd_loader | 72 | 4 | unclassified |
|
||||
@@ -155,7 +149,7 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| growth_delta | 449 | 5/20 | _ctx_budget, _store_db, _llm, _parse_json, _load_deltas, _save_deltas |
|
||||
| provider_budget | 357 | 5/20 | get_tracker, track_usage, check_provider, get_best_provider, failover, get_status |
|
||||
| safety_override | 574 | 7/22 | _keyword_stopwords, _cc_probe, cc_loader_pin, self_test, _py_matches, escalation_rate |
|
||||
| spine | 965 | 20/35 | _py_sorted, _module_imports, _py_declared_surfaces, _py_project_meta_surfaces, _gather_meta_inputs, _rust_core |
|
||||
| spine | 975 | 20/35 | _py_sorted, _module_imports, _py_declared_surfaces, _py_project_meta_surfaces, _gather_meta_inputs, _rust_core |
|
||||
| tool_executor | 1836 | 32/47 | _execute_tool_now, serialize_prepared_tool, deserialize_prepared_tool, confirm_write, _resolve_execution_cwd, _run_command |
|
||||
| cron_engine | 440 | 13/27 | get_cron_engine, run_task, start_engine, stop_engine, to_dict, fire_on_task_complete |
|
||||
| night_mode | 421 | 5/19 | _ctx_budget, get_night_mode, start_night_mode, stop_night_mode, get_status, get_report |
|
||||
@@ -166,6 +160,7 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| identity_anchors | 542 | 5/16 | show_anchors, show_rules, show_pronouns, _announcement_patterns, _pronoun_patterns, _hedge_patterns |
|
||||
| parallel_processor | 194 | 3/14 | _shutdown_processor, get_processor, run_parallel_with_args, get_status, test_parallel, run_parallel_with_args |
|
||||
| structural_model | 388 | 9/20 | workspace_record, workspace_advance, _init_project, save, reload, add |
|
||||
| token_optimizer | 514 | 16/27 | get_optimizer, _tc_core, _to_core, _py_estimate_tokens, _py_compress_context, to_dict |
|
||||
| capability_broker | 798 | 10/20 | _native_tool_schemas, load_registry, _cards_are_stale, load_cards, generate_card, build_cards |
|
||||
| capability_fit | 414 | 3/13 | record_semantic_fit, _provides_contracts, _consumers, discover_candidates, _cs_core, decide |
|
||||
| claim_audit | 484 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
|
||||
@@ -176,7 +171,9 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -198,8 +195,11 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| tiered_executor | 291 | 3/8 | _ctx_budget, test_executor, to_dict, to_dict, _execute_model |
|
||||
| adaptive_router | 625 | 15/19 | _floor_applies_from_flags, _floor_reason, _record_transition_with, _detect_external_refs |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| salience_gate | 276 | 1/5 | _sg_core, test_salience, handle_hook, to_dict |
|
||||
| self_improve | 246 | 1/5 | _now, suggest_improvements, handle_hook, main |
|
||||
| stage_guards | 303 | 7/11 | _py_liveness_canary, _py_reconcile, to_dict, chk |
|
||||
@@ -227,13 +227,23 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| abstraction | 656 | 17/17 | GO |
|
||||
| system_registry | 603 | 14/16 | RUST |
|
||||
| construction_intent | 580 | 13/16 | RUST |
|
||||
| capability_ledger | 571 | 18/19 | RUST |
|
||||
| event_wait | 518 | 14/14 | GO |
|
||||
| tool_protocol | 461 | 22/27 | GO |
|
||||
| auto_tasks | 460 | 14/14 | GO |
|
||||
| programme_continuation | 458 | 11/12 | RUST |
|
||||
| goal | 453 | 20/20 | GO |
|
||||
| release_notes | 446 | 16/16 | GO |
|
||||
| model_routing | 432 | 13/13 | GO |
|
||||
| build_basis | 375 | 11/13 | RUST |
|
||||
| attention_capsule | 365 | 6/7 | RUST |
|
||||
| message_queue | 316 | 13/13 | GO |
|
||||
| toolchain | 295 | 5/6 | RUST |
|
||||
| auto_context | 273 | 8/8 | GO |
|
||||
| evidence_probe | 213 | 9/9 | GO |
|
||||
| recall_uplift | 141 | 1/1 | RUST |
|
||||
| rune_spinner | 130 | 4/5 | RUST |
|
||||
| wait_guard | 116 | 1/1 | RUST |
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.209.0"
|
||||
version = "0.229.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: behavior delegating facade (Rust core) vs the Python-pin
|
||||
oracle path, over the SAME case matrix the truth generator froze.
|
||||
Run: python3 scripts/check_behavior_parity.py -> x/y lines, exit 1 on any
|
||||
divergence.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
_tmp = tempfile.mkdtemp(prefix="bh-parity-")
|
||||
os.environ["RUNE_HOME"] = _tmp
|
||||
os.environ["RUNE_MEMORY_DIR"] = os.path.join(_tmp, "memory")
|
||||
|
||||
from rune import behavior as bh # noqa: E402
|
||||
from rune import bh_loader # noqa: E402
|
||||
|
||||
if not bh_loader.available():
|
||||
print("0/0 — rust behavior core not built")
|
||||
sys.exit(0)
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
DIVERGENCES = []
|
||||
|
||||
_CLOCK_KEYS = {"created_at", "last_reinforced", "last_activated"}
|
||||
|
||||
|
||||
def strip_clocks(obj):
|
||||
"""Timestamps are pinned by the shim (`now`) — wall-clock values
|
||||
differ per run by design; compare everything else."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: strip_clocks(v) for k, v in obj.items() if k not in _CLOCK_KEYS}
|
||||
if isinstance(obj, list):
|
||||
return [strip_clocks(x) for x in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def check(name, rust, py):
|
||||
global PASS, FAIL
|
||||
if strip_clocks(rust) == strip_clocks(py):
|
||||
PASS += 1
|
||||
else:
|
||||
FAIL += 1
|
||||
DIVERGENCES.append((name, rust, py))
|
||||
|
||||
|
||||
def row(table, rid):
|
||||
db = bh.connect()
|
||||
try:
|
||||
r = db.execute(f"SELECT * FROM {table} WHERE id=?", (rid,)).fetchone()
|
||||
return dict(r) if r else None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def both(name, fn):
|
||||
"""Drive fn() twice: pinned-python first (fresh db), rust second
|
||||
(fresh db). Databases are reset between runs so ids align."""
|
||||
os.environ["RUNE_BEHAVIOR_CORE"] = "python"
|
||||
bh.DB_PATH = os.path.join(os.environ["RUNE_MEMORY_DIR"], "behavior.db")
|
||||
if os.path.exists(bh.DB_PATH):
|
||||
os.remove(bh.DB_PATH)
|
||||
py = fn()
|
||||
os.environ["RUNE_BEHAVIOR_CORE"] = "auto"
|
||||
if os.path.exists(bh.DB_PATH):
|
||||
os.remove(bh.DB_PATH)
|
||||
rust = fn()
|
||||
os.environ["RUNE_BEHAVIOR_CORE"] = "python"
|
||||
check(name, rust, py)
|
||||
|
||||
|
||||
# ── add shapes ────────────────────────────────────────────────────────
|
||||
def add_shapes():
|
||||
out = [bh.add_correction("say X", "say Y"),
|
||||
bh.add_correction("orig", "corr", scope_tags={"k": "v"}, domain="shell", pattern="shell"),
|
||||
bh.add_habit("trig", "act"),
|
||||
bh.add_habit("trig2", "act2", name="n", context_tags=["a"], min_activations=5)]
|
||||
return json.loads(json.dumps(out))
|
||||
|
||||
|
||||
both("add-shapes", add_shapes)
|
||||
|
||||
# ── seed the shared match fixture in BOTH modes ───────────────────────
|
||||
def seed_match():
|
||||
bh.add_correction("o1", "scoped corr", scope_tags={}, domain="shell", pattern="deploy")
|
||||
bh.add_correction("o2", "domain corr", scope_tags={}, domain="editor")
|
||||
bh.add_correction("o3", "plain corr")
|
||||
bh.add_correction("o4", "bad regex corr", pattern="([unclosed")
|
||||
low = bh.add_correction("o5", "low conf corr")["added"]
|
||||
bh.add_habit("deploy", "run deploy script")
|
||||
bh.add_habit("test (unit|e2e)", "run tests")
|
||||
bh.add_habit("([broken", "bad regex habit")
|
||||
hslow = bh.add_habit("zzz-no-match-ever", "quiet")["added"]
|
||||
db = bh.connect()
|
||||
db.execute("UPDATE corrections SET confidence=0.2 WHERE id=?", (low,))
|
||||
db.execute("UPDATE habits SET confidence=0.2 WHERE id=?", (hslow,))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def match_shapes():
|
||||
seed_match()
|
||||
cases = []
|
||||
for ctx, mt in [("deploy to shell now", None), ("deploy the editor", None),
|
||||
("unrelated chatter here", None), ("deploy to shell now", "habit"),
|
||||
("deploy to shell now", "correction"), ("run the unit tests", None),
|
||||
("zzz-no-match-ever quiet", None), ("([unclosed anything", None)]:
|
||||
got = bh.match_context(ctx, match_type=mt)
|
||||
cases.append({
|
||||
"corrections": sorted(c["corrected_behavior"] for c in got["corrections"]),
|
||||
"habits": sorted(h["action_template"] for h in got["habits"]),
|
||||
"full": json.loads(json.dumps(got)),
|
||||
})
|
||||
return cases
|
||||
|
||||
|
||||
both("match-matrix", match_shapes)
|
||||
|
||||
# ── reinforce matrix ──────────────────────────────────────────────────
|
||||
def reinforce_shapes():
|
||||
seed_match()
|
||||
out = []
|
||||
for name, item_type, outcome, seed_fn, pre, n in [
|
||||
("success-promotes-at-3", "habit", "success", lambda: bh.add_habit("t1", "a1"), {}, 3),
|
||||
("failure-counts", "habit", "failure", lambda: bh.add_habit("t2", "a2"), {}, 2),
|
||||
("neutral-bumps-activation-only", "habit", "neutral", lambda: bh.add_habit("t3", "a3"), {}, 1),
|
||||
("already-active-stays", "habit", "success", lambda: bh.add_habit("t4", "a4", min_activations=1), {}, 2),
|
||||
("min5-gate", "habit", "success", lambda: bh.add_habit("t5", "a5", min_activations=5), {}, 4),
|
||||
("success-plus-point-one", "correction", "success", lambda: bh.add_correction("x1", "y1"), {}, 1),
|
||||
("failure-archives", "correction", "failure", lambda: bh.add_correction("x2", "y2"), {}, 1),
|
||||
("double-failure-floor", "correction", "failure", lambda: bh.add_correction("x3", "y3"), {}, 3),
|
||||
("ceiling-at-one", "correction", "success", lambda: bh.add_correction("x4", "y4"), {"confidence": 0.95}, 1),
|
||||
("neutral-noop", "correction", "neutral", lambda: bh.add_correction("x5", "y5"), {}, 1),
|
||||
]:
|
||||
rid = seed_fn()["added"]
|
||||
if pre:
|
||||
db = bh.connect()
|
||||
table = "habits" if item_type == "habit" else "corrections"
|
||||
for k, v in pre.items():
|
||||
db.execute(f"UPDATE {table} SET {k}=? WHERE id=?", (v, rid))
|
||||
db.commit()
|
||||
db.close()
|
||||
results = [bh.reinforce(rid, outcome, item_type=item_type) for _ in range(n)]
|
||||
out.append({"name": name, "results": json.loads(json.dumps(results)),
|
||||
"final": json.loads(json.dumps(row("habits" if item_type == "habit" else "corrections", rid)))})
|
||||
out.append({"name": "not-found-habit",
|
||||
"results": [bh.reinforce(99991, "success", item_type="habit")], "final": None})
|
||||
out.append({"name": "not-found-correction",
|
||||
"results": [bh.reinforce(99992, "success", item_type="correction")], "final": None})
|
||||
return out
|
||||
|
||||
|
||||
both("reinforce-matrix", reinforce_shapes)
|
||||
|
||||
# ── list + stats ──────────────────────────────────────────────────────
|
||||
def list_stats_shapes():
|
||||
seed_match()
|
||||
out = {}
|
||||
check("list-keys", sorted(bh.list_items().keys()), ["corrections", "habits"])
|
||||
check("limit-1", len(bh.list_items(limit=1).get("corrections", [])), 1)
|
||||
check("type-correction-keys", sorted(bh.list_items(item_type="correction").keys()), ["corrections"])
|
||||
check("type-habit-keys", sorted(bh.list_items(item_type="habit").keys()), ["habits"])
|
||||
check("type-all-keys", sorted(bh.list_items(item_type="all").keys()), ["corrections", "habits"])
|
||||
out["stats"] = bh.behavior_stats()
|
||||
return json.loads(json.dumps(out))
|
||||
|
||||
|
||||
both("list-stats", list_stats_shapes)
|
||||
|
||||
print(f"{PASS}/{PASS + FAIL} live parity (rust facade vs python pin)")
|
||||
for name, rust, py in DIVERGENCES:
|
||||
print(f"DIVERGENCE {name}:")
|
||||
print(f" rust: {json.dumps(rust, sort_keys=True)[:400]}")
|
||||
print(f" py: {json.dumps(py, sort_keys=True)[:400]}")
|
||||
sys.exit(1 if FAIL else 0)
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: Rust build_basis kernel vs the Python oracle (dichotomy
|
||||
wave #2479). Replays the frozen truth corpus PLUS seeded fuzz over every
|
||||
public function and branch. Timestamps the kernel generates are masked
|
||||
on both sides (wall clock, not logic).
|
||||
|
||||
The facade reads RUNE_BB_CORE per call, so one process can flip between
|
||||
the pinned oracle (RUNE_BB_CORE=python) and the kernel (unset).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
MASKED = ("decided_at", "created_at", "revised_at", "plan_generated_at")
|
||||
|
||||
|
||||
def mask(doc):
|
||||
if isinstance(doc, dict):
|
||||
return {k: mask(v) for k, v in doc.items() if k not in MASKED}
|
||||
if isinstance(doc, list):
|
||||
return [mask(v) for v in doc]
|
||||
return doc
|
||||
|
||||
|
||||
def call(fn, *args, **kwargs):
|
||||
"""Call through the facade under the given core pin."""
|
||||
return getattr(bb, fn)(*args, **kwargs)
|
||||
|
||||
|
||||
def call_core(core, fn, *args, **kwargs):
|
||||
old = os.environ.get("RUNE_BB_CORE")
|
||||
os.environ["RUNE_BB_CORE"] = core
|
||||
try:
|
||||
return call(fn, *args, **kwargs)
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_BB_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_BB_CORE"] = old
|
||||
|
||||
|
||||
def mutator_case(fn, basis_in, *args, **kwargs):
|
||||
"""Run a mutating function on both cores over deep copies; compare
|
||||
(return value, post-mutation basis) with time fields masked."""
|
||||
py = call_core("python", fn, copy.deepcopy(basis_in), *args, **kwargs)
|
||||
rs = call_core("rust", fn, copy.deepcopy(basis_in), *args, **kwargs)
|
||||
ok = (mask(py) == mask(rs)
|
||||
and mask(basis_in_py := None or basis_in) is not None)
|
||||
return ok, py, rs
|
||||
|
||||
|
||||
# ── fuzz pools ─────────────────────────────────────────────────────────
|
||||
ROUTES = ["LOCAL_FIX", "STANDARD_CHANGE", "RESEARCHED_CHANGE", "WEIRD",
|
||||
"", "RESEARCHED_CHANGE ", "local_fix", "SPIKE"]
|
||||
TIERS = ["NONE", "INWARD", "OUTWARD", "FULL", "WEIRD", None]
|
||||
JUSTS = ["", " ", "tiny fix", "OUTWARD: precedent needed",
|
||||
"FULL: x:y", " INWARD ", "resolved already",
|
||||
"не-mátter 🎉", "prior-art question answered elsewhere",
|
||||
"NONE: skip", "discovery resolved it", "unnecessary"]
|
||||
WORDS = ["existing", "prior art", "framework", "typo", "schema",
|
||||
"architecture", "watchdog", "delete", "", "u" * 90, "café ☕",
|
||||
"tool", "standard", "breaking", "deploy"]
|
||||
DISPOSITIONS = ["ADOPT", "ADAPT", "COMBINE", "REJECT", "INNOVATE", "MAYBE", "", None]
|
||||
|
||||
|
||||
def rand_basis(rng):
|
||||
b = bb.new_basis(question=rng.choice(WORDS),
|
||||
depth_decision=bb.research_depth(rng.choice(ROUTES),
|
||||
justification=rng.choice(JUSTS)),
|
||||
issue_ref=rng.choice(["research/issue-1", "", "gitea/2479"]))
|
||||
b["created_at"] = "2026-10-11T00:00:00Z"
|
||||
return b
|
||||
|
||||
|
||||
def fuzz_cases(rng):
|
||||
out = []
|
||||
for _ in range(120):
|
||||
out.append(("research_depth",
|
||||
(rng.choice(ROUTES), rng.choice(["", "rune-internal", "cli"]),
|
||||
rng.choice(JUSTS)), {}))
|
||||
for _ in range(60):
|
||||
d = bb.research_depth(rng.choice(ROUTES), justification=rng.choice(JUSTS))
|
||||
d["decided_at"] = "FIXED"
|
||||
out.append(("new_basis", (rng.choice(WORDS), d,
|
||||
rng.choice(["research/x", ""])), {}))
|
||||
for _ in range(120):
|
||||
disp = [{"item": rng.choice(WORDS), "disposition": rng.choice(DISPOSITIONS),
|
||||
"why": rng.choice(JUSTS), "ref": rng.choice(["r1", ""]) }
|
||||
for _ in range(rng.randrange(0, 4))]
|
||||
out.append(("set_dispositions", (None,), {"basis": None, "dispositions": disp}))
|
||||
for _ in range(120):
|
||||
b = rand_basis(rng)
|
||||
if rng.random() < 0.5:
|
||||
b["schema_version"] = rng.choice([1, 2, None, True])
|
||||
if rng.random() < 0.4:
|
||||
b["depth"] = rng.choice([{"depth": rng.choice(TIERS), "why": rng.choice(["", "w"])},
|
||||
{}, None])
|
||||
if rng.random() < 0.3:
|
||||
b["dispositions"] = [{"item": rng.choice(WORDS),
|
||||
"disposition": rng.choice(DISPOSITIONS)}]
|
||||
out.append(("validate_basis", (), {"basis": b}))
|
||||
for _ in range(60):
|
||||
out.append(("attach_evidence_ref", (),
|
||||
{"basis": rng.choice([rand_basis(rng),
|
||||
{"created_at": None},
|
||||
{"issue_ref": "", "created_at": "T"},
|
||||
{}])}))
|
||||
for _ in range(60):
|
||||
b = rand_basis(rng)
|
||||
if rng.random() < 0.4:
|
||||
b["implications"] = rng.choice([None, {}, {"architecture": ["a"], "x": "yy"}])
|
||||
b["uncertainty"] = rng.choice([[], ["u1"], ["u1", "u2"]])
|
||||
b["risks"] = rng.choice([[], ["r1"]])
|
||||
out.append(("summary", (), {"basis": b}))
|
||||
for _ in range(120):
|
||||
b = rand_basis(rng)
|
||||
out.append(("revise_depth", (),
|
||||
{"basis": b, "new_depth": rng.choice(TIERS),
|
||||
"reason": rng.choice(JUSTS + ["skip it", "x" * 250, "r"])}))
|
||||
for _ in range(80):
|
||||
b = rand_basis(rng)
|
||||
if rng.random() < 0.3:
|
||||
b["question"] = "héllo 🎉 \"q\" \\n\x01"
|
||||
out.append(("to_json", (), {"basis": b}))
|
||||
for _ in range(60):
|
||||
out.append(("from_json", (rng.choice([
|
||||
json.dumps({"a": [1, 2.5, True, None], "é": "ü"}),
|
||||
"[1,2,3]", "{}", "null", "\"str\"",
|
||||
"{bad", "{'single': 1}", ""]),), {}))
|
||||
for _ in range(150):
|
||||
kw = dict(intent=rng.choice(WORDS + ["migrate the schema", "deploy it"]),
|
||||
target=rng.choice(["", "wr-bridge"]),
|
||||
current_reality=rng.choice(["", "c" * 250, "réalité"]),
|
||||
unknowns=[rng.choice(WORDS) for _ in range(rng.randrange(0, 3))],
|
||||
decisions=[rng.choice(WORDS) for _ in range(rng.randrange(0, 2))],
|
||||
risks=[rng.choice(WORDS) for _ in range(rng.randrange(0, 2))],
|
||||
depth=rng.choice([None] + TIERS[:4]))
|
||||
out.append(("generate_research_plan", (), kw))
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global bb
|
||||
from rune import build_basis as bb_mod
|
||||
from rune import bb_loader
|
||||
bb = bb_mod
|
||||
if not bb_loader.available():
|
||||
print("Rust core not built — run cargo build --release in src/rust/build_basis")
|
||||
return 2
|
||||
|
||||
total = passed = 0
|
||||
failures: list[str] = []
|
||||
|
||||
def check(label, ok, detail=""):
|
||||
nonlocal total, passed
|
||||
total += 1
|
||||
if ok:
|
||||
passed += 1
|
||||
else:
|
||||
failures.append(f"{label}: {detail}")
|
||||
|
||||
# 1. frozen truth corpus, replayed live
|
||||
truth = json.loads((Path(__file__).resolve().parents[1] / "src" / "rust" /
|
||||
"build_basis" / "src" / "frozen_bb_truth.json").read_text())
|
||||
for fn, cases in truth["cases"].items():
|
||||
for case in cases:
|
||||
name = f"truth:{fn}:{case['name']}"
|
||||
inp = case["input"]
|
||||
if "error" in case and fn == "revise_depth":
|
||||
# error path: oracle raises; kernel must refuse identically
|
||||
try:
|
||||
py_err = None
|
||||
call_core("python", fn, copy.deepcopy(inp["basis"]),
|
||||
new_depth=inp["new_depth"], reason=inp["reason"])
|
||||
except ValueError as e:
|
||||
py_err = str(e)
|
||||
try:
|
||||
bb_loader.revise_depth(copy.deepcopy(inp["basis"]),
|
||||
inp["new_depth"], inp["reason"])
|
||||
check(name, False, "kernel accepted a refused revision")
|
||||
except RuntimeError as e:
|
||||
check(name, str(e) == py_err,
|
||||
f"kernel err {e!r} != oracle {py_err!r}")
|
||||
continue
|
||||
if "error_kind" in case: # from_json invalid → both must refuse
|
||||
try:
|
||||
call_core("python", fn, **inp)
|
||||
py_ok, rs_ok = True, True
|
||||
except ValueError:
|
||||
py_ok = False
|
||||
try:
|
||||
call_core("rust", fn, **inp)
|
||||
rs_ok = True
|
||||
except ValueError:
|
||||
rs_ok = False
|
||||
check(name, py_ok == rs_ok == False, f"py={py_ok} rs={rs_ok}")
|
||||
continue
|
||||
if fn in ("set_dispositions", "revise_depth"):
|
||||
b1, b2 = copy.deepcopy(inp["basis"]), copy.deepcopy(inp["basis"])
|
||||
kw = dict(inp)
|
||||
kw["basis"] = b1
|
||||
py = call_core("python", fn, **kw)
|
||||
kw["basis"] = b2
|
||||
rs = call_core("rust", fn, **kw)
|
||||
ok = mask(py) == mask(rs) and mask(b1) == mask(b2)
|
||||
check(name, ok, f"py={py!r} rs={rs!r}")
|
||||
else:
|
||||
if fn == "new_basis":
|
||||
py = call_core("python", fn, question=inp["question"],
|
||||
depth_decision=inp["depth_decision"],
|
||||
issue_ref=inp.get("issue_ref", ""))
|
||||
rs = call_core("rust", fn, question=inp["question"],
|
||||
depth_decision=inp["depth_decision"],
|
||||
issue_ref=inp.get("issue_ref", ""))
|
||||
else:
|
||||
py = call_core("python", fn, **inp)
|
||||
rs = call_core("rust", fn, **inp)
|
||||
check(name, mask(py) == mask(rs), f"py={py!r} rs={rs!r}")
|
||||
|
||||
# 2. seeded fuzz
|
||||
rng = random.Random(2479)
|
||||
for fn, args, kwargs in fuzz_cases(rng):
|
||||
name = f"fuzz:{fn}"
|
||||
if fn in ("set_dispositions", "revise_depth"):
|
||||
b = rand_basis(rng)
|
||||
b1, b2 = copy.deepcopy(b), copy.deepcopy(b)
|
||||
kw_clean = {k: v for k, v in kwargs.items() if k != "basis"}
|
||||
try:
|
||||
py = call_core("python", fn, b1, **kw_clean)
|
||||
except ValueError as e:
|
||||
py = f"ValueError: {e}"
|
||||
try:
|
||||
rs = call_core("rust", fn, b2, **kw_clean)
|
||||
except ValueError as e:
|
||||
rs = f"ValueError: {e}"
|
||||
check(name, mask(py) == mask(rs) and mask(b1) == mask(b2),
|
||||
f"kwargs={kwargs} py={py!r} rs={rs!r}")
|
||||
elif fn == "new_basis":
|
||||
py = call_core("python", fn, question=args[0],
|
||||
depth_decision=args[1], issue_ref=args[2])
|
||||
rs = call_core("rust", fn, question=args[0],
|
||||
depth_decision=args[1], issue_ref=args[2])
|
||||
check(name, mask(py) == mask(rs), f"args={args}")
|
||||
elif fn in ("validate_basis", "attach_evidence_ref", "summary",
|
||||
"to_json", "from_json", "generate_research_plan"):
|
||||
try:
|
||||
py = call_core("python", fn, *args, **kwargs)
|
||||
except ValueError as e:
|
||||
py = f"ValueError: {e}"
|
||||
try:
|
||||
rs = call_core("rust", fn, *args, **kwargs)
|
||||
except ValueError as e:
|
||||
rs = f"ValueError: {e}"
|
||||
check(name, mask(py) == mask(rs), f"args={args} kwargs={kwargs}")
|
||||
elif fn == "research_depth":
|
||||
py = call_core("python", fn, args[0], mode=args[1],
|
||||
justification=args[2])
|
||||
rs = call_core("rust", fn, args[0], mode=args[1],
|
||||
justification=args[2])
|
||||
check(name, mask(py) == mask(rs), f"args={args}")
|
||||
|
||||
print(f"parity {passed}/{total}")
|
||||
if failures:
|
||||
for f in failures[:12]:
|
||||
print("FAIL", f[:400])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: Rust task_gate kernel vs the Python oracle (dichotomy
|
||||
wave #2479). Replays the frozen truth corpus PLUS seeded fuzz over every
|
||||
public function and branch. Timestamps are frozen (no-clock law — the
|
||||
kernel takes `now` as an argument), so nothing is masked.
|
||||
|
||||
The facade reads RUNE_TG_CORE per call, so one process can flip between
|
||||
the pinned oracle (RUNE_TG_CORE=python) and the kernel (unset).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
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"))
|
||||
|
||||
import task_gate as tg # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-11T23:00:00.123456+00:00"
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
"""No-clock law: the facade's `now` stamps are wall clock, not logic —
|
||||
freeze them so the two core runs stamp identically."""
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return datetime(2026, 10, 11, 23, 0, 0, 123456, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
tg.datetime = FrozenDatetime
|
||||
|
||||
TRUTH_PATH = Path(__file__).resolve().parents[1] / (
|
||||
"src/rust/task_gate/src/frozen_tg_truth.json")
|
||||
|
||||
|
||||
def call_core(core, fn, *args, **kwargs):
|
||||
old = os.environ.get("RUNE_TG_CORE")
|
||||
os.environ["RUNE_TG_CORE"] = core
|
||||
try:
|
||||
return getattr(tg, fn)(*args, **kwargs)
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_TG_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_TG_CORE"] = old
|
||||
|
||||
|
||||
def fresh_db():
|
||||
tmp = tempfile.mkdtemp(prefix="tg_parity_")
|
||||
tg.DB_PATH = os.path.join(tmp, "task_gate.db")
|
||||
db = tg.connect()
|
||||
db.close()
|
||||
|
||||
|
||||
def read_rows():
|
||||
db = sqlite3.connect(tg.DB_PATH)
|
||||
db.row_factory = sqlite3.Row
|
||||
rows = [dict(r) for r in db.execute(
|
||||
"SELECT * FROM instructions ORDER BY id").fetchall()]
|
||||
db.close()
|
||||
return rows
|
||||
|
||||
|
||||
def seed_rows(rows):
|
||||
fresh_db()
|
||||
db = tg.connect()
|
||||
for i, r in enumerate(rows, start=1):
|
||||
db.execute(
|
||||
"INSERT INTO instructions (id, instruction, pattern, count,"
|
||||
" threshold, status, promoted_at, created_at, last_seen)"
|
||||
" VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
r.get("id", i), r.get("instruction", "seed %d" % i),
|
||||
r.get("pattern", r"\bseed\b"), r.get("count", 1),
|
||||
r.get("threshold", 3), r.get("status", "observing"),
|
||||
r.get("promoted_at"), r.get("created_at", FROZEN_NOW),
|
||||
r.get("last_seen", FROZEN_NOW),
|
||||
),
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def scenario_record(rows_before, text):
|
||||
"""Run record under both cores on identical pre-state; compare
|
||||
(return, rows_after)."""
|
||||
seed_rows(rows_before)
|
||||
py_ret = call_core("python", "record_instruction", text)
|
||||
py_rows = read_rows()
|
||||
seed_rows(rows_before)
|
||||
rs_ret = call_core("auto", "record_instruction", text)
|
||||
rs_rows = read_rows()
|
||||
ok = (json.dumps(py_ret, sort_keys=True)
|
||||
== json.dumps(rs_ret, sort_keys=True)
|
||||
and json.dumps(py_rows, sort_keys=True)
|
||||
== json.dumps(rs_rows, sort_keys=True))
|
||||
return ok, py_ret, rs_ret, py_rows, rs_rows
|
||||
|
||||
|
||||
def scenario_ctx(rows, context):
|
||||
seed_rows(rows)
|
||||
py = call_core("python", "check_context", context)
|
||||
rs = call_core("auto", "check_context", context)
|
||||
return (json.dumps(py, sort_keys=True)
|
||||
== json.dumps(rs, sort_keys=True)), py, rs
|
||||
|
||||
|
||||
def scenario_list(rows):
|
||||
seed_rows(rows)
|
||||
py = call_core("python", "list_gates")
|
||||
rs = call_core("auto", "list_gates")
|
||||
return (json.dumps(py, sort_keys=True)
|
||||
== json.dumps(rs, sort_keys=True)), py, rs
|
||||
|
||||
|
||||
def scenario_promote(rows, gate_id):
|
||||
seed_rows(rows)
|
||||
py = call_core("python", "promote_gate", gate_id)
|
||||
py_rows = read_rows()
|
||||
seed_rows(rows)
|
||||
rs = call_core("auto", "promote_gate", gate_id)
|
||||
rs_rows = read_rows()
|
||||
return (json.dumps(py, sort_keys=True)
|
||||
== json.dumps(rs, sort_keys=True)
|
||||
and json.dumps(py_rows, sort_keys=True)
|
||||
== json.dumps(rs_rows, sort_keys=True)), py, rs, py_rows, rs_rows
|
||||
|
||||
|
||||
def scenario_hook(event):
|
||||
py = call_core("python", "handle_hook", event, {"ignored": True})
|
||||
rs = call_core("auto", "handle_hook", event, {"ignored": True})
|
||||
return (json.dumps(py, sort_keys=True)
|
||||
== json.dumps(rs, sort_keys=True)), py, rs
|
||||
|
||||
|
||||
# ── fuzz pools ─────────────────────────────────────────────────────────
|
||||
WORDS = ["always", "check", "the", "benchmark", "before", "building",
|
||||
"ship", "it", "do", "stop", "now", "café", "über", "v1_2",
|
||||
"test_it", "fix", "bug", "release", "deploy", "kraken", "ab",
|
||||
"cd", "research", "first", "wash", "spin", "gate", "task"]
|
||||
PATTERNS = [r"\bcheck\b", r"\bship\b", r"\bbenchmark\b", r"do\ it",
|
||||
r"\balways\b|\bcheck\b", r"\bcafé\b", r"\bresearch\b",
|
||||
r"\bgate\b|\btask\b", r"\bfirst\b"]
|
||||
STATUSES = ["observing", "active", "retired"]
|
||||
|
||||
|
||||
def rand_text(rng):
|
||||
n = rng.randint(1, 8)
|
||||
sep = rng.choice([" ", " ", " ", " ", "! ", " ("])
|
||||
return sep.join(rng.choice(WORDS) for _ in range(n)) + rng.choice(
|
||||
["", "!", "?", " (now)"])
|
||||
|
||||
|
||||
def rand_rows(rng, max_rows=4):
|
||||
rows = []
|
||||
for _ in range(rng.randint(0, max_rows)):
|
||||
rows.append({
|
||||
"pattern": rng.choice(PATTERNS),
|
||||
"count": rng.randint(1, 5),
|
||||
"threshold": rng.randint(2, 4),
|
||||
"status": rng.choice(STATUSES),
|
||||
"instruction": rand_text(rng),
|
||||
"promoted_at": FROZEN_NOW if rng.random() < 0.3 else None,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def fuzz_cases(rng):
|
||||
cases = []
|
||||
for _ in range(300):
|
||||
cases.append(("record", rand_rows(rng), rand_text(rng)))
|
||||
for _ in range(250):
|
||||
cases.append(("ctx", rand_rows(rng), rand_text(rng)))
|
||||
for _ in range(200):
|
||||
cases.append(("list", rand_rows(rng, 5), None))
|
||||
for _ in range(150):
|
||||
cases.append(("promote", rand_rows(rng), rng.randint(1, 5)))
|
||||
for _ in range(50):
|
||||
cases.append(("hook", None, rand_text(rng)))
|
||||
return cases
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures = []
|
||||
|
||||
def check(label, ok, detail=""):
|
||||
if not ok:
|
||||
failures.append((label, detail))
|
||||
print(f"FAIL {label}\n{detail}")
|
||||
|
||||
# 1. frozen truth corpus (facade level, both cores already proven by
|
||||
# gen_task_gate_truth.py --replay; re-run kernel level here live)
|
||||
truth = json.loads(TRUTH_PATH.read_text())["cases"]
|
||||
n_truth = 0
|
||||
for group, cases in truth.items():
|
||||
for case in cases:
|
||||
if group == "record_instruction":
|
||||
inp = case["input"]
|
||||
ok, p, r, pr, rr = scenario_record(
|
||||
inp["instructions"], inp["text"])
|
||||
elif group == "check_context":
|
||||
inp = case["input"]
|
||||
ok, p, r = scenario_ctx(inp["instructions"], inp["context"])
|
||||
elif group == "list_gates":
|
||||
ok, p, r = scenario_list(
|
||||
case["rows_after"] and _rows_before(case))
|
||||
elif group == "promote_gate":
|
||||
inp = case["input"]
|
||||
ok, p, r, pr, rr = scenario_promote(
|
||||
[{"pattern": r"\bx\b"}], inp["id"])
|
||||
else: # handle_hook
|
||||
ok, p, r = scenario_hook(case["input"]["event"])
|
||||
n_truth += 1
|
||||
check(f"truth:{group}:{case['name']}", ok,
|
||||
f"py={json.dumps(p, sort_keys=True)[:300]}\n"
|
||||
f"rs={json.dumps(r, sort_keys=True)[:300]}")
|
||||
|
||||
# 2. seeded fuzz
|
||||
rng = random.Random(2479)
|
||||
n_fuzz = 0
|
||||
for kind, rows, arg in fuzz_cases(rng):
|
||||
n_fuzz += 1
|
||||
label = f"fuzz:{kind}:{n_fuzz}"
|
||||
if kind == "record":
|
||||
ok, p, r, pr, rr = scenario_record(rows, arg)
|
||||
elif kind == "ctx":
|
||||
ok, p, r = scenario_ctx(rows, arg)
|
||||
elif kind == "list":
|
||||
ok, p, r = scenario_list(rows)
|
||||
elif kind == "promote":
|
||||
ok, p, r, pr, rr = scenario_promote(rows, arg)
|
||||
else:
|
||||
ok, p, r = scenario_hook(arg)
|
||||
check(label, ok,
|
||||
f"py={json.dumps(p, sort_keys=True)[:300]}\n"
|
||||
f"rs={json.dumps(r, sort_keys=True)[:300]}\n"
|
||||
f"rows={json.dumps(rows, sort_keys=True)[:200]}")
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _rows_before(case):
|
||||
"""list_gates truth carries rows_after (the full pre-state — listing
|
||||
never mutates); replay it as the seed state."""
|
||||
return case["rows_after"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity gate for the time_awareness dichotomy port (#2479 tranche).
|
||||
|
||||
Three legs, every observation compared EXACTLY (the clock is pinned, so
|
||||
nothing is masked):
|
||||
|
||||
1. frozen truth replay on the pinned-python path (truth self-consistency)
|
||||
2. frozen truth replay on the kernel path (RUNE_TA_CORE unset)
|
||||
3. seeded fuzz (seed 2479): random scenarios + random temporal-reference
|
||||
texts, each driven through the live facade twice — pinned python vs
|
||||
kernel — results, state and file must match bit-for-bit.
|
||||
|
||||
Exit 0 only on 100% parity.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _HERE)
|
||||
sys.path.insert(0, os.path.join(_ROOT, "src"))
|
||||
|
||||
import gen_time_awareness_truth as gen # noqa: E402 — shared clock-pinned harness
|
||||
from rune import ta_loader # noqa: E402
|
||||
|
||||
TRUTH_PATH = os.path.join(_ROOT, "src", "rust", "time_awareness", "src", "frozen_ta_truth.json")
|
||||
|
||||
|
||||
def drive_pin(scenarios, pin, workdir):
|
||||
os.environ["RUNE_TA_CORE"] = pin
|
||||
driver = gen.Driver(workdir)
|
||||
return [driver.run(s["ops"]) for s in scenarios]
|
||||
|
||||
|
||||
def replay_truth(workdir):
|
||||
import json
|
||||
with open(TRUTH_PATH) as f:
|
||||
truth = json.load(f)
|
||||
scenarios = truth["scenarios"]
|
||||
total = sum(len(s["obs"]) for s in scenarios)
|
||||
py = drive_pin(scenarios, "python", os.path.join(workdir, "py"))
|
||||
rs = drive_pin(scenarios, "auto", os.path.join(workdir, "rs"))
|
||||
ok = 0
|
||||
for s, a, b in zip(scenarios, py, rs):
|
||||
for i, (x, y) in enumerate(zip(a, b)):
|
||||
if x == y:
|
||||
ok += 1
|
||||
else:
|
||||
print(f"DIVERGE {s['name']} op#{i} ({s['ops'][i].get('op')})")
|
||||
print(" py:", x)
|
||||
print(" rs:", y)
|
||||
return ok, total
|
||||
|
||||
|
||||
def fuzz_texts(rng, n):
|
||||
words = ["yesterday", "today", "tomorrow", "last", "next", "this", "morning",
|
||||
"afternoon", "tonight", "week", "month", "yesterdays", "mondays",
|
||||
"café", "lastweek", "soon", "later", "now", "the", "a"]
|
||||
seps = [" ", " ", "\t", "\n", ", ", ". "]
|
||||
out = []
|
||||
for _ in range(n):
|
||||
k = rng.randint(1, 6)
|
||||
out.append(rng.choice(seps).join(rng.choice(words) for _ in range(k)))
|
||||
return out
|
||||
|
||||
|
||||
def fuzz_iso(rng, valid_only=False):
|
||||
import datetime as _dt
|
||||
r = rng.random() * (0.8 if valid_only else 1.0)
|
||||
if r < 0.55:
|
||||
dt = gen.EPOCH + _dt.timedelta(microseconds=rng.randint(0, gen.BASE_US))
|
||||
dt += _dt.timedelta(days=rng.randint(-500, 500))
|
||||
off = rng.choice([None, 0, 3600, -18000])
|
||||
if off is not None:
|
||||
dt = dt.replace(tzinfo=_dt.timezone(_dt.timedelta(seconds=off)))
|
||||
return dt.isoformat()
|
||||
if r < 0.7:
|
||||
dt = gen.EPOCH + _dt.timedelta(microseconds=rng.randint(0, gen.BASE_US))
|
||||
return dt.replace(tzinfo=None).isoformat() # naive → TypeError branch
|
||||
if r < 0.8:
|
||||
return "2026-10-10T10:00:00Z"
|
||||
return rng.choice(["garbage", "", "2026-13-40T99:00:00+00:00", "[1,2]"])
|
||||
|
||||
|
||||
def fuzz_scenarios(rng, n, texts):
|
||||
scenarios = []
|
||||
for i in range(n):
|
||||
ops = [{"op": "new", "mono": round(rng.uniform(0.0, 400.0), 3)}]
|
||||
if rng.random() < 0.5:
|
||||
ops[0]["seed_file"] = gen.json.dumps(
|
||||
{"last_interaction": fuzz_iso(rng)}, indent=2
|
||||
)
|
||||
elif rng.random() < 0.1:
|
||||
ops[0]["seed_file"] = rng.choice(["corrupt{", "[1,2]"])
|
||||
for _ in range(rng.randint(1, 8)):
|
||||
kind = rng.choice(["new", "new", "seed_last", "ctx", "ctx", "status",
|
||||
"parse", "flush", "mark_dirty", "save_if_dirty"])
|
||||
op = {"op": kind, "mono": round(rng.uniform(0.0, 400.0), 3)}
|
||||
if kind == "new":
|
||||
if rng.random() < 0.5:
|
||||
op["seed_file"] = gen.json.dumps(
|
||||
{"last_interaction": fuzz_iso(rng)}, indent=2
|
||||
)
|
||||
elif rng.random() < 0.1:
|
||||
op["seed_file"] = rng.choice(["corrupt{", "[1,2]"])
|
||||
if kind == "seed_last":
|
||||
op["last_interaction"] = fuzz_iso(rng, valid_only=True) if rng.random() < 0.8 else None
|
||||
if kind in ("ctx", "status", "parse"):
|
||||
op["now_us"] = gen.BASE_US + rng.randint(-3000, 3000) * 3600 * 1_000_000
|
||||
if kind == "ctx":
|
||||
op["update"] = rng.random() < 0.6
|
||||
if kind == "parse":
|
||||
op["text"] = rng.choice(texts)
|
||||
op["now_us"] = gen.BASE_US + rng.randint(-3000, 3000) * 3600 * 1_000_000
|
||||
ops.append(op)
|
||||
scenarios.append({"name": f"fuzz_{i}", "ops": ops})
|
||||
return scenarios
|
||||
|
||||
|
||||
def main():
|
||||
import shutil
|
||||
work = "/tmp/ta_parity_work"
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
ok, total = replay_truth(work)
|
||||
print(f"truth replay parity: {ok}/{total}")
|
||||
|
||||
rng = random.Random(2479)
|
||||
texts = fuzz_texts(rng, 300)
|
||||
scenarios = fuzz_scenarios(rng, 200, texts)
|
||||
fz_total = sum(len(s["ops"]) for s in scenarios)
|
||||
py = drive_pin(scenarios, "python", os.path.join(work, "fpy"))
|
||||
rs = drive_pin(scenarios, "auto", os.path.join(work, "frs"))
|
||||
fz_ok = 0
|
||||
for s, a, b in zip(scenarios, py, rs):
|
||||
for i, (x, y) in enumerate(zip(a, b)):
|
||||
if x == y:
|
||||
fz_ok += 1
|
||||
else:
|
||||
print(f"DIVERGE {s['name']} op#{i} ({s['ops'][i].get('op')})")
|
||||
print(" py:", x)
|
||||
print(" rs:", y)
|
||||
print(f"fuzz parity (seed 2479): {fz_ok}/{fz_total}")
|
||||
|
||||
if ok == total and fz_ok == fz_total:
|
||||
print(f"PARITY OK — {ok + fz_ok}/{total + fz_total} observations, 100%")
|
||||
return 0
|
||||
print("PARITY FAILED")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: Rust toolchain kernel vs the Python oracle (dichotomy
|
||||
wave #2479). Replays the frozen truth corpus PLUS seeded fuzz over every
|
||||
public function and branch. The module is deterministic — no masking.
|
||||
Both sides must agree on values AND on raising.
|
||||
|
||||
The facade reads RUNE_TC_CORE per call, so one process can flip between
|
||||
the pinned oracle (RUNE_TC_CORE=python) and the kernel (unset).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
LANGS = ["python", "rust", "typescript", "go", "zig", "", "C++",
|
||||
"café-lang ☕", "r"]
|
||||
KEYS = sorted([
|
||||
"existing_language", "existing_runtime", "existing_framework",
|
||||
"target_platform", "deployment", "performance_sensitive",
|
||||
"latency_sensitive", "memory_constrained", "concurrency_model",
|
||||
"interoperability", "gui_native", "expected_lifespan",
|
||||
"user_constraints", "ecosystem_requirement"])
|
||||
UNKNOWN_KEYS = ["wibble", "flurb", "custom_axis", "future_thing"]
|
||||
VALS = ["linux", "server", "tokio", "svelte", True, False, None, 0, 1,
|
||||
"", ["a", "b"], {"deep": True}, 2.5, "x" * 60]
|
||||
KEYWORD_POOL = ["existing_language", "target_platform", "deployment",
|
||||
"performance_sensitive", "latency_sensitive",
|
||||
"memory_constrained", "gui_native", "wibble"]
|
||||
|
||||
|
||||
def call_core(core, fn, *args, **kwargs):
|
||||
from rune import toolchain as tc
|
||||
old = os.environ.get("RUNE_TC_CORE")
|
||||
os.environ["RUNE_TC_CORE"] = core
|
||||
try:
|
||||
return getattr(tc, fn)(*args, **kwargs)
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_TC_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_TC_CORE"] = old
|
||||
|
||||
|
||||
def toj(x):
|
||||
return x.to_dict() if hasattr(x, "to_dict") else x
|
||||
|
||||
|
||||
def constraints_of(core, kw):
|
||||
return call_core(core, "ToolchainConstraints", **kw)
|
||||
|
||||
|
||||
def run_pair(invoke):
|
||||
"""invoke(core) runs the full call chain under one core pin. Returns
|
||||
(ok, detail): ok True when values match, None when BOTH sides raised
|
||||
(agreement on refusing), False on any divergence."""
|
||||
outs = []
|
||||
for core in ("python", "rust"):
|
||||
try:
|
||||
outs.append((toj(invoke(core)), None))
|
||||
except Exception as e:
|
||||
outs.append((None, repr(e)))
|
||||
(py, py_err), (rs, rs_err) = outs
|
||||
if (py_err is None) != (rs_err is None):
|
||||
return False, f"py_err={py_err} rs_err={rs_err}"
|
||||
if py_err is not None:
|
||||
return None, f"both raised: {py_err}"
|
||||
return py == rs, f"py={py!r} rs={rs!r}"
|
||||
|
||||
|
||||
def fuzz_cases(rng):
|
||||
out = []
|
||||
# constraints classification
|
||||
for _ in range(120):
|
||||
kw = {}
|
||||
for k in KEYWORD_POOL + rng.sample(UNKNOWN_KEYS, rng.randrange(0, 3)):
|
||||
if rng.random() < 0.5:
|
||||
kw[k] = rng.choice(VALS)
|
||||
out.append(("constraints", kw))
|
||||
# constraints_get
|
||||
for _ in range(60):
|
||||
kw = {rng.choice(KEYS + UNKNOWN_KEYS): rng.choice(VALS)}
|
||||
key = rng.choice(list(kw) + ["missing_key"])
|
||||
out.append(("constraints_get", kw, key,
|
||||
rng.choice([None, "dflt", 0, False, ["x"]])))
|
||||
# decision_record normalization
|
||||
for _ in range(80):
|
||||
fields = dict(
|
||||
selected_language=rng.choice(LANGS),
|
||||
runtime=rng.choice(["", "tokio", None]),
|
||||
framework=rng.choice(["", "svelte", None]),
|
||||
libraries=rng.choice([None, [], ["b", "a"], ["z", "a", "m"]]),
|
||||
build_system=rng.choice(["", "cargo"]),
|
||||
alternatives=rng.choice([None, [], [{"language": "go"}]]),
|
||||
decisive_constraints=rng.choice([None, [], ["z", "a"]]),
|
||||
rationale=rng.choice(["", "measured 10x"]),
|
||||
evidence_refs=rng.choice([None, [], ["e2", "e1"]]),
|
||||
confidence=rng.choice([0.0, 0.4, 0.5, 0.7, 0.8, 0.85]),
|
||||
user_approval_required=rng.choice([True, False]),
|
||||
resolution=rng.choice(["", "KEEP_EXISTING", "CHANGE_MAJOR",
|
||||
"INSUFFICIENT_EVIDENCE"]),
|
||||
)
|
||||
fields = {k: v for k, v in fields.items() if rng.random() < 0.85}
|
||||
out.append(("decision_record", fields))
|
||||
# major_change
|
||||
for _ in range(60):
|
||||
out.append(("major_change", {"existing_language": rng.choice(LANGS)},
|
||||
rng.choice(LANGS)))
|
||||
# resolve_toolchain — every branch reachable
|
||||
for _ in range(250):
|
||||
kw = {}
|
||||
if rng.random() < 0.8:
|
||||
kw["existing_language"] = rng.choice(LANGS)
|
||||
for k in ("existing_runtime", "existing_framework"):
|
||||
if rng.random() < 0.3:
|
||||
kw[k] = rng.choice(["tokio", "node", ""])
|
||||
for k in ("target_platform", "deployment", "performance_sensitive",
|
||||
"latency_sensitive", "memory_constrained", "gui_native"):
|
||||
if rng.random() < 0.3:
|
||||
kw[k] = rng.choice(VALS)
|
||||
rkw = {}
|
||||
if rng.random() < 0.6:
|
||||
rkw["requested_language"] = rng.choice(
|
||||
[l for l in LANGS if l] + [None])
|
||||
if rng.random() < 0.4:
|
||||
rkw["evidence_refs"] = rng.choice([[], ["b", "a"], ["only"]])
|
||||
if rng.random() < 0.4:
|
||||
rkw["performance_evidence"] = rng.choice([
|
||||
{"significant_benefit": True, "basis": "measured"},
|
||||
{"significant_benefit": False},
|
||||
{"significant_benefit": None},
|
||||
{}, {"other": 1},
|
||||
])
|
||||
out.append(("resolve_toolchain", kw, rkw))
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from rune import toolchain as tc
|
||||
from rune import tl_loader
|
||||
if not tl_loader.available():
|
||||
print("Rust core not built — run cargo build --release in src/rust/toolchain")
|
||||
return 2
|
||||
|
||||
total = passed = agreed_errors = 0
|
||||
failures: list[str] = []
|
||||
|
||||
def record(label, ok, detail=""):
|
||||
nonlocal total, passed, agreed_errors
|
||||
total += 1
|
||||
if ok is None: # both sides raised — agreement on refusal
|
||||
agreed_errors += 1
|
||||
passed += 1
|
||||
elif ok:
|
||||
passed += 1
|
||||
else:
|
||||
failures.append(f"{label}: {detail}")
|
||||
|
||||
def resolve_invoke(rkw):
|
||||
return lambda c: call_core(
|
||||
c, "resolve_toolchain",
|
||||
constraints_of(c, rkw[0]), **rkw[1]).to_dict()
|
||||
|
||||
# 1. frozen truth corpus, replayed live on BOTH cores
|
||||
truth = json.loads((Path(__file__).resolve().parents[1] / "src" / "rust" /
|
||||
"toolchain" / "src" / "frozen_tc_truth.json").read_text())
|
||||
for fn, cases in truth["cases"].items():
|
||||
for case in cases:
|
||||
name = f"truth:{fn}:{case['name']}"
|
||||
inp = case["input"]
|
||||
if fn == "constraint_keys":
|
||||
py = sorted(tc.CONSTRAINT_KEYS)
|
||||
rs = tl_loader.constraint_keys()
|
||||
record(name, py == rs, f"py={py} rs={rs}")
|
||||
elif fn == "constraints":
|
||||
record(name, *run_pair(
|
||||
lambda c, i=inp: constraints_of(c, i["kwargs"]).to_dict()))
|
||||
elif fn == "constraints_get":
|
||||
record(name, *run_pair(
|
||||
lambda c, i=inp: constraints_of(c, i["known"])
|
||||
.get(i["key"], i["default"])))
|
||||
elif fn == "decision_record":
|
||||
record(name, *run_pair(
|
||||
lambda c, i=inp: call_core(c, "DecisionRecord", **i).to_dict()))
|
||||
elif fn == "major_change":
|
||||
record(name, *run_pair(
|
||||
lambda c, i=inp: call_core(
|
||||
c, "_major_change", constraints_of(c, i["known"]),
|
||||
i["language"])))
|
||||
elif fn == "resolve_toolchain":
|
||||
rkw = {k: v for k, v in inp.items()
|
||||
if k in ("requested_language", "evidence_refs",
|
||||
"performance_evidence")}
|
||||
record(name, *run_pair(resolve_invoke((inp["known"], rkw))))
|
||||
|
||||
# 2. seeded fuzz (kernel core vs freshly-pinned oracle per case)
|
||||
rng = random.Random(2479)
|
||||
for case in fuzz_cases(rng):
|
||||
fn = case[0]
|
||||
name = f"fuzz:{fn}"
|
||||
if fn == "constraints":
|
||||
ok, d = run_pair(lambda c, i=case[1]: constraints_of(c, i).to_dict())
|
||||
elif fn == "constraints_get":
|
||||
_, kw, key, default = case
|
||||
ok, d = run_pair(lambda c, k=kw, key=key, df=default:
|
||||
constraints_of(c, k).get(key, df))
|
||||
elif fn == "decision_record":
|
||||
ok, d = run_pair(lambda c, i=case[1]:
|
||||
call_core(c, "DecisionRecord", **i).to_dict())
|
||||
elif fn == "major_change":
|
||||
_, kw, language = case
|
||||
ok, d = run_pair(lambda c, k=kw, l=language: call_core(
|
||||
c, "_major_change", constraints_of(c, k), l))
|
||||
else: # resolve_toolchain
|
||||
_, kw, rkw = case
|
||||
ok, d = run_pair(resolve_invoke((kw, rkw)))
|
||||
record(name, ok, f"case={case} {d}")
|
||||
|
||||
print(f"parity {passed}/{total} ({agreed_errors} agreed-error cases)")
|
||||
if failures:
|
||||
for f in failures[:12]:
|
||||
print("FAIL", f[:400])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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")
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open + walk + close the #2479 dichotomy-wave construction contract for
|
||||
message_graph. 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(
|
||||
"Port src/rune/message_graph.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": "message_graph parity port — parity 1f7a2664 "
|
||||
"feat(message_graph): port to FULL Rust — dichotomy wave; "
|
||||
"Phase B 7194f69a refactor(message_graph): Phase B "
|
||||
"language-advantage pass — dichotomy wave (both pushed "
|
||||
"gitea/master). Battery 12 tests replay 68 frozen-oracle "
|
||||
"cases VERBATIM; live parity 68/68 byte-identical; clippy 0; "
|
||||
"Python suite green under both pins (9+9).",
|
||||
}], scope_files=[
|
||||
"src/rune/message_graph.py",
|
||||
"src/rune/mg_loader.py",
|
||||
"src/rust/message_graph/src/lib.rs",
|
||||
"src/rust/message_graph/src/branch_battery.rs",
|
||||
"scripts/gen_message_graph_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close the #2479 dichotomy-wave construction contract for self_modify.
|
||||
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(
|
||||
"Port src/rune/self_modify.py to FULL Rust (dichotomy factory, issue "
|
||||
"#2479 tranche): kernel-owned subprocess seam (_run), doc-envelope FFI, "
|
||||
"ctypes loader, facade delegation with RUNE_SMOD_CORE=python pin, "
|
||||
"frozen-truth battery with deterministic git fixtures, 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": "self_modify parity port — evidence attached at close (SHAs "
|
||||
"reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/self_modify.py",
|
||||
"src/rune/smod_loader.py",
|
||||
"src/rust/self_modify/src/lib.rs",
|
||||
"src/rust/self_modify/src/branch_battery.rs",
|
||||
"src/rust/self_modify/src/frozen_smod_truth.json",
|
||||
"scripts/gen_self_modify_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live-parity fuzz for the capability_ledger port (dichotomy factory,
|
||||
#2479 tranche). Generates ONE seeded random op sequence (seed 2479) and
|
||||
replays it through the FULL Python facade twice:
|
||||
|
||||
* RUNE_LEDGER_CORE=python — the oracle body
|
||||
* RUNE_LEDGER_CORE=auto — the Rust kernel via cl_loader (facade
|
||||
delegation), falling back silently only if the .so is absent
|
||||
|
||||
and diffs every op result (value JSON + exception type/message) plus the
|
||||
final saved-file bytes. Clock is pinned per op so both runs stamp
|
||||
identically (the kernel receives the same `now` string the oracle's
|
||||
_now() returns).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
SEED = 2479
|
||||
N_OPS = 500
|
||||
|
||||
IDS = ["alpha", "beta", "gamma", "delta", "ghost", "cap:beta", "cap:ghost"]
|
||||
COMPONENTS = ["component:c1", "component:c2", "component:missing"]
|
||||
LONG = "L" * 260
|
||||
|
||||
|
||||
def gen_ops():
|
||||
import random
|
||||
rng = random.Random(SEED)
|
||||
ops = [("new", {})]
|
||||
has_spec = set()
|
||||
for _ in range(N_OPS - 1):
|
||||
r = rng.random()
|
||||
cid = rng.choice(IDS)
|
||||
if r < 0.14:
|
||||
ops.append(("define", {
|
||||
"capability_id": cid,
|
||||
"name": rng.choice(["A", "Bee", "C-3"]),
|
||||
"purpose": rng.choice(["p1", "purpose two"]),
|
||||
"owning_project": "rune",
|
||||
"component": rng.choice(COMPONENTS),
|
||||
"source_locations": rng.choice(
|
||||
[None, [], ["src/a.py"], ["src/a.py", "src/b.py"]]),
|
||||
"dependencies": rng.choice([None, [], ["dep-1"]]),
|
||||
"consumers": rng.choice([None, [], ["cons-1"]]),
|
||||
"invariants": rng.choice([None, [], ["inv-1"]]),
|
||||
"behaviour_specs": rng.choice([None, [], ["bs-1"]]),
|
||||
"contract": rng.choice(["", "contract text"]),
|
||||
"state_touched": rng.choice(["", "store/scaffold"]),
|
||||
"constraints": rng.choice(["", "no raw tokens"]),
|
||||
"generation": rng.choice(["", "gen-1"]),
|
||||
}))
|
||||
elif r < 0.30:
|
||||
kw = {"note": rng.choice(["n1", LONG, "ünïcode ✓ note"])}
|
||||
if rng.random() < 0.4:
|
||||
kw["generation"] = rng.choice(["", "gen-9"])
|
||||
if rng.random() < 0.4:
|
||||
kw["source_locations"] = rng.choice(
|
||||
[[], ["src/moved.py"], ["a.py", "b.py"]])
|
||||
ops.append(("material_change", {"capability_id": cid, **kw}))
|
||||
elif r < 0.38:
|
||||
ops.append(("move", {"capability_id": cid,
|
||||
"new_locations": rng.choice(
|
||||
[[], ["new/loc.py"], ["x.py", "y.py"]])}))
|
||||
elif r < 0.46:
|
||||
ops.append(("supersede", {"capability_id": cid,
|
||||
"by": rng.choice(["new-cap", "cap:beta", "ghost"]),
|
||||
"note": rng.choice(["", "why not", LONG])}))
|
||||
elif r < 0.52:
|
||||
ops.append(("retire", {"capability_id": cid,
|
||||
"note": rng.choice(["", "dead", LONG])}))
|
||||
elif r < 0.62:
|
||||
ops.append(("get", {"capability_id": cid}))
|
||||
elif r < 0.68:
|
||||
ops.append(("active", {}))
|
||||
elif r < 0.74:
|
||||
ops.append(("by_component", {"component": rng.choice(COMPONENTS)}))
|
||||
elif r < 0.80:
|
||||
ops.append(("context_packet", {"capability_id": cid}))
|
||||
elif r < 0.83:
|
||||
ops.append(("save", {}))
|
||||
elif r < 0.89:
|
||||
kw = {"requirements": rng.choice([[], ["r1"], ["r1", "r2"]]),
|
||||
"expected_behaviours": rng.choice([[], ["b1"]]),
|
||||
"non_goals": rng.choice([[], ["ng1"]]),
|
||||
"acceptance_criteria": rng.choice([[], ["ac1"]])}
|
||||
if rng.random() < 0.3:
|
||||
kw["baseline_version"] = rng.choice(
|
||||
["0.1.0", "1.0.0", "1.0", "not-semver"])
|
||||
if rng.random() < 0.3:
|
||||
kw["baseline_reason"] = rng.choice(["stable", LONG])
|
||||
if rng.random() < 0.3:
|
||||
kw["governance_version_started_at"] = "2025-12-31T00:00:00Z"
|
||||
ops.append(("attach_spec", {"capability_id": cid, **kw}))
|
||||
elif r < 0.97:
|
||||
kw = {"reason": rng.choice(["why", LONG]),
|
||||
"contract_changed": rng.random() < 0.2,
|
||||
"behaviour_added": rng.random() < 0.3,
|
||||
"consumer_break": rng.random() < 0.15,
|
||||
"implementation_only": rng.random() < 0.5,
|
||||
"changed_sections": rng.choice([None, [], ["sec-1"]]),
|
||||
"affected_consumers": rng.choice([None, [], ["cons-x"]]),
|
||||
"validation_consequences": rng.choice([None, [], ["retest"]])}
|
||||
if rng.random() < 0.35:
|
||||
kw["proposed_kind"] = rng.choice(
|
||||
["PATCH", "MINOR", "MAJOR", "HUGE", ""])
|
||||
ops.append(("revise_spec", {"capability_id": cid, **kw}))
|
||||
elif r < 0.99:
|
||||
ops.append(("spec_drift_status", {"capability_id": cid}))
|
||||
elif r < 0.995:
|
||||
ops.append(("spec_context", {"capability_id": cid}))
|
||||
else:
|
||||
if rng.random() < 0.5:
|
||||
ops.append(("bump_version", {
|
||||
"version": rng.choice(
|
||||
["0.1.0", "1.2.3", "01.02.03", "0.0.9", "1.2",
|
||||
"abc", "", "1.2.3.4", "-1.2.3", "1.2.x",
|
||||
"18446744073709551615.0.0"]),
|
||||
"kind": rng.choice(["MAJOR", "MINOR", "PATCH", "BOGUS"])}))
|
||||
else:
|
||||
ops.append(("classify_version_bump", {
|
||||
"contract_changed": rng.random() < 0.3,
|
||||
"behaviour_added": rng.random() < 0.4,
|
||||
"consumer_break": rng.random() < 0.2,
|
||||
"implementation_only": rng.random() < 0.5}))
|
||||
return ops
|
||||
|
||||
|
||||
def run(ops):
|
||||
import rune.capability_ledger as cl
|
||||
trace = []
|
||||
stamp = [0]
|
||||
|
||||
def now():
|
||||
return f"2026-03-04T00:{stamp[0] // 60:02d}:{stamp[0] % 60:02d}Z"
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="clfuzz-")
|
||||
led = None
|
||||
for opname, kw in ops:
|
||||
stamp[0] += 1
|
||||
cl._now = now # pinned for the whole op (python path); kernel gets now where passed
|
||||
try:
|
||||
if opname == "new":
|
||||
led = cl.CapabilityLedger(tmp)
|
||||
out = None
|
||||
elif opname == "define":
|
||||
out = led.define(**kw)
|
||||
elif opname == "material_change":
|
||||
out = led.material_change(kw["capability_id"],
|
||||
note=kw["note"],
|
||||
generation=kw.get("generation", ""),
|
||||
source_locations=kw.get("source_locations"))
|
||||
elif opname == "move":
|
||||
out = led.move(kw["capability_id"], kw["new_locations"])
|
||||
elif opname == "supersede":
|
||||
out = led.supersede(kw["capability_id"], kw["by"],
|
||||
note=kw["note"])
|
||||
elif opname == "retire":
|
||||
out = led.retire(kw["capability_id"], note=kw["note"])
|
||||
elif opname == "get":
|
||||
out = led.get(kw["capability_id"])
|
||||
elif opname == "active":
|
||||
out = led.active()
|
||||
elif opname == "by_component":
|
||||
out = led.by_component(kw["component"])
|
||||
elif opname == "context_packet":
|
||||
out = led.context_packet(kw["capability_id"])
|
||||
elif opname == "save":
|
||||
led.save()
|
||||
out = None
|
||||
elif opname == "attach_spec":
|
||||
out = cl.attach_spec(led, kw["capability_id"],
|
||||
requirements=kw["requirements"],
|
||||
expected_behaviours=kw["expected_behaviours"],
|
||||
non_goals=kw["non_goals"],
|
||||
acceptance_criteria=kw["acceptance_criteria"],
|
||||
baseline_version=kw.get("baseline_version", "0.1.0"),
|
||||
baseline_reason=kw.get("baseline_reason", "initial governed baseline"),
|
||||
governance_version_started_at=kw.get("governance_version_started_at"))
|
||||
elif opname == "revise_spec":
|
||||
out = cl.revise_spec(led, kw["capability_id"],
|
||||
contract_changed=kw["contract_changed"],
|
||||
behaviour_added=kw["behaviour_added"],
|
||||
consumer_break=kw["consumer_break"],
|
||||
implementation_only=kw["implementation_only"],
|
||||
reason=kw["reason"],
|
||||
changed_sections=kw["changed_sections"],
|
||||
affected_consumers=kw["affected_consumers"],
|
||||
validation_consequences=kw["validation_consequences"],
|
||||
proposed_kind=kw.get("proposed_kind"))
|
||||
elif opname == "spec_drift_status":
|
||||
out = cl.spec_drift_status(led, kw["capability_id"])
|
||||
elif opname == "spec_context":
|
||||
out = cl.spec_context(led.get(kw["capability_id"]))
|
||||
elif opname == "bump_version":
|
||||
out = cl.bump_version(kw["version"], kw["kind"])
|
||||
else:
|
||||
out = cl.classify_version_bump(
|
||||
contract_changed=kw["contract_changed"],
|
||||
behaviour_added=kw["behaviour_added"],
|
||||
consumer_break=kw["consumer_break"],
|
||||
implementation_only=kw["implementation_only"])
|
||||
trace.append(json.dumps({"ok": True, "value": out},
|
||||
sort_keys=True, default=str))
|
||||
except Exception as e:
|
||||
trace.append(json.dumps(
|
||||
{"ok": False, "err": f"{type(e).__name__}: {e}"},
|
||||
sort_keys=True))
|
||||
saved = (Path(tmp) / "capability_ledger.json")
|
||||
trace.append(saved.read_text() if saved.is_file() else "NO_FILE")
|
||||
return trace
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ops = gen_ops()
|
||||
# kernel path first (loader caches; env must differ per run)
|
||||
os.environ["RUNE_LEDGER_CORE"] = "auto"
|
||||
from rune import cl_loader
|
||||
if not cl_loader.available():
|
||||
print("KERNEL .so ABSENT — build libcapability_ledger.so first")
|
||||
return 2
|
||||
rust = run(ops)
|
||||
os.environ["RUNE_LEDGER_CORE"] = "python"
|
||||
py = run(ops)
|
||||
os.environ["RUNE_LEDGER_CORE"] = "auto"
|
||||
|
||||
div = [i for i, (a, b) in enumerate(zip(py, rust)) if a != b]
|
||||
print(f"ops={len(ops)} compared={len(py)} divergences={len(div)}")
|
||||
for i in div[:10]:
|
||||
print(f"--- op {i} {ops[i] if i < len(ops) else '(file bytes)'}")
|
||||
print(" PY :", py[i][:300])
|
||||
print(" RUST:", rust[i][:300])
|
||||
print("PARITY:", "100%" if not div else f"{100 * (len(py) - len(div)) // len(py)}%")
|
||||
return 0 if not div else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze behavior oracle truth for the Rust port battery
|
||||
(dichotomy wave #2479 tranche, batch: behavior).
|
||||
|
||||
behavior.py's decision surface (9 public fns):
|
||||
connect, add_correction, add_habit, match_context, reinforce,
|
||||
list_items, behavior_stats, main (CLI — not frozen; argparse/IO),
|
||||
PLUGIN (data — embedded verbatim in the crate).
|
||||
|
||||
The decision math (reinforce counting/confidence/promotion/state) is
|
||||
served IN-PROCESS by the Rust evidence_provenance cdylib through
|
||||
ep_loader — the oracle IS the strangler shim. The frozen truth pins
|
||||
RUNE_BEHAVIOR_CORE=python (no-op today; the POISONED-TRUTH LAW: a
|
||||
future delegating facade must never silently record kernel output as
|
||||
oracle truth).
|
||||
|
||||
Frozen: add_correction/add_habit return shapes, match_context branch
|
||||
matrix (pattern/domain/confidence/match_type/invalid-regex), reinforce
|
||||
branch matrix (habit/correction x success/failure/neutral x
|
||||
promotion/archival/not-found), list_items filters+limits, stats
|
||||
counts. SQLite rows replayed as dicts (row_factory=Row -> dict(c)).
|
||||
|
||||
Output: /tmp/behavior_truth.json — replayed VERBATIM by
|
||||
src/rust/behavior/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_BEHAVIOR_CORE"] = "python" # freeze the ORACLE, never the kernel
|
||||
|
||||
_tmp = tempfile.mkdtemp(prefix="bh-truth-")
|
||||
os.environ["RUNE_HOME"] = _tmp
|
||||
os.environ["RUNE_MEMORY_DIR"] = os.path.join(_tmp, "memory")
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import behavior as bh # noqa: E402
|
||||
|
||||
|
||||
def row(table, rid):
|
||||
db = bh.connect()
|
||||
try:
|
||||
r = db.execute(f"SELECT * FROM {table} WHERE id=?", (rid,)).fetchone()
|
||||
return dict(r) if r else None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
cases = {"add_correction": [], "add_habit": [], "match": [],
|
||||
"reinforce_habit": [], "reinforce_correction": [],
|
||||
"list": [], "stats": []}
|
||||
|
||||
# ── add_correction shapes ─────────────────────────────────────────────
|
||||
r = bh.add_correction("say X", "say Y")
|
||||
cases["add_correction"].append({"name": "defaults", "args": ["say X", "say Y"],
|
||||
"expected": r, "row": row("corrections", r["added"])})
|
||||
r = bh.add_correction("orig", "corr", scope_tags={"k": "v"}, domain="shell",
|
||||
pattern="shell")
|
||||
cases["add_correction"].append({"name": "scoped", "args": ["orig", "corr"],
|
||||
"expected": r, "row": row("corrections", r["added"])})
|
||||
|
||||
# ── add_habit shapes ──────────────────────────────────────────────────
|
||||
r = bh.add_habit("trig", "act")
|
||||
cases["add_habit"].append({"name": "defaults", "expected": r,
|
||||
"row": row("habits", r["added"])})
|
||||
r = bh.add_habit("trig2", "act2", name="n", context_tags=["a"],
|
||||
min_activations=5)
|
||||
cases["add_habit"].append({"name": "full", "expected": r,
|
||||
"row": row("habits", r["added"])})
|
||||
|
||||
# ── match_context branch matrix ───────────────────────────────────────
|
||||
# seed: pattern+domain correction, domain-only correction, low-conf
|
||||
# correction, pattern-only correction, bad-regex correction, habits
|
||||
bh.add_correction("o1", "scoped corr", scope_tags={}, domain="shell",
|
||||
pattern="deploy")
|
||||
bh.add_correction("o2", "domain corr", scope_tags={}, domain="editor")
|
||||
bh.add_correction("o3", "plain corr")
|
||||
bh.add_correction("o4", "bad regex corr", pattern="([unclosed")
|
||||
low = bh.add_correction("o5", "low conf corr")["added"]
|
||||
db = bh.connect()
|
||||
db.execute("UPDATE corrections SET confidence=0.2 WHERE id=?", (low,))
|
||||
db.commit()
|
||||
db.close()
|
||||
bh.add_habit("deploy", "run deploy script")
|
||||
bh.add_habit("test (unit|e2e)", "run tests")
|
||||
bh.add_habit("([broken", "bad regex habit")
|
||||
hslow = bh.add_habit("zzz-no-match-ever", "quiet")["added"]
|
||||
db = bh.connect()
|
||||
db.execute("UPDATE habits SET confidence=0.2 WHERE id=?", (hslow,))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def match_case(name, context, match_type=None):
|
||||
got = bh.match_context(context, match_type=match_type)
|
||||
return {"name": name, "context": context, "match_type": match_type,
|
||||
"corrections": sorted(c["corrected_behavior"] for c in got["corrections"]),
|
||||
"habits": sorted(h["action_template"] for h in got["habits"]),
|
||||
"n_corr_fields": sorted(got["corrections"][0].keys()) if got["corrections"] else [],
|
||||
"scope_tags_type": type(got["corrections"][0]["scope_tags"]).__name__ if got["corrections"] else None}
|
||||
|
||||
|
||||
cases["match"].append(match_case("pattern-domain-hit", "deploy to shell now"))
|
||||
cases["match"].append(match_case("domain-only-miss", "deploy the editor"))
|
||||
cases["match"].append(match_case("plain-always", "unrelated chatter here"))
|
||||
cases["match"].append(match_case("type-habit-only", "deploy to shell now", "habit"))
|
||||
cases["match"].append(match_case("type-correction-only", "deploy to shell now", "correction"))
|
||||
cases["match"].append(match_case("habit-regex-hit", "run the unit tests"))
|
||||
cases["match"].append(match_case("low-conf-filtered", "zzz-no-match-ever quiet"))
|
||||
cases["match"].append(match_case("bad-regex-skipped", "([unclosed anything"))
|
||||
|
||||
# ── reinforce matrix ──────────────────────────────────────────────────
|
||||
def reinf_case(name, item_type, outcome, seed_fn, pre=None, n=1):
|
||||
rid = seed_fn()["added"]
|
||||
if pre:
|
||||
db = bh.connect()
|
||||
for k, v in pre.items():
|
||||
db.execute(f"UPDATE {'habits' if item_type == 'habit' else 'corrections'} SET {k}=? WHERE id=?", (v, rid))
|
||||
db.commit()
|
||||
db.close()
|
||||
outs = [bh.reinforce(rid, outcome, item_type=item_type) for _ in range(n)]
|
||||
return {"name": name, "item_type": item_type, "outcome": outcome,
|
||||
"pre": pre or {}, "n": n, "results": outs,
|
||||
"final_row": row("habits" if item_type == "habit" else "corrections", rid)}
|
||||
|
||||
|
||||
cases["reinforce_habit"].append(reinf_case(
|
||||
"success-promotes-at-3", "habit", "success", lambda: bh.add_habit("t1", "a1"), n=3))
|
||||
cases["reinforce_habit"].append(reinf_case(
|
||||
"failure-counts", "habit", "failure", lambda: bh.add_habit("t2", "a2"), n=2))
|
||||
cases["reinforce_habit"].append(reinf_case(
|
||||
"neutral-bumps-activation-only", "habit", "neutral",
|
||||
lambda: bh.add_habit("t3", "a3"), n=1))
|
||||
cases["reinforce_habit"].append(reinf_case(
|
||||
"already-active-stays", "habit", "success",
|
||||
lambda: bh.add_habit("t4", "a4", min_activations=1), n=2))
|
||||
cases["reinforce_habit"].append(reinf_case(
|
||||
"min5-gate", "habit", "success",
|
||||
lambda: bh.add_habit("t5", "a5", min_activations=5), n=4))
|
||||
cases["reinforce_correction"].append(reinf_case(
|
||||
"success-plus-point-one", "correction", "success",
|
||||
lambda: bh.add_correction("x1", "y1"), n=1))
|
||||
cases["reinforce_correction"].append(reinf_case(
|
||||
"failure-archives", "correction", "failure",
|
||||
lambda: bh.add_correction("x2", "y2"), n=1))
|
||||
cases["reinforce_correction"].append(reinf_case(
|
||||
"double-failure-floor", "correction", "failure",
|
||||
lambda: bh.add_correction("x3", "y3"), n=3))
|
||||
cases["reinforce_correction"].append(reinf_case(
|
||||
"ceiling-at-one", "correction", "success",
|
||||
lambda: bh.add_correction("x4", "y4"), pre={"confidence": 0.95}, n=1))
|
||||
cases["reinforce_correction"].append(reinf_case(
|
||||
"neutral-noop", "correction", "neutral",
|
||||
lambda: bh.add_correction("x5", "y5"), n=1))
|
||||
cases["reinforce_habit"].append(
|
||||
{"name": "habit-not-found", "item_type": "habit", "outcome": "success",
|
||||
"pre": {}, "n": 1,
|
||||
"results": [bh.reinforce(99991, "success", item_type="habit")],
|
||||
"final_row": None})
|
||||
cases["reinforce_correction"].append(
|
||||
{"name": "correction-not-found", "item_type": "correction",
|
||||
"outcome": "success", "pre": {}, "n": 1,
|
||||
"results": [bh.reinforce(99992, "success", item_type="correction")],
|
||||
"final_row": None})
|
||||
|
||||
# ── list_items filters ────────────────────────────────────────────────
|
||||
cases["list"].append({"name": "all-keys",
|
||||
"result_keys": sorted(bh.list_items().keys())})
|
||||
for t in ("all", "correction", "habit"):
|
||||
got = bh.list_items(item_type=t)
|
||||
cases["list"].append({"name": f"type-{t}", "keys": sorted(got.keys()),
|
||||
"n_corr": len(got.get("corrections", [])),
|
||||
"n_hab": len(got.get("habits", []))})
|
||||
got = bh.list_items(limit=1)
|
||||
cases["list"].append({"name": "limit-1", "n_corr": len(got.get("corrections", [])),
|
||||
"n_hab": len(got.get("habits", []))})
|
||||
|
||||
# ── stats ─────────────────────────────────────────────────────────────
|
||||
cases["stats"].append({"name": "counts", "expected": bh.behavior_stats()})
|
||||
|
||||
truth = {"frozen": "behavior decision surface (reinforce math via ep strangler shim)",
|
||||
"oracle": "src/rune/behavior.py",
|
||||
"cases": cases,
|
||||
"plugin": bh.PLUGIN}
|
||||
|
||||
out = Path("/tmp/behavior_truth.json")
|
||||
out.write_text(json.dumps(truth, indent=1, default=str) + "\n")
|
||||
n = (len(cases["add_correction"]) + len(cases["add_habit"]) + len(cases["match"])
|
||||
+ len(cases["reinforce_habit"]) + len(cases["reinforce_correction"])
|
||||
+ len(cases["list"]) + len(cases["stats"]))
|
||||
print(f"froze {n} cases -> {out}")
|
||||
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze build_basis oracle truth for the Rust port battery (dichotomy
|
||||
wave #2479 tranche, batch: build_basis — RUST side verdict).
|
||||
|
||||
Exercises EVERY public function across EVERY branch:
|
||||
research_depth (all route defaults, unknown-route floor, tier-selecting
|
||||
vs annotating justifications, blank justification), new_basis,
|
||||
set_dispositions (legal/illegal/missing keys), validate_basis (schema,
|
||||
depth, WHY rule D, OUTWARD/FULL prior-art gate, INWARD evidence gate,
|
||||
illegal disposition, item truncation), is_qualified,
|
||||
attach_evidence_ref (issue_ref/adhoc/missing created_at), summary
|
||||
(empty/qualified/None implications/counts), revise_depth (legal
|
||||
revision, blocked downgrade, no-op, unknown depth, reason truncation),
|
||||
to_json (Python json.dumps indent=1 sort_keys incl. non-ASCII escaping),
|
||||
from_json, generate_research_plan (topic kinds, outward keywords,
|
||||
depth heuristics FULL/OUTWARD/INWARD/NONE, blank filtering, caller
|
||||
depth, truncations).
|
||||
|
||||
Output: src/rust/build_basis/src/frozen_bb_truth.json — replayed
|
||||
VERBATIM by src/rust/build_basis/src/branch_battery.rs.
|
||||
|
||||
Timestamps the kernel generates (decided_at/created_at/revised_at/
|
||||
plan_generated_at) are masked (removed on both sides) — they are wall
|
||||
clock, not logic. Fixed created_at values are INJECTED into basis inputs
|
||||
so kernel pass-through fields compare exactly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_BB_CORE"] = "python" # POISONED-TRUTH LAW: freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import build_basis as bb # noqa: E402
|
||||
|
||||
MASKED = ("decided_at", "created_at", "revised_at", "plan_generated_at")
|
||||
|
||||
|
||||
def mask(doc):
|
||||
"""Remove kernel-generated wall-clock fields wherever they appear."""
|
||||
if isinstance(doc, dict):
|
||||
return {k: mask(v) for k, v in doc.items() if k not in MASKED}
|
||||
if isinstance(doc, list):
|
||||
return [mask(v) for v in doc]
|
||||
return doc
|
||||
|
||||
|
||||
def fixed_basis(depth_route="RESEARCHED_CHANGE", question="q",
|
||||
issue_ref="research/issue-1"):
|
||||
b = bb.new_basis(question=question,
|
||||
depth_decision=bb.research_depth(depth_route),
|
||||
issue_ref=issue_ref)
|
||||
b["created_at"] = "2026-10-11T00:00:00Z" # fixed: pass-through, exact-compare
|
||||
return b
|
||||
|
||||
|
||||
cases: dict[str, list] = {}
|
||||
|
||||
|
||||
def run(fn: str, name: str, **input):
|
||||
out = getattr(bb, fn)(**input)
|
||||
cases.setdefault(fn, []).append(
|
||||
{"name": name, "input": input, "expected": mask(out)})
|
||||
|
||||
|
||||
def run_err(fn: str, name: str, **input):
|
||||
try:
|
||||
getattr(bb, fn)(**input)
|
||||
except ValueError as e:
|
||||
cases.setdefault(fn, []).append(
|
||||
{"name": name, "input": input, "error": str(e)})
|
||||
return
|
||||
raise AssertionError(f"{fn}({name}) did not raise")
|
||||
|
||||
|
||||
# ── research_depth: every branch ──────────────────────────────────────
|
||||
for route in ("LOCAL_FIX", "STANDARD_CHANGE", "RESEARCHED_CHANGE"):
|
||||
run("research_depth", f"route-{route}", route=route)
|
||||
run("research_depth", "unknown-route-floor", route="MYSTERY_ROUTE")
|
||||
run("research_depth", "annotation-justification", route="RESEARCHED_CHANGE",
|
||||
justification="tiny config fix; full pipeline disproportionate")
|
||||
run("research_depth", "tier-select-justification", route="STANDARD_CHANGE",
|
||||
justification="OUTWARD: new provider integration, precedent needed")
|
||||
run("research_depth", "tier-select-downgrade-NONE", route="RESEARCHED_CHANGE",
|
||||
justification="NONE: nothing to research")
|
||||
run("research_depth", "tier-bare-word", route="STANDARD_CHANGE",
|
||||
justification=" OUTWARD ")
|
||||
run("research_depth", "tier-multi-colon", route="STANDARD_CHANGE",
|
||||
justification="FULL: architectural; check heuristics first")
|
||||
run("research_depth", "blank-justification-default", route="LOCAL_FIX",
|
||||
justification=" ")
|
||||
run("research_depth", "blank-justification-unknown", route="WEIRD",
|
||||
justification=" ")
|
||||
run("research_depth", "unknown-route-with-justification", route="WEIRD",
|
||||
justification="check the spike notes")
|
||||
run("research_depth", "mode-recorded", route="RESEARCHED_CHANGE",
|
||||
mode="rune-internal")
|
||||
|
||||
# ── new_basis ─────────────────────────────────────────────────────────
|
||||
b = bb.new_basis(question="structural model for project scaffolds",
|
||||
depth_decision=bb.research_depth("RESEARCHED_CHANGE"),
|
||||
issue_ref="research/structural-model")
|
||||
b["created_at"] = "2026-10-11T00:00:00Z"
|
||||
cases.setdefault("new_basis", []).append(
|
||||
{"name": "full", "input": {"question": b["question"],
|
||||
"depth_decision": mask(b["depth"]),
|
||||
"issue_ref": b["issue_ref"]},
|
||||
"expected": mask(b)})
|
||||
b2 = bb.new_basis(question="q", depth_decision=bb.research_depth("LOCAL_FIX"))
|
||||
b2["created_at"] = "2026-10-11T00:00:00Z"
|
||||
cases["new_basis"].append(
|
||||
{"name": "no-issue-ref", "input": {"question": "q",
|
||||
"depth_decision": mask(b2["depth"])},
|
||||
"expected": mask(b2)})
|
||||
|
||||
# ── set_dispositions ──────────────────────────────────────────────────
|
||||
for nm, disp in (
|
||||
("legal", [{"item": "Backstage catalog entity model", "disposition": "ADAPT",
|
||||
"why": "nesting fits", "ref": "backstage.io"},
|
||||
{"item": "own graph database", "disposition": "REJECT",
|
||||
"why": "structural_store is adequate", "ref": "repo"}]),
|
||||
("illegal", [{"item": "x", "disposition": "MAYBE"}]),
|
||||
("missing-keys", [{}]),
|
||||
("empty-list", []),
|
||||
):
|
||||
basis = fixed_basis()
|
||||
before = json.loads(json.dumps(basis))
|
||||
errs = bb.set_dispositions(basis, disp)
|
||||
cases.setdefault("set_dispositions", []).append(
|
||||
{"name": nm, "input": {"basis": before, "dispositions": disp},
|
||||
"expected": {"basis": mask(basis), "errors": errs}})
|
||||
|
||||
# ── validate_basis: every error branch ────────────────────────────────
|
||||
def val_case(nm, basis):
|
||||
cases.setdefault("validate_basis", []).append(
|
||||
{"name": nm, "input": {"basis": basis},
|
||||
"expected": bb.validate_basis(basis)})
|
||||
|
||||
b = fixed_basis(); b["schema_version"] = 2
|
||||
val_case("schema-mismatch", b)
|
||||
b = fixed_basis(); b["schema_version"] = None
|
||||
val_case("schema-none", b)
|
||||
b = fixed_basis(); b["depth"] = {}
|
||||
val_case("depth-missing", b)
|
||||
b = fixed_basis(); b["depth"] = {"depth": "WEIRD", "why": "w"}
|
||||
val_case("depth-illegal", b)
|
||||
b = fixed_basis(); b["depth"] = {"depth": "FULL"}
|
||||
val_case("why-missing-rule-D", b)
|
||||
val_case("outward-no-evidence", fixed_basis(depth_route="STANDARD_CHANGE",
|
||||
issue_ref="",
|
||||
question="q") | {"depth": {"depth": "OUTWARD", "why": "w"}})
|
||||
b = fixed_basis(); b["depth"] = {"depth": "FULL", "why": "w"}
|
||||
val_case("full-no-evidence", b)
|
||||
val_case("inward-empty", bb.new_basis(question="q",
|
||||
depth_decision={"depth": "INWARD", "why": "w"}))
|
||||
b = bb.new_basis(question="q", depth_decision={"depth": "INWARD", "why": "w"})
|
||||
b["inward"]["findings"] = ["f"]
|
||||
val_case("inward-with-findings", b)
|
||||
b = fixed_basis()
|
||||
b["dispositions"] = [{"item": "x" * 60, "disposition": "MAYBE"}]
|
||||
val_case("illegal-disposition-item-truncated-40", b)
|
||||
b = fixed_basis(); b.pop("dispositions")
|
||||
val_case("dispositions-key-absent", b)
|
||||
b = fixed_basis(); b["outward"] = None
|
||||
val_case("outward-null-inward-ok", b)
|
||||
val_case("qualified-basis-zero-errors", fixed_basis() | {
|
||||
"outward": {"prior_art_questions_answered": ["q1"], "findings": [], "refs": []},
|
||||
"dispositions": [{"item": "i", "disposition": "ADOPT", "why": "w", "ref": "r"}]})
|
||||
|
||||
# ── is_qualified ──────────────────────────────────────────────────────
|
||||
q = fixed_basis()
|
||||
q["outward"] = {"prior_art_questions_answered": ["q1"], "findings": [], "refs": []}
|
||||
q["dispositions"] = [{"item": "i", "disposition": "ADOPT", "why": "w", "ref": "r"}]
|
||||
cases.setdefault("is_qualified", []).append(
|
||||
{"name": "qualified", "input": {"basis": q}, "expected": bb.is_qualified(q)})
|
||||
cases["is_qualified"].append(
|
||||
{"name": "empty-unqualified", "input": {"basis": fixed_basis()},
|
||||
"expected": False})
|
||||
|
||||
# ── attach_evidence_ref ───────────────────────────────────────────────
|
||||
cases.setdefault("attach_evidence_ref", []).append(
|
||||
{"name": "with-issue-ref", "input": {"basis": fixed_basis()},
|
||||
"expected": bb.attach_evidence_ref(fixed_basis())})
|
||||
b = fixed_basis(); b["issue_ref"] = ""
|
||||
cases["attach_evidence_ref"].append(
|
||||
{"name": "adhoc", "input": {"basis": b}, "expected": bb.attach_evidence_ref(b)})
|
||||
b = fixed_basis(); b.pop("created_at")
|
||||
cases["attach_evidence_ref"].append(
|
||||
{"name": "missing-created-at", "input": {"basis": b},
|
||||
"expected": bb.attach_evidence_ref(b)})
|
||||
|
||||
# ── summary ───────────────────────────────────────────────────────────
|
||||
b = fixed_basis()
|
||||
cases.setdefault("summary", []).append(
|
||||
{"name": "empty", "input": {"basis": json.loads(json.dumps(b))},
|
||||
"expected": mask(bb.summary(b))})
|
||||
q2 = fixed_basis()
|
||||
q2["outward"] = {"prior_art_questions_answered": [], "findings": ["f"], "refs": ["r"]}
|
||||
q2["dispositions"] = [{"item": "x" * 100, "disposition": "ADOPT", "why": "w", "ref": "r"},
|
||||
{"item": "short", "disposition": "ADAPT"}]
|
||||
q2["uncertainty"] = ["u1", "u2"]
|
||||
q2["risks"] = ["r1"]
|
||||
q2["implications"] = {"architecture": ["a1", "a2"], "migration": [], "verification": ["v"]}
|
||||
q2s = json.loads(json.dumps(q2))
|
||||
cases["summary"].append(
|
||||
{"name": "qualified-truncation-counts",
|
||||
"input": {"basis": q2s}, "expected": mask(bb.summary(q2))})
|
||||
b = fixed_basis(); b["implications"] = None
|
||||
cases["summary"].append(
|
||||
{"name": "implications-null", "input": {"basis": b},
|
||||
"expected": mask(bb.summary(b))})
|
||||
|
||||
# ── revise_depth ──────────────────────────────────────────────────────
|
||||
def rev_case(nm, basis, new_depth, reason, error_expected=False):
|
||||
before = json.loads(json.dumps(basis))
|
||||
if error_expected:
|
||||
try:
|
||||
bb.revise_depth(basis, new_depth=new_depth, reason=reason)
|
||||
except ValueError as e:
|
||||
cases.setdefault("revise_depth", []).append(
|
||||
{"name": nm, "input": {"basis": before, "new_depth": new_depth,
|
||||
"reason": reason}, "error": str(e)})
|
||||
return
|
||||
raise AssertionError(f"revise_depth {nm} did not raise")
|
||||
rev = bb.revise_depth(basis, new_depth=new_depth, reason=reason)
|
||||
cases.setdefault("revise_depth", []).append(
|
||||
{"name": nm, "input": {"basis": before, "new_depth": new_depth,
|
||||
"reason": reason},
|
||||
"expected": {"basis": mask(basis), "revision": mask(rev)}})
|
||||
|
||||
rev_case("full-to-inward-justified", fixed_basis(), "INWARD",
|
||||
"prior-art question resolved as non-material")
|
||||
rev_case("full-to-inward-blocked", fixed_basis(), "INWARD",
|
||||
"skip it, not relevant", error_expected=True)
|
||||
rev_case("no-op", fixed_basis("INWARD"), "INWARD", "same", error_expected=True)
|
||||
rev_case("unknown-depth", fixed_basis(), "WEIRD", "why not", error_expected=True)
|
||||
rev_case("outward-to-none-unnecessary", fixed_basis(), "NONE",
|
||||
"prior-art question unnecessary: internal solution exists")
|
||||
rev_case("upgrade-full-to-outward", fixed_basis(), "OUTWARD", "scope narrowed")
|
||||
rev_case("none-to-inward", fixed_basis("LOCAL_FIX"), "INWARD", "repo code matters")
|
||||
rev_case("reason-truncated-200", fixed_basis(), "OUTWARD", "r" * 300)
|
||||
rev_case("inward-to-none-keyword", fixed_basis("INWARD"), "NONE",
|
||||
"discovery showed nothing to record")
|
||||
|
||||
# ── to_json (Python json.dumps indent=1 sort_keys semantics) ──────────
|
||||
cases.setdefault("to_json", []).append(
|
||||
{"name": "basis", "input": {"basis": fixed_basis()},
|
||||
"expected": bb.to_json(fixed_basis())})
|
||||
uni = fixed_basis()
|
||||
uni["question"] = "héllo 日本語 🎉 quoted \"x\" back\\slash \n\t ctrl\x01"
|
||||
cases["to_json"].append(
|
||||
{"name": "unicode-escapes", "input": {"basis": uni},
|
||||
"expected": bb.to_json(uni)})
|
||||
cases["to_json"].append(
|
||||
{"name": "empty-dict", "input": {"basis": {}}, "expected": bb.to_json({})})
|
||||
mixed = {"b": 1, "a": [1, 2.5, True, None, "x"], "c": {"z": [], "y": {}},
|
||||
"é": "café", "Z": -17, "nested": {"deep": {"deeper": [{}]},
|
||||
"flag": False}}
|
||||
cases["to_json"].append(
|
||||
{"name": "mixed-sorted", "input": {"basis": mixed},
|
||||
"expected": bb.to_json(mixed)})
|
||||
|
||||
# ── from_json ─────────────────────────────────────────────────────────
|
||||
cases.setdefault("from_json", []).append(
|
||||
{"name": "object", "input": {"text": bb.to_json(fixed_basis())},
|
||||
"expected": mask(json.loads(bb.to_json(fixed_basis())))})
|
||||
cases["from_json"].append(
|
||||
{"name": "array", "input": {"text": "[1, \"two\", null, {\"k\": true}]"},
|
||||
"expected": [1, "two", None, {"k": True}]})
|
||||
try:
|
||||
bb.from_json("{bad json")
|
||||
raise AssertionError("from_json did not raise")
|
||||
except ValueError as e:
|
||||
cases["from_json"].append(
|
||||
{"name": "invalid", "input": {"text": "{bad json"},
|
||||
"error_kind": "ValueError"})
|
||||
|
||||
# ── generate_research_plan: every branch ──────────────────────────────
|
||||
for nm, kw in (
|
||||
("no-args-none", dict(intent="fix a typo in the README")),
|
||||
("unknowns-inward", dict(intent="i", unknowns=["which code path renders tables?"])),
|
||||
("outward-prior-art", dict(intent="i",
|
||||
unknowns=["is there an existing Python PDF library?"])),
|
||||
("full-consequential", dict(intent="migrate the auth schema",
|
||||
unknowns=["which user tables hold credential hashes?"],
|
||||
risks=["breaking change to login flow"])),
|
||||
("consequential-but-no-topics", dict(intent="deploy and delete the old cluster")),
|
||||
("blank-filtered", dict(intent="i", unknowns=["", " ", "real unknown u"],
|
||||
decisions=[" ", "d1"], risks=["", "r1"])),
|
||||
("decisions-and-risks-kinds", dict(intent="i", unknowns=["u1"],
|
||||
decisions=["d1"], risks=["r1"])),
|
||||
("truncation-80", dict(intent="i", unknowns=["u" * 120])),
|
||||
("depth-supplied", dict(intent="i", unknowns=["big unknown"], depth="INWARD")),
|
||||
("multiword-prior-art", dict(intent="i",
|
||||
unknowns=["prior art on vector stores?"])),
|
||||
("target-and-reality", dict(intent="i", target="wr-bridge",
|
||||
current_reality="c" * 300,
|
||||
unknowns=["u"], depth="NONE")),
|
||||
("tool-framework-keywords", dict(intent="i",
|
||||
unknowns=["which framework/tool convention applies?"])),
|
||||
):
|
||||
plan = bb.generate_research_plan(**kw)
|
||||
cases.setdefault("generate_research_plan", []).append(
|
||||
{"name": nm, "input": kw, "expected": mask(plan)})
|
||||
|
||||
truth = {
|
||||
"frozen": "build_basis full oracle truth (dichotomy wave #2479)",
|
||||
"oracle": "src/rune/build_basis.py",
|
||||
"note": ("Timestamp fields the kernel GENERATES (decided_at, created_at, "
|
||||
"revised_at, plan_generated_at) are masked on both sides — wall "
|
||||
"clock, not logic. created_at values inside basis INPUTS are "
|
||||
"fixed pass-through and compare exactly. error = the exact "
|
||||
"Python ValueError message."),
|
||||
"cases": cases,
|
||||
}
|
||||
out = Path(__file__).resolve().parents[1] / "src" / "rust" / "build_basis" / "src" / "frozen_bb_truth.json"
|
||||
out.write_text(json.dumps(truth, indent=1, sort_keys=True) + "\n")
|
||||
total = sum(len(v) for v in cases.values())
|
||||
print(f"frozen {total} cases across {len(cases)} functions -> {out}")
|
||||
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze capability_ledger oracle truth for the Rust port battery
|
||||
(dichotomy factory batch, #2479 tranche).
|
||||
|
||||
Exercises EVERY public function across all branches: CapabilityLedger
|
||||
(new/load, save, define defaults + full + duplicate, get prefixed/plain/
|
||||
unknown, material_change revisions + generation + source_locations + note
|
||||
truncation, move, supersede (plain + prefixed by, note truncation), retire,
|
||||
active, by_component, context_packet known/unknown) plus the module-level
|
||||
spec machinery (classify_version_bump full 4-bool matrix, bump_version
|
||||
valid/invalid/leading-zero/overflow, attach_spec defaults + errors,
|
||||
revise_spec classification + proposed-kind governance + unjustified bump +
|
||||
KeyError + covers_material_revision, spec_drift_status all outcomes,
|
||||
spec_context with/without spec).
|
||||
|
||||
The wall clock is FROZEN: every op records the exact _now() value it was
|
||||
given, and the kernel receives that same string via the FFI payload — the
|
||||
kernel itself never reads a clock.
|
||||
|
||||
Output: /tmp/cl_truth.json — replayed VERBATIM by
|
||||
src/rust/capability_ledger/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_LEDGER_CORE"] = "python" # freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import capability_ledger as cl # noqa: E402
|
||||
|
||||
# ── deterministic frozen clock ───────────────────────────────────────────
|
||||
_T0 = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
_tick = [0]
|
||||
|
||||
|
||||
def _next_now() -> str:
|
||||
_tick[0] += 1
|
||||
return (_T0 + timedelta(seconds=_tick[0] - 1)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _pin_clock(now: str) -> None:
|
||||
"""All _now() calls WITHIN one op return the same value (the kernel
|
||||
receives exactly one now per op; live wall-clock is same-second)."""
|
||||
cl._now = lambda n=now: n # noqa: E731
|
||||
|
||||
|
||||
class Seq:
|
||||
"""One replayable op sequence against one ledger document."""
|
||||
|
||||
def __init__(self, name: str, sequences: list):
|
||||
self.name = name
|
||||
self.ops: list[dict] = []
|
||||
self.expected: list[dict] = []
|
||||
self.saved_files: dict[str, str] = {}
|
||||
self.ledger: cl.CapabilityLedger | None = None
|
||||
self.tmpdir = tempfile.mkdtemp(prefix=f"cltruth-{name}-")
|
||||
sequences.append(self)
|
||||
|
||||
# -- op plumbing -------------------------------------------------------
|
||||
|
||||
def _call(self, opname: str, **args) -> None:
|
||||
op = {"op": opname, **args}
|
||||
now = _next_now()
|
||||
_pin_clock(now)
|
||||
if opname not in ("spec_context", "classify_version_bump",
|
||||
"bump_version"):
|
||||
op["now"] = now
|
||||
self.ops.append(op)
|
||||
try:
|
||||
out = getattr(self, f"_{opname}")(op)
|
||||
# oracle returns LIVE references (rec dicts) — snapshot NOW or
|
||||
# later ops mutate the frozen expectation
|
||||
out = json.loads(json.dumps(out))
|
||||
self.expected.append({"ok": True, "value": out})
|
||||
except Exception as e: # ValueError/SpecError/KeyError — message parity
|
||||
self.expected.append({"ok": False, "err": str(e)})
|
||||
|
||||
def _new(self, op) -> dict:
|
||||
file_text = op.get("file_text")
|
||||
if file_text is not None:
|
||||
(Path(self.tmpdir) / "capability_ledger.json").write_text(file_text)
|
||||
self.ledger = cl.CapabilityLedger(self.tmpdir)
|
||||
return self.ledger.doc
|
||||
|
||||
def _define(self, op) -> dict:
|
||||
kwargs = {k: v for k, v in op.items()
|
||||
if k not in ("op", "now")}
|
||||
return self.ledger.define(**kwargs)
|
||||
|
||||
def _get(self, op) -> dict | None:
|
||||
return self.ledger.get(op["capability_id"])
|
||||
|
||||
def _material_change(self, op) -> dict:
|
||||
kwargs = {"note": op["note"]}
|
||||
if "generation" in op:
|
||||
kwargs["generation"] = op["generation"]
|
||||
if "source_locations" in op:
|
||||
kwargs["source_locations"] = op["source_locations"]
|
||||
return self.ledger.material_change(op["capability_id"], **kwargs)
|
||||
|
||||
def _move(self, op) -> dict:
|
||||
return self.ledger.move(op["capability_id"], op["new_locations"])
|
||||
|
||||
def _supersede(self, op) -> dict:
|
||||
return self.ledger.supersede(op["capability_id"], op["by"],
|
||||
note=op.get("note", ""))
|
||||
|
||||
def _retire(self, op) -> dict:
|
||||
return self.ledger.retire(op["capability_id"],
|
||||
note=op.get("note", ""))
|
||||
|
||||
def _active(self, op) -> list:
|
||||
return self.ledger.active()
|
||||
|
||||
def _by_component(self, op) -> list:
|
||||
return self.ledger.by_component(op["component"])
|
||||
|
||||
def _context_packet(self, op) -> dict:
|
||||
return self.ledger.context_packet(op["capability_id"])
|
||||
|
||||
def _save(self, op) -> None:
|
||||
self.ledger.save()
|
||||
self.saved_files["capability_ledger.json"] = (
|
||||
Path(self.tmpdir) / "capability_ledger.json").read_text()
|
||||
return None
|
||||
|
||||
def _classify_version_bump(self, op) -> str:
|
||||
return cl.classify_version_bump(
|
||||
contract_changed=op["contract_changed"],
|
||||
behaviour_added=op["behaviour_added"],
|
||||
consumer_break=op["consumer_break"],
|
||||
implementation_only=op["implementation_only"])
|
||||
|
||||
def _bump_version(self, op) -> str:
|
||||
return cl.bump_version(op["version"], op["kind"])
|
||||
|
||||
def _attach_spec(self, op) -> dict:
|
||||
kwargs = {k: v for k, v in op.items()
|
||||
if k not in ("op", "now", "capability_id")}
|
||||
return cl.attach_spec(self.ledger, op["capability_id"], **kwargs)
|
||||
|
||||
def _revise_spec(self, op) -> dict:
|
||||
kwargs = {k: v for k, v in op.items()
|
||||
if k not in ("op", "now", "capability_id")}
|
||||
return cl.revise_spec(self.ledger, op["capability_id"], **kwargs)
|
||||
|
||||
def _spec_drift_status(self, op) -> dict:
|
||||
return cl.spec_drift_status(self.ledger, op["capability_id"])
|
||||
|
||||
def _spec_context(self, op) -> dict:
|
||||
rec = self.ledger.get(op["capability_id"])
|
||||
return cl.spec_context(rec)
|
||||
|
||||
|
||||
sequences: list[Seq] = []
|
||||
LONG_NOTE = "x" * 260
|
||||
LONG_REASON = "r" * 260
|
||||
|
||||
|
||||
def frozen_json(doc: dict) -> str:
|
||||
"""A saved ledger file EXACTLY as save() writes it (indent=1, sorted)."""
|
||||
return json.dumps(doc, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
# ── S1: fresh ledger — defaults, queries, save ───────────────────────────
|
||||
s = Seq("fresh-defaults-queries", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="auth-token", name="Auth Token",
|
||||
purpose="issue + verify session tokens", owning_project="rune",
|
||||
component="component:auth-core")
|
||||
s._call("define", capability_id="cap:prefixed-given", name="Prefixed",
|
||||
purpose="id given with prefix", owning_project="rune",
|
||||
component="component:auth-core")
|
||||
s._call("get", capability_id="auth-token")
|
||||
s._call("get", capability_id="capability:auth-token")
|
||||
s._call("get", capability_id="nope")
|
||||
s._call("active")
|
||||
s._call("by_component", component="component:auth-core")
|
||||
s._call("by_component", component="component:nowhere")
|
||||
s._call("context_packet", capability_id="auth-token")
|
||||
s._call("context_packet", capability_id="ghost")
|
||||
s._call("save")
|
||||
|
||||
# ── S2: define with every field provided ─────────────────────────────────
|
||||
s = Seq("define-full-fields", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="full-rec", name="Full", purpose="all fields",
|
||||
owning_project="rune", component="component:c1",
|
||||
source_locations=["src/a.py", "src/b.py"],
|
||||
contract="contract text", state_touched="store/scaffold",
|
||||
dependencies=["dep-x"], consumers=["consumer-y"],
|
||||
invariants=["inv-1"], behaviour_specs=["bs-1"],
|
||||
constraints="no raw tokens", generation="gen-7")
|
||||
s._call("define", capability_id="empty-lists", name="Empty", purpose="empties",
|
||||
owning_project="rune", component="component:c1",
|
||||
source_locations=[], dependencies=[], consumers=[],
|
||||
invariants=[], behaviour_specs=[])
|
||||
s._call("get", capability_id="full-rec")
|
||||
s._call("get", capability_id="empty-lists")
|
||||
s._call("context_packet", capability_id="full-rec")
|
||||
|
||||
# ── S3: duplicate define raises ──────────────────────────────────────────
|
||||
s = Seq("duplicate-define", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="dup", name="D", purpose="first",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("define", capability_id="dup", name="D2", purpose="second",
|
||||
owning_project="rune", component="component:c")
|
||||
|
||||
# ── S4: unknown-capability errors for every mutator/query ───────────────
|
||||
s = Seq("unknown-cap-errors", sequences)
|
||||
s._call("new")
|
||||
s._call("material_change", capability_id="ghost", note="n")
|
||||
s._call("move", capability_id="ghost", new_locations=["a.py"])
|
||||
s._call("supersede", capability_id="ghost", by="other")
|
||||
s._call("retire", capability_id="ghost")
|
||||
s._call("attach_spec", capability_id="ghost", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=["n"], acceptance_criteria=["a"])
|
||||
s._call("revise_spec", capability_id="ghost", reason="x")
|
||||
s._call("spec_drift_status", capability_id="ghost")
|
||||
|
||||
# ── S5: supersede / retire / move lifecycle ──────────────────────────────
|
||||
s = Seq("supersede-retire-move", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="old-cap", name="Old", purpose="p",
|
||||
owning_project="rune", component="component:c",
|
||||
source_locations=["src/old.py"])
|
||||
s._call("define", capability_id="doomed", name="Doomed", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("supersede", capability_id="old-cap", by="new-cap", note="replaced")
|
||||
s._call("supersede", capability_id="doomed", by="capability:explicit-prefixed",
|
||||
note=LONG_NOTE)
|
||||
s._call("retire", capability_id="doomed", note="dead")
|
||||
s._call("retire", capability_id="doomed", note=LONG_NOTE) # already retired: still appends history
|
||||
s._call("active")
|
||||
s._call("move", capability_id="old-cap", new_locations=["src/moved/old.py", "src/moved/old2.py"])
|
||||
s._call("context_packet", capability_id="old-cap") # packet still served for superseded
|
||||
s._call("by_component", component="component:c")
|
||||
|
||||
# ── S6: material_change revision counter + branches ─────────────────────
|
||||
s = Seq("material-change-revisions", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="rev-cap", name="Rev", purpose="p",
|
||||
owning_project="rune", component="component:c",
|
||||
source_locations=["src/a.py"], constraints="c0")
|
||||
s._call("material_change", capability_id="rev-cap", note="first change")
|
||||
s._call("material_change", capability_id="rev-cap", note=LONG_NOTE,
|
||||
generation="gen-9")
|
||||
s._call("material_change", capability_id="rev-cap", note="third",
|
||||
generation="", source_locations=["src/b.py"])
|
||||
s._call("material_change", capability_id="rev-cap", note="fourth",
|
||||
source_locations=[])
|
||||
s._call("get", capability_id="rev-cap")
|
||||
s._call("context_packet", capability_id="rev-cap")
|
||||
|
||||
# ── S7: classify_version_bump — full 4-bool matrix ───────────────────────
|
||||
s = Seq("classify-bump-matrix", sequences)
|
||||
s._call("new")
|
||||
for cc in (False, True):
|
||||
for ba in (False, True):
|
||||
for cb in (False, True):
|
||||
for io in (False, True):
|
||||
s._call("classify_version_bump", contract_changed=cc,
|
||||
behaviour_added=ba, consumer_break=cb,
|
||||
implementation_only=io)
|
||||
|
||||
# ── S8: bump_version — valid / invalid / zeros / overflow ────────────────
|
||||
s = Seq("bump-version-cases", sequences)
|
||||
s._call("new")
|
||||
for version, kind in [
|
||||
("0.1.0", "MAJOR"), ("0.1.0", "MINOR"), ("0.1.0", "PATCH"),
|
||||
("1.2.3", "MAJOR"), ("1.2.3", "MINOR"), ("1.2.3", "PATCH"),
|
||||
("0.0.9", "PATCH"), ("999.999.999", "MAJOR"), ("01.02.03", "MINOR"),
|
||||
("007.0.0", "PATCH"),
|
||||
("18446744073709551615.0.0", "MAJOR"), # u64 boundary
|
||||
("99999999999999999999999999.0.0", "PATCH"), # beyond u128
|
||||
("1.2", "PATCH"), ("1.2.3.4", "PATCH"), ("abc", "MAJOR"),
|
||||
("", "PATCH"), ("1..3", "MINOR"), ("-1.2.3", "MAJOR"),
|
||||
("1.2.x", "PATCH"), (" 1.2.3", "PATCH"), ("1.2.3 ", "PATCH"),
|
||||
("1.2.3", "BOGUS"), ("1.2.3", ""), ("1.2.3", "major"),
|
||||
]:
|
||||
s._call("bump_version", version=version, kind=kind)
|
||||
|
||||
# ── S9: attach_spec — defaults + explicit + errors ───────────────────────
|
||||
s = Seq("attach-spec-branches", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="specd", name="S", purpose="spec'd",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("spec_drift_status", capability_id="specd") # no spec attached
|
||||
s._call("attach_spec", capability_id="specd", requirements=["r1", "r2"],
|
||||
expected_behaviours=["b1"], non_goals=["ng1"],
|
||||
acceptance_criteria=["ac1"])
|
||||
s._call("spec_drift_status", capability_id="specd") # baseline covers rev 0
|
||||
s._call("define", capability_id="specd2", name="S2", purpose="x",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="specd2", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[],
|
||||
baseline_version="1.0.0", baseline_reason="stable, evidenced",
|
||||
governance_version_started_at="2025-12-31T23:59:59Z")
|
||||
s._call("get", capability_id="specd2")
|
||||
# errors
|
||||
s._call("define", capability_id="badbase", name="B", purpose="x",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="badbase", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[],
|
||||
baseline_version="1.0")
|
||||
s._call("attach_spec", capability_id="badbase", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[],
|
||||
baseline_version="not-semver")
|
||||
s._call("attach_spec", capability_id="ghost", requirements=[],
|
||||
expected_behaviours=[], non_goals=[], acceptance_criteria=[])
|
||||
|
||||
# ── S10: revise_spec — classification, governance, drift closure ─────────
|
||||
s = Seq("revise-spec-governance", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="gov", name="G", purpose="governed",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="gov", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=["n"], acceptance_criteria=["a"])
|
||||
s._call("material_change", capability_id="gov", note="uncovered rev 1")
|
||||
s._call("spec_drift_status", capability_id="gov") # DRIFT: rev 1 > covered 0
|
||||
s._call("revise_spec", capability_id="gov", implementation_only=True,
|
||||
reason="internal tidy-up")
|
||||
s._call("spec_drift_status", capability_id="gov") # drift closed
|
||||
s._call("material_change", capability_id="gov", note="rev 2")
|
||||
s._call("material_change", capability_id="gov", note="rev 3")
|
||||
s._call("spec_drift_status", capability_id="gov") # 2 uncovered
|
||||
s._call("revise_spec", capability_id="gov", behaviour_added=True,
|
||||
reason=LONG_REASON, changed_sections=["requirements"],
|
||||
affected_consumers=["consumer-x"],
|
||||
validation_consequences=["retest suite B"])
|
||||
s._call("spec_drift_status", capability_id="gov")
|
||||
s._call("revise_spec", capability_id="gov", consumer_break=True,
|
||||
reason="breaking contract change")
|
||||
s._call("spec_drift_status", capability_id="gov")
|
||||
s._call("get", capability_id="gov")
|
||||
|
||||
# ── S11: revise_spec — proposed-kind governance branches ─────────────────
|
||||
s = Seq("revise-proposed-kind", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="pk", name="PK", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("attach_spec", capability_id="pk", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=["n"], acceptance_criteria=["a"])
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="ok patch, proposed matches", proposed_kind="PATCH")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="patch proposed but minor earned", proposed_kind="MINOR")
|
||||
s._call("revise_spec", capability_id="pk", behaviour_added=True,
|
||||
reason="minor earned, downgraded to patch on purpose",
|
||||
proposed_kind="PATCH")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="major proposed for metadata churn — unjustified",
|
||||
proposed_kind="MAJOR")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="minor proposed for metadata churn — unjustified",
|
||||
proposed_kind="MINOR")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="garbage proposed kind", proposed_kind="HUGE")
|
||||
s._call("revise_spec", capability_id="pk", implementation_only=True,
|
||||
reason="empty proposed kind falls back to classify",
|
||||
proposed_kind="")
|
||||
s._call("revise_spec", capability_id="pk", reason="no flags at all — churn")
|
||||
s._call("get", capability_id="pk")
|
||||
|
||||
# ── S12: revise_spec on a capability with no spec ────────────────────────
|
||||
s = Seq("revise-no-spec", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="bare", name="B", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("revise_spec", capability_id="bare", implementation_only=True,
|
||||
reason="x")
|
||||
|
||||
# ── S13: save/reload roundtrip (sorted file bytes pinned) ────────────────
|
||||
s = Seq("save-reload-roundtrip", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="zzz-last", name="Z", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("define", capability_id="aaa-first", name="A", purpose="p",
|
||||
owning_project="rune", component="component:c")
|
||||
s._call("save")
|
||||
saved = s.saved_files["capability_ledger.json"]
|
||||
s2 = Seq("reload-from-saved-file", sequences)
|
||||
s2._call("new", file_text=saved)
|
||||
s2._call("active") # sorted-key order from the file
|
||||
s2._call("get", capability_id="aaa-first")
|
||||
s2._call("save")
|
||||
|
||||
# ── S14: unicode + repr edge cases ───────────────────────────────────────
|
||||
s = Seq("unicode-repr-edges", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="café-x", name="Café", purpose="rôle",
|
||||
owning_project="rune", component="component:coffeé")
|
||||
s._call("context_packet", capability_id="café-x")
|
||||
s._call("material_change", capability_id="café-x", note="naïve change → ünïcode ✓")
|
||||
s._call("material_change", capability_id="ghost-cap", note="x")
|
||||
s._call("bump_version", version="café.1.0", kind="PATCH")
|
||||
s._call("context_packet", capability_id="quo'te")
|
||||
s._call("context_packet", capability_id='dq"te')
|
||||
|
||||
# ── S15: spec_context with and without spec ──────────────────────────────
|
||||
s = Seq("spec-context", sequences)
|
||||
s._call("new")
|
||||
s._call("define", capability_id="ctx-bare", name="CB", purpose="purpose-y",
|
||||
owning_project="rune", component="component:c",
|
||||
contract="contract-y", invariants=["i1"], behaviour_specs=["bs1"])
|
||||
s._call("spec_context", capability_id="ctx-bare") # no spec — empty spec half
|
||||
s._call("attach_spec", capability_id="ctx-bare", requirements=["req1"],
|
||||
expected_behaviours=["eb1"], non_goals=["ngo1"],
|
||||
acceptance_criteria=["ac1"])
|
||||
s._call("spec_context", capability_id="ctx-bare")
|
||||
s._call("material_change", capability_id="ctx-bare", note="m")
|
||||
s._call("spec_context", capability_id="ctx-bare") # last_bump = BASELINE entry
|
||||
|
||||
truth = {
|
||||
"frozen": "capability_ledger full public surface + spec machinery",
|
||||
"oracle": "src/rune/capability_ledger.py",
|
||||
"note": "Every op records the exact _now() it was given; the kernel "
|
||||
"receives that string via the FFI payload and never reads a "
|
||||
"clock. saved_files pin save() byte output (Python facade "
|
||||
"serializes with indent=1 sort_keys=True).",
|
||||
"sequences": [
|
||||
{"name": s.name, "ops": s.ops, "expected": s.expected,
|
||||
"saved_files": s.saved_files,
|
||||
"final_doc": s.ledger.doc}
|
||||
for s in sequences
|
||||
],
|
||||
}
|
||||
|
||||
out = Path("/tmp/cl_truth.json")
|
||||
out.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
||||
n_ops = sum(len(s.ops) for s in sequences)
|
||||
print(f"froze {len(sequences)} sequences / {n_ops} ops -> {out}")
|
||||
@@ -0,0 +1,562 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_drift_cache_truth.py — freeze/replay parity truth for drift_cache
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/drift_cache.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/drift_cache/src/frozen_dc_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle methods delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_DC_CORE=python AND
|
||||
RUNE_MEMORY_CORE=python BEFORE any import (the module-level helpers carry
|
||||
an older _ms_core delegation chain that must never poison the truth).
|
||||
No-clock law: the wall clock is frozen by patching the datetime symbol in
|
||||
drift_cache, so every kernel payload carries its own `now` and replay is
|
||||
deterministic regardless of run date.
|
||||
|
||||
Boundary law: the sqlite read (update_from_store's per-id SELECT) and the
|
||||
cache FILE IO (_load/_save, malformed-JSON reset) stay in the Python
|
||||
facade — the kernel is a pure function of their results. The `load`
|
||||
truth group is facade-only (no kernel symbol) and is skipped by the
|
||||
kernel-level replay pass.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "drift_cache", "src", "frozen_dc_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
os.environ["RUNE_DC_CORE"] = "python" # poisoned-truth law (no-op pre-port, load-bearing post-port)
|
||||
os.environ["RUNE_MEMORY_CORE"] = "python"
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import drift_cache as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import dc_loader # noqa: E402 (kernel-level replay side)
|
||||
|
||||
FROZEN_UTC = datetime(2026, 10, 11, 12, 0, 0, 123456, tzinfo=timezone.utc)
|
||||
FROZEN_NOW = FROZEN_UTC.isoformat()
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return FROZEN_UTC
|
||||
|
||||
|
||||
oracle_mod.datetime = FrozenDatetime
|
||||
|
||||
|
||||
class Case:
|
||||
def __init__(self, name, fn):
|
||||
self.name = name
|
||||
self.fn = fn
|
||||
|
||||
|
||||
def fresh_cache(entries_pre=None):
|
||||
tmp = tempfile.mkdtemp(prefix="dc_truth_")
|
||||
oracle_mod.CACHE_DIR = tmp
|
||||
oracle_mod.CACHE_FILE = os.path.join(tmp, "drift_cache.json")
|
||||
cache = oracle_mod.DriftCache()
|
||||
if entries_pre is not None:
|
||||
cache.entries = copy.deepcopy(entries_pre)
|
||||
return cache
|
||||
|
||||
|
||||
def stamped(priority, wash=0, rock="washable", access_count=0):
|
||||
return {"priority": priority, "added_at": FROZEN_NOW, "last_accessed": FROZEN_NOW,
|
||||
"access_count": access_count, "wash_count": wash, "rock_status": rock}
|
||||
|
||||
|
||||
# ── compute_priority ─────────────────────────────────────────────────────────
|
||||
|
||||
def c_prio_default(rec):
|
||||
rec("prio-default-empty-entry", {"entry": {}}, oracle_mod.compute_priority({}),
|
||||
{"priority": oracle_mod.compute_priority({})})
|
||||
|
||||
|
||||
def c_prio_wash20_imp1(rec):
|
||||
e = {"wash_count": 20, "importance": 1.0}
|
||||
rec("prio-wash20-imp1-washfactor-1", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_typical(rec):
|
||||
e = {"wash_count": 7, "importance": 0.6}
|
||||
rec("prio-wash7-imp06", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_rock(rec):
|
||||
e = {"wash_count": 3, "importance": 0.7, "rock_status": "rock"}
|
||||
rec("prio-rock-boost", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_established(rec):
|
||||
e = {"wash_count": 3, "importance": 0.7, "rock_status": "established"}
|
||||
rec("prio-established-boost", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_none_wash(rec):
|
||||
e = {"wash_count": None, "importance": 0.9}
|
||||
rec("prio-none-wash-coerced-0", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_none_imp(rec):
|
||||
e = {"wash_count": 4, "importance": None}
|
||||
rec("prio-none-imp-coerced-05", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_zero_imp(rec):
|
||||
# falsy 0 → `or 0.5` coerces to 0.5
|
||||
e = {"wash_count": 4, "importance": 0}
|
||||
rec("prio-zero-imp-coerced-05", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_int_importance(rec):
|
||||
e = {"wash_count": 2, "importance": 1}
|
||||
rec("prio-int-importance", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_wash_beyond_20(rec):
|
||||
# log1p scaling caps at 1.0 via min()
|
||||
e = {"wash_count": 1000, "importance": 0.5}
|
||||
rec("prio-wash1000-capped", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
def c_prio_unknown_status(rec):
|
||||
e = {"wash_count": 2, "importance": 0.5, "rock_status": "muddy"}
|
||||
rec("prio-unknown-status-no-boost", {"entry": e}, oracle_mod.compute_priority(e),
|
||||
{"priority": oracle_mod.compute_priority(e)})
|
||||
|
||||
|
||||
# ── qualifies_for_cache ──────────────────────────────────────────────────────
|
||||
|
||||
def _q_case(rec, name, entry):
|
||||
rec(name, {"entry": entry}, oracle_mod.qualifies_for_cache(entry),
|
||||
{"qualifies": oracle_mod.qualifies_for_cache(entry)})
|
||||
|
||||
|
||||
def c_qual_below_both(rec):
|
||||
_q_case(rec, "qual-wash0-imp05-denied", {"wash_count": 0, "importance": 0.5})
|
||||
|
||||
|
||||
def c_qual_imp_boundary(rec):
|
||||
_q_case(rec, "qual-imp-079-denied", {"wash_count": 0, "importance": 0.79})
|
||||
|
||||
|
||||
def c_qual_imp_08_admitted(rec):
|
||||
_q_case(rec, "qual-imp-08-admitted", {"wash_count": 0, "importance": 0.8})
|
||||
|
||||
|
||||
def c_qual_wash_boundary(rec):
|
||||
_q_case(rec, "qual-wash1-imp0-admitted", {"wash_count": 1, "importance": 0.0})
|
||||
|
||||
|
||||
def c_qual_defaults(rec):
|
||||
_q_case(rec, "qual-defaults-denied", {})
|
||||
|
||||
|
||||
def c_qual_none_fields(rec):
|
||||
_q_case(rec, "qual-none-fields-denied", {"wash_count": None, "importance": None})
|
||||
|
||||
|
||||
def c_qual_proven(rec):
|
||||
_q_case(rec, "qual-wash5-admitted", {"wash_count": 5, "importance": 0.3})
|
||||
|
||||
|
||||
# ── add ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _add_case(rec, name, entries_pre, entry_id, entry):
|
||||
cache = fresh_cache(entries_pre)
|
||||
cache.add(entry_id, entry)
|
||||
admitted = entry_id in cache.entries
|
||||
evicted = sorted(set(entries_pre) - set(cache.entries))
|
||||
rec(name,
|
||||
{"entries": entries_pre, "entry_id": entry_id, "entry": entry, "now": FROZEN_NOW},
|
||||
None, {"entries": cache.entries, "admitted": admitted, "evicted": evicted})
|
||||
|
||||
|
||||
def c_add_denied(rec):
|
||||
_add_case(rec, "add-denied-unproven-unimportant", {}, 1,
|
||||
{"wash_count": 0, "importance": 0.5})
|
||||
|
||||
|
||||
def c_add_denied_important_but_unwashed(rec):
|
||||
_add_case(rec, "add-denied-imp-below-08", {}, 2,
|
||||
{"wash_count": 0, "importance": 0.5, "text": "x"})
|
||||
|
||||
|
||||
def c_add_washable(rec):
|
||||
_add_case(rec, "add-washable-admitted", {}, 3,
|
||||
{"wash_count": 2, "importance": 0.5, "rock_status": "washable"})
|
||||
|
||||
|
||||
def c_add_high_importance_unwashed(rec):
|
||||
_add_case(rec, "add-important-unwashed-admitted", {}, 4,
|
||||
{"wash_count": 0, "importance": 0.95})
|
||||
|
||||
|
||||
def c_add_rock(rec):
|
||||
_add_case(rec, "add-rock-stamped", {}, 5,
|
||||
{"wash_count": 3, "importance": 0.7, "rock_status": "rock"})
|
||||
|
||||
|
||||
def c_add_overwrites(rec):
|
||||
pre = {7: stamped(0.9, wash=9, rock="rock", access_count=4)}
|
||||
_add_case(rec, "add-overwrites-existing", pre, 7,
|
||||
{"wash_count": 1, "importance": 0.4})
|
||||
|
||||
|
||||
def c_add_rock_status_none(rec):
|
||||
# explicit None rock_status is stored verbatim (entry.get returns None)
|
||||
_add_case(rec, "add-rock-status-none-verbatim", {}, 8,
|
||||
{"wash_count": 2, "importance": 0.5, "rock_status": None})
|
||||
|
||||
|
||||
def c_add_boundary_200_no_evict(rec):
|
||||
pre = {i: {"priority": (i + 1) / 200.0} for i in range(199)}
|
||||
_add_case(rec, "add-at-200-no-evict", pre, 199,
|
||||
{"wash_count": 20, "importance": 1.0})
|
||||
|
||||
|
||||
def c_add_evict_lowest_20(rec):
|
||||
pre = {i: {"priority": (i + 1) / 200.0} for i in range(200)}
|
||||
_add_case(rec, "add-evicts-lowest-20", pre, 999,
|
||||
{"wash_count": 20, "importance": 1.0, "rock_status": "rock"})
|
||||
|
||||
|
||||
def c_add_evict_tie_stable_order(rec):
|
||||
pre = {i: {"priority": 0.5} for i in range(196)}
|
||||
for i in range(196, 200):
|
||||
pre[i] = {"priority": 9.0}
|
||||
_add_case(rec, "add-evict-tie-stable-insertion-order", pre, 999,
|
||||
{"wash_count": 20, "importance": 1.0})
|
||||
|
||||
|
||||
# ── access ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _access_case(rec, name, entries_pre, entry_id):
|
||||
cache = fresh_cache(entries_pre)
|
||||
cache.access(entry_id)
|
||||
changed = entry_id in entries_pre
|
||||
rec(name,
|
||||
{"entries": entries_pre, "entry_id": entry_id, "now": FROZEN_NOW},
|
||||
None, {"entries": cache.entries, "changed": changed})
|
||||
|
||||
|
||||
def c_access_hit(rec):
|
||||
pre = {11: stamped(0.5, access_count=3)}
|
||||
_access_case(rec, "access-hit-reinforces", pre, 11)
|
||||
|
||||
|
||||
def c_access_miss(rec):
|
||||
pre = {11: stamped(0.5)}
|
||||
_access_case(rec, "access-miss-noop", pre, 12)
|
||||
|
||||
|
||||
def c_access_zero_count(rec):
|
||||
pre = {13: stamped(0.5, access_count=0)}
|
||||
_access_case(rec, "access-from-zero", pre, 13)
|
||||
|
||||
|
||||
# ── contains / get_priority ──────────────────────────────────────────────────
|
||||
|
||||
def c_contains_hit(rec):
|
||||
pre = {21: stamped(0.5)}
|
||||
cache = fresh_cache(pre)
|
||||
rec("contains-hit", {"entries": pre, "entry_id": 21},
|
||||
cache.contains(21), {"contains": cache.contains(21)})
|
||||
|
||||
|
||||
def c_contains_miss(rec):
|
||||
cache = fresh_cache({})
|
||||
rec("contains-miss", {"entries": {}, "entry_id": 21},
|
||||
cache.contains(21), {"contains": cache.contains(21)})
|
||||
|
||||
|
||||
def c_getprio_hit(rec):
|
||||
pre = {31: stamped(0.625)}
|
||||
cache = fresh_cache(pre)
|
||||
rec("getprio-hit", {"entries": pre, "entry_id": 31},
|
||||
cache.get_priority(31), {"priority": cache.get_priority(31)})
|
||||
|
||||
|
||||
def c_getprio_miss(rec):
|
||||
cache = fresh_cache({})
|
||||
rec("getprio-miss-zero", {"entries": {}, "entry_id": 31},
|
||||
cache.get_priority(31), {"priority": cache.get_priority(31)})
|
||||
|
||||
|
||||
# ── update_from_store ────────────────────────────────────────────────────────
|
||||
|
||||
def make_db(rows):
|
||||
db = sqlite3.connect(":memory:")
|
||||
db.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, wash_count INTEGER,"
|
||||
" importance REAL, rock_status TEXT)")
|
||||
db.executemany("INSERT INTO entries VALUES (?,?,?,?)", rows)
|
||||
return db
|
||||
|
||||
|
||||
def _sync_case(rec, name, entries_pre, db_rows):
|
||||
db = make_db(db_rows)
|
||||
cache = fresh_cache(entries_pre)
|
||||
rows = {}
|
||||
for entry_id in list(cache.entries.keys()):
|
||||
row = db.execute(
|
||||
"SELECT wash_count, importance, rock_status FROM entries WHERE id = ?",
|
||||
(entry_id,)).fetchone()
|
||||
rows[str(entry_id)] = list(row) if row else None
|
||||
cache.update_from_store(db)
|
||||
rec(name, {"entries": entries_pre, "rows": rows}, None,
|
||||
{"entries": cache.entries})
|
||||
|
||||
|
||||
def c_sync_updates_priority(rec):
|
||||
pre = {41: stamped(0.1, wash=1)}
|
||||
_sync_case(rec, "sync-updates-priority-wash-rock", pre,
|
||||
[(41, 15, 0.8, "rock")])
|
||||
|
||||
|
||||
def c_sync_null_fields(rec):
|
||||
pre = {42: stamped(0.9, wash=5, rock="rock")}
|
||||
_sync_case(rec, "sync-null-row-fields-coerced", pre,
|
||||
[(42, None, None, None)])
|
||||
|
||||
|
||||
def c_sync_deleted_row_removed(rec):
|
||||
pre = {43: stamped(0.5), 44: stamped(0.7)}
|
||||
_sync_case(rec, "sync-deleted-row-removed", pre,
|
||||
[(44, 2, 0.5, "washable")])
|
||||
|
||||
|
||||
def c_sync_empty_cache(rec):
|
||||
_sync_case(rec, "sync-empty-cache-noop", {}, [])
|
||||
|
||||
|
||||
def c_sync_downgrade(rec):
|
||||
pre = {45: stamped(1.0, wash=20, rock="rock")}
|
||||
_sync_case(rec, "sync-downgrade-washable", pre,
|
||||
[(45, 0, 0.1, "washable")])
|
||||
|
||||
|
||||
# ── get_stats ────────────────────────────────────────────────────────────────
|
||||
|
||||
def _stats_case(rec, name, entries_pre):
|
||||
cache = fresh_cache(entries_pre)
|
||||
stats = cache.get_stats()
|
||||
rec(name, {"entries": entries_pre}, stats, {"stats": stats})
|
||||
|
||||
|
||||
def c_stats_empty(rec):
|
||||
_stats_case(rec, "stats-empty", {})
|
||||
|
||||
|
||||
def c_stats_mixed(rec):
|
||||
pre = {
|
||||
51: stamped(0.2, rock="rock"),
|
||||
52: stamped(0.5, rock="established"),
|
||||
53: stamped(0.9),
|
||||
}
|
||||
_stats_case(rec, "stats-mixed", pre)
|
||||
|
||||
|
||||
def c_stats_all_washable(rec):
|
||||
pre = {54: stamped(0.3), 55: stamped(0.4)}
|
||||
_stats_case(rec, "stats-all-washable", pre)
|
||||
|
||||
|
||||
def c_stats_missing_priority(rec):
|
||||
pre = {56: {"rock_status": "rock"}, 57: stamped(0.8)}
|
||||
_stats_case(rec, "stats-missing-priority-zero", pre)
|
||||
|
||||
|
||||
def c_stats_missing_rock_status(rec):
|
||||
pre = {58: {"priority": 0.5}}
|
||||
_stats_case(rec, "stats-missing-rock-status-washable", pre)
|
||||
|
||||
|
||||
# ── _load branches (facade-only: FILE IO stays in Python) ────────────────────
|
||||
|
||||
def c_load_missing(rec):
|
||||
cache = fresh_cache()
|
||||
rec("load-missing-file-empty", {"path": "missing"}, None,
|
||||
{"entries": cache.entries})
|
||||
|
||||
|
||||
def c_load_valid(rec):
|
||||
tmp = tempfile.mkdtemp(prefix="dc_truth_")
|
||||
oracle_mod.CACHE_DIR = tmp
|
||||
oracle_mod.CACHE_FILE = os.path.join(tmp, "drift_cache.json")
|
||||
pre = {61: stamped(0.5), 62: stamped(0.9, rock="rock")}
|
||||
with open(oracle_mod.CACHE_FILE, "w") as f:
|
||||
json.dump({str(k): v for k, v in pre.items()}, f, indent=2)
|
||||
cache = oracle_mod.DriftCache()
|
||||
rec("load-valid-int-keys", {"path": "valid"}, None,
|
||||
{"entries": cache.entries})
|
||||
|
||||
|
||||
def c_load_malformed(rec):
|
||||
tmp = tempfile.mkdtemp(prefix="dc_truth_")
|
||||
oracle_mod.CACHE_DIR = tmp
|
||||
oracle_mod.CACHE_FILE = os.path.join(tmp, "drift_cache.json")
|
||||
with open(oracle_mod.CACHE_FILE, "w") as f:
|
||||
f.write("{not json")
|
||||
cache = oracle_mod.DriftCache()
|
||||
rec("load-malformed-reset", {"path": "malformed"}, None,
|
||||
{"entries": cache.entries})
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
PRIO = [c_prio_default, c_prio_wash20_imp1, c_prio_typical, c_prio_rock,
|
||||
c_prio_established, c_prio_none_wash, c_prio_none_imp, c_prio_zero_imp,
|
||||
c_prio_int_importance, c_prio_wash_beyond_20, c_prio_unknown_status]
|
||||
QUAL = [c_qual_below_both, c_qual_imp_boundary, c_qual_imp_08_admitted,
|
||||
c_qual_wash_boundary, c_qual_defaults, c_qual_none_fields, c_qual_proven]
|
||||
ADD = [c_add_denied, c_add_denied_important_but_unwashed, c_add_washable,
|
||||
c_add_high_importance_unwashed, c_add_rock, c_add_overwrites,
|
||||
c_add_rock_status_none, c_add_boundary_200_no_evict,
|
||||
c_add_evict_lowest_20, c_add_evict_tie_stable_order]
|
||||
ACCESS = [c_access_hit, c_access_miss, c_access_zero_count]
|
||||
CONTAINS = [c_contains_hit, c_contains_miss]
|
||||
GETPRIO = [c_getprio_hit, c_getprio_miss]
|
||||
SYNC = [c_sync_updates_priority, c_sync_null_fields, c_sync_deleted_row_removed,
|
||||
c_sync_empty_cache, c_sync_downgrade]
|
||||
STATS = [c_stats_empty, c_stats_mixed, c_stats_all_washable,
|
||||
c_stats_missing_priority, c_stats_missing_rock_status]
|
||||
LOAD = [c_load_missing, c_load_valid, c_load_malformed]
|
||||
|
||||
GROUPS = [("compute_priority", PRIO), ("qualifies_for_cache", QUAL),
|
||||
("add", ADD), ("access", ACCESS), ("contains", CONTAINS),
|
||||
("get_priority", GETPRIO), ("update_entries", SYNC),
|
||||
("get_stats", STATS), ("load", LOAD)]
|
||||
|
||||
# kernel symbol per group (None → facade-only group, skipped kernel-side)
|
||||
KERNEL = {
|
||||
"compute_priority": "dcc_compute_priority",
|
||||
"qualifies_for_cache": "dcc_qualifies_for_cache",
|
||||
"add": "dcc_add",
|
||||
"access": "dcc_access",
|
||||
"contains": "dcc_contains",
|
||||
"get_priority": "dcc_get_priority",
|
||||
"update_entries": "dcc_update_entries",
|
||||
"get_stats": "dcc_get_stats",
|
||||
"load": None,
|
||||
}
|
||||
|
||||
# kernel-level replay: which slice of `expected` the unwrapped FFI value is
|
||||
UNWRAP = {
|
||||
"compute_priority": lambda w: w["priority"],
|
||||
"qualifies_for_cache": lambda w: w["qualifies"],
|
||||
"add": lambda w: w,
|
||||
"access": lambda w: w,
|
||||
"contains": lambda w: w["contains"],
|
||||
"get_priority": lambda w: w["priority"],
|
||||
"update_entries": lambda w: w["entries"],
|
||||
"get_stats": lambda w: w["stats"],
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization: int dict keys become strings through the
|
||||
JSON round-trip (the frozen file stores string keys), then a sorted
|
||||
dump makes the comparison key-order independent."""
|
||||
return json.dumps(json.loads(json.dumps(x)), sort_keys=True)
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, oracle_ret, expected, _r=recs):
|
||||
entry = {"name": name, "input": inp, "return": oracle_ret,
|
||||
"expected": expected}
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_failures = sum(1 for v in groups.values() for c in v if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
checked = 0
|
||||
for group, cases in frozen.items():
|
||||
sym = KERNEL[group]
|
||||
if sym is None:
|
||||
continue
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = getattr(dc_loader, group)(**copy.deepcopy(case["input"]))
|
||||
want = UNWRAP[group](case["expected"])
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {checked - failures}/{checked}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/drift_cache.py",
|
||||
"pin": "RUNE_DC_CORE=python + RUNE_MEMORY_CORE=python",
|
||||
"note": "Frozen via scripts/gen_drift_cache_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_NOW +
|
||||
". Battery embeds this VERBATIM. Group `load` is facade-only "
|
||||
"(FILE IO stays in Python).",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
# NO sort_keys: the entries maps are Python dicts whose INSERTION
|
||||
# order is load-bearing (stable eviction tie-breaks) — sorting
|
||||
# keys here would freeze a re-ordered kernel input.
|
||||
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,599 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_evidence_probe_truth.py — evidence_probe FULL-port truth battery
|
||||
(dichotomy wave #2479).
|
||||
|
||||
Freezes the LIVE src/rune/evidence_probe.py behavior (validate_probe and
|
||||
count_probes served by the Rust evidence_provenance core through ep_loader)
|
||||
for every public function and branch:
|
||||
|
||||
- build_probe_prompt: baseline / unicode / path-like question / empty args
|
||||
- validate_probe: the 5-key schema gate — missing fields, non-string and
|
||||
blank fields, status rendering (repr, ints, floats, bools, None, list,
|
||||
dict incl. nested key order), GROUNDED source-citation boundary (7/8
|
||||
chars, missing/non-string source), extra-field sorting (meta fields
|
||||
allowed), non-object artifacts, combined-error ordering
|
||||
- count_probes: absent dir/file, empty, blank-only, Rust `lines()`
|
||||
semantics (trailing \\r, no trailing newline, trailing newline,
|
||||
whitespace-only lines)
|
||||
- check_budget: 0/1 ok, 2/3 ProbeBudgetExceeded
|
||||
- record_probe: fresh dir creation, probe.jsonl append, unicode
|
||||
ensure_ascii bytes, returned path
|
||||
- run_probe: injected runner — success (grounded / still_unknown), model
|
||||
fallback chain (explicit / MIMO_MODEL env / unset -> UNKNOWN /
|
||||
set-but-empty -> ""), budget exhausted BEFORE the runner fires, silent
|
||||
runner -> declared-artifact failure, gate-failure joins, and the exact
|
||||
prompt + absolute artifact path the runner receives
|
||||
- ProbeResult.to_dict: field order + ensure_ascii
|
||||
- _default_runner: stubbed MIMO_BIN subprocess — argv parity, stdout log,
|
||||
exit-code error, MIMO_AGENT default/override, require_model
|
||||
resolution + ProviderUnresolved (set/unset/blank)
|
||||
|
||||
Clock frozen at FROZEN_TS (probe_id + decided_at deterministic). The only
|
||||
excluded branch is the invalid-JSON artifact crash (Python json.JSONDecodeError
|
||||
rendering is interpreter-specific, same policy as the auto_context batch).
|
||||
|
||||
Usage: python3 scripts/gen_evidence_probe_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import evidence_probe as ep # noqa: E402
|
||||
import rune.provider_context as provider_context # noqa: E402
|
||||
|
||||
FROZEN_TS = 1791753600
|
||||
ARTDIR = "@ARTDIR@"
|
||||
STUB_NAME = "ep_stub.sh"
|
||||
|
||||
_real_gmtime = time.gmtime
|
||||
|
||||
GROUNDED = {
|
||||
"question": "Does the API support streaming?",
|
||||
"source": "https://platformdirs.readthedocs.io/changelog.html",
|
||||
"answer": "Current version guarantees the assumed behaviour.",
|
||||
"implication": "Local wiring is sufficient; no version guard needed.",
|
||||
"status": "GROUNDED",
|
||||
}
|
||||
|
||||
|
||||
def norm(s, base):
|
||||
return s.replace(base, ARTDIR) if isinstance(s, str) else s
|
||||
|
||||
|
||||
def write_files(cdir, files):
|
||||
os.makedirs(cdir, exist_ok=True)
|
||||
for rel, content in (files or {}).items():
|
||||
p = os.path.join(cdir, rel)
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
with open(p, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def exc_row(fn, base, types=None):
|
||||
"""Call fn(), freeze either {error: None, value: ...} or the exception."""
|
||||
try:
|
||||
return {"error": None, "value": fn()}
|
||||
except Exception as e: # noqa: BLE001 — the frozen point IS the exception
|
||||
t = type(e).__name__
|
||||
if types and t not in types:
|
||||
raise
|
||||
return {"error": {"type": t, "msg": norm(str(e), base)}}
|
||||
|
||||
|
||||
def main():
|
||||
base = tempfile.mkdtemp(prefix="ep_truth_")
|
||||
rows = []
|
||||
real_time = time.time
|
||||
ep.time.time = lambda: FROZEN_TS
|
||||
ep.time.gmtime = lambda *a: _real_gmtime(FROZEN_TS)
|
||||
try:
|
||||
_build_rows(base, rows)
|
||||
finally:
|
||||
ep.time.time = real_time
|
||||
ep.time.gmtime = _real_gmtime
|
||||
|
||||
def _build_rows(base, rows):
|
||||
# ── build_probe_prompt ────────────────────────────────────────────────
|
||||
prompt_cases = [
|
||||
("prompt_baseline", "Does the API support streaming?", "/tmp/probe.json"),
|
||||
("prompt_unicode", "Wird die Größenbeschränkung überschritten?", "/tmp/probe.json"),
|
||||
(
|
||||
"prompt_path_like_question",
|
||||
"Is the file at /tmp/probe.json overwritten per probe?",
|
||||
"/tmp/artifacts/evidence_probe.json",
|
||||
),
|
||||
("prompt_empty_question", "", ""),
|
||||
("prompt_multiline_question", "line one\nline two\nline three", "/tmp/x.json"),
|
||||
]
|
||||
for name, q, ap in prompt_cases:
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "prompt",
|
||||
"setup": {"question": q, "artifact_path": ap},
|
||||
"want": ep.build_probe_prompt(q, ap),
|
||||
}
|
||||
)
|
||||
|
||||
# ── validate_probe ────────────────────────────────────────────────────
|
||||
def val(name, artifact):
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "validate",
|
||||
"setup": {"artifact": artifact},
|
||||
"want": ep.validate_probe(artifact),
|
||||
}
|
||||
)
|
||||
|
||||
val("valid_grounded", dict(GROUNDED))
|
||||
val(
|
||||
"valid_grounded_with_meta",
|
||||
dict(GROUNDED, probe_id="probe-1", decided_at="2026-10-11T00:00:00Z", model="m"),
|
||||
)
|
||||
val(
|
||||
"valid_still_unknown_empty_source",
|
||||
{
|
||||
"question": "probe",
|
||||
"source": "",
|
||||
"answer": "Could not find authoritative evidence.",
|
||||
"implication": "Uncertainty remains.",
|
||||
"status": "STILL_UNKNOWN",
|
||||
},
|
||||
)
|
||||
val(
|
||||
"valid_contradicted",
|
||||
dict(GROUNDED, status="CONTRADICTED", source="task assumption"),
|
||||
)
|
||||
for field in ("question", "source", "answer", "implication", "status"):
|
||||
art = dict(GROUNDED)
|
||||
del art[field]
|
||||
val(f"missing_{field}", art)
|
||||
val("empty_artifact", {})
|
||||
val("nonstring_question", dict(GROUNDED, question=7))
|
||||
val("nonstring_answer", dict(GROUNDED, answer=["a"]))
|
||||
val("nonstring_implication", dict(GROUNDED, implication={"k": 1}))
|
||||
val("nonstring_source", dict(GROUNDED, source=3.5))
|
||||
val("blank_question", dict(GROUNDED, question=""))
|
||||
val("whitespace_question", dict(GROUNDED, question=" \t\n"))
|
||||
val("blank_answer", dict(GROUNDED, answer=""))
|
||||
val("blank_implication", dict(GROUNDED, implication=""))
|
||||
val("blank_source_allowed", dict(GROUNDED, source=""))
|
||||
val("status_empty_string", dict(GROUNDED, status=""))
|
||||
val("status_lowercase", dict(GROUNDED, status="grounded"))
|
||||
val("status_int", dict(GROUNDED, status=1))
|
||||
val("status_float", dict(GROUNDED, status=1.5))
|
||||
val("status_float_integral", dict(GROUNDED, status=2.0))
|
||||
val("status_float_small", dict(GROUNDED, status=0.1))
|
||||
val("status_big_float", dict(GROUNDED, status=1e30))
|
||||
val("status_huge_int", dict(GROUNDED, status=9007199254740993))
|
||||
val("status_true", dict(GROUNDED, status=True))
|
||||
val("status_false", dict(GROUNDED, status=False))
|
||||
val("status_null", dict(GROUNDED, status=None))
|
||||
val("status_list", dict(GROUNDED, status=[1, "x"]))
|
||||
val("status_obj_order", dict(GROUNDED, status={"b": 1, "a": {"z": 2, "y": 3}}))
|
||||
val("status_repr_quote", dict(GROUNDED, status="it's"))
|
||||
val("status_repr_newline", dict(GROUNDED, status="a\nb"))
|
||||
val("status_repr_unicode", dict(GROUNDED, status="größe"))
|
||||
val("grounded_short_source_7", dict(GROUNDED, source="1234567"))
|
||||
val("grounded_source_8_ok", dict(GROUNDED, source="12345678"))
|
||||
val("grounded_missing_source", {k: v for k, v in GROUNDED.items() if k != "source"})
|
||||
val("grounded_nonstring_source", dict(GROUNDED, source=42))
|
||||
val("extra_single", dict(GROUNDED, extra=1))
|
||||
val("extra_sorted", dict(GROUNDED, z_extra=1, a_extra=2, m_extra=3))
|
||||
val("extra_meta_allowed", dict(GROUNDED, model="m", probe_id="p", decided_at="d"))
|
||||
val("extra_unicode_key", dict(GROUNDED, **{"ключ": 1}))
|
||||
val("nonobject_list", ["a", 1])
|
||||
val("nonobject_string", "nope")
|
||||
val("nonobject_int", 3)
|
||||
val("nonobject_bool", True)
|
||||
val("nonobject_null", None)
|
||||
val(
|
||||
"combined_ordering",
|
||||
{"status": 5, "extra_b": 1, "extra_a": 2, "answer": ""},
|
||||
)
|
||||
|
||||
# ── count_probes ──────────────────────────────────────────────────────
|
||||
count_cases = [
|
||||
("count_absent_dir", None),
|
||||
("count_absent_file", {}),
|
||||
("count_empty_file", {"probe.jsonl": ""}),
|
||||
("count_blank_only", {"probe.jsonl": "\n\n \n"}),
|
||||
("count_three_lines", {"probe.jsonl": "a\nb\nc\n"}),
|
||||
("count_no_trailing_newline", {"probe.jsonl": "a\nb"}),
|
||||
("count_interleaved_blank", {"probe.jsonl": "a\n\nb\n\n\nc"}),
|
||||
("count_cr_lines", {"probe.jsonl": "a\r\nb\r\n"}),
|
||||
("count_whitespace_lines", {"probe.jsonl": " \n x\n\t\n"}),
|
||||
("count_cr_only_line", {"probe.jsonl": "\r\r\nx"}),
|
||||
("count_single_line", {"probe.jsonl": "one"}),
|
||||
]
|
||||
for name, files in count_cases:
|
||||
cdir = os.path.join(base, name)
|
||||
if files is not None:
|
||||
write_files(cdir, files)
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "count",
|
||||
"setup": {"dir": norm(cdir, base), "files": files or {}},
|
||||
"want": ep.count_probes(cdir),
|
||||
}
|
||||
)
|
||||
|
||||
# ── check_budget ──────────────────────────────────────────────────────
|
||||
for n, name in ((0, "budget_zero"), (1, "budget_one"), (2, "budget_two"), (3, "budget_three")):
|
||||
cdir = os.path.join(base, name)
|
||||
write_files(cdir, {"probe.jsonl": "x\n" * n})
|
||||
out = exc_row(lambda d=cdir: ep.check_budget(d), base, types={"ProbeBudgetExceeded"})
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "budget",
|
||||
"setup": {"dir": norm(cdir, base), "files": {"probe.jsonl": "x\n" * n}},
|
||||
"want": out,
|
||||
}
|
||||
)
|
||||
|
||||
# ── record_probe ──────────────────────────────────────────────────────
|
||||
def rec(name, result, files=None):
|
||||
cdir = os.path.join(base, name)
|
||||
if files:
|
||||
write_files(cdir, files)
|
||||
path = ep.record_probe(ep.ProbeResult(**result), cdir)
|
||||
with open(os.path.join(cdir, "probe.json")) as f:
|
||||
pj = f.read()
|
||||
with open(os.path.join(cdir, "probe.jsonl")) as f:
|
||||
pjl = f.read()
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "record",
|
||||
"setup": {"result": result, "files": files or {}},
|
||||
"want": {
|
||||
"path": norm(path, base),
|
||||
"probe_json": pj,
|
||||
"probe_jsonl": pjl,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
rec(
|
||||
"record_fresh_dir_created",
|
||||
{
|
||||
"question": "q",
|
||||
"source": "https://example.com/a",
|
||||
"answer": "a",
|
||||
"implication": "i",
|
||||
"status": "GROUNDED",
|
||||
"probe_id": "probe-1791753600",
|
||||
"decided_at": "2026-10-12T04:00:00Z",
|
||||
"model": "UNKNOWN",
|
||||
},
|
||||
)
|
||||
rec(
|
||||
"record_appends_existing",
|
||||
{
|
||||
"question": "q2",
|
||||
"source": "",
|
||||
"answer": "a2",
|
||||
"implication": "i2",
|
||||
"status": "STILL_UNKNOWN",
|
||||
"probe_id": "probe-1791753600",
|
||||
"decided_at": "2026-10-12T04:00:00Z",
|
||||
"model": "m",
|
||||
},
|
||||
files={"probe.jsonl": '{"question": "old"}\n'},
|
||||
)
|
||||
rec(
|
||||
"record_unicode_ensure_ascii",
|
||||
{
|
||||
"question": "Größe überschritten — warum?",
|
||||
"source": "https://example.com/ä",
|
||||
"answer": "ja ✓",
|
||||
"implication": "🚀",
|
||||
"status": "GROUNDED",
|
||||
"probe_id": "probe-1791753600",
|
||||
"decided_at": "2026-10-12T04:00:00Z",
|
||||
"model": "model-ü",
|
||||
},
|
||||
)
|
||||
|
||||
# ── run_probe ─────────────────────────────────────────────────────────
|
||||
def run(name, *, question, runner_spec, model="UNKNOWN", env=None, files=None):
|
||||
cdir = os.path.join(base, name)
|
||||
write_files(cdir, files or {})
|
||||
old_env = {k: os.environ.get(k) for k in ("MIMO_MODEL",)}
|
||||
try:
|
||||
for k, v in (env or {}).items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
captures = []
|
||||
|
||||
def runner(prompt, artifact_path):
|
||||
captures.append((prompt, artifact_path))
|
||||
kind = runner_spec["type"]
|
||||
if kind == "write":
|
||||
with open(artifact_path, "w") as f:
|
||||
json.dump(runner_spec["artifact"], f)
|
||||
elif kind == "silent":
|
||||
pass
|
||||
else:
|
||||
raise AssertionError(f"unknown runner spec {kind}")
|
||||
|
||||
try:
|
||||
result = ep.run_probe(question, cdir, runner=runner, model=model)
|
||||
out = {
|
||||
"error": None,
|
||||
"result": result.to_dict(),
|
||||
}
|
||||
with open(os.path.join(cdir, "probe.json")) as f:
|
||||
out["probe_json"] = f.read()
|
||||
with open(os.path.join(cdir, "probe.jsonl")) as f:
|
||||
out["probe_jsonl"] = f.read()
|
||||
except Exception as e: # noqa: BLE001
|
||||
out = {"error": {"type": type(e).__name__, "msg": norm(str(e), base)}}
|
||||
out["runner_called"] = bool(captures)
|
||||
if captures:
|
||||
out["prompt"] = norm(captures[0][0], base)
|
||||
out["artifact_path"] = norm(captures[0][1], base)
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "run",
|
||||
"setup": {
|
||||
"question": question,
|
||||
"model": model,
|
||||
"env": env or {},
|
||||
"files": files or {},
|
||||
"runner": runner_spec,
|
||||
},
|
||||
"want": out,
|
||||
}
|
||||
)
|
||||
finally:
|
||||
for k, v in old_env.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
|
||||
STILL_UNKNOWN = {
|
||||
"question": "probe",
|
||||
"source": "",
|
||||
"answer": "Could not find authoritative evidence.",
|
||||
"implication": "Uncertainty remains.",
|
||||
"status": "STILL_UNKNOWN",
|
||||
}
|
||||
run(
|
||||
"run_success_grounded",
|
||||
question="Does the API support streaming?",
|
||||
runner_spec={"type": "write", "artifact": dict(GROUNDED)},
|
||||
model="m-1",
|
||||
)
|
||||
run(
|
||||
"run_success_still_unknown",
|
||||
question="probe",
|
||||
runner_spec={"type": "write", "artifact": dict(STILL_UNKNOWN)},
|
||||
model="m-1",
|
||||
)
|
||||
run(
|
||||
"run_model_from_env",
|
||||
question="probe",
|
||||
runner_spec={"type": "write", "artifact": dict(GROUNDED)},
|
||||
model="",
|
||||
env={"MIMO_MODEL": "env-model"},
|
||||
)
|
||||
run(
|
||||
"run_model_unset_unknown",
|
||||
question="probe",
|
||||
runner_spec={"type": "write", "artifact": dict(GROUNDED)},
|
||||
model="",
|
||||
env={"MIMO_MODEL": None},
|
||||
)
|
||||
run(
|
||||
"run_model_env_blank_is_empty_string",
|
||||
question="probe",
|
||||
runner_spec={"type": "write", "artifact": dict(GROUNDED)},
|
||||
model="",
|
||||
env={"MIMO_MODEL": ""},
|
||||
)
|
||||
run(
|
||||
"run_budget_exhausted_before_runner",
|
||||
question="one too many",
|
||||
runner_spec={"type": "silent"},
|
||||
model="m",
|
||||
files={"probe.jsonl": "a\nb\n"},
|
||||
)
|
||||
run(
|
||||
"run_budget_one_left",
|
||||
question="still allowed",
|
||||
runner_spec={"type": "write", "artifact": dict(GROUNDED)},
|
||||
model="m",
|
||||
files={"probe.jsonl": "a\n"},
|
||||
)
|
||||
run(
|
||||
"run_silent_runner_missing_artifact",
|
||||
question="q",
|
||||
runner_spec={"type": "silent"},
|
||||
model="m",
|
||||
)
|
||||
run(
|
||||
"run_gate_failure_joined",
|
||||
question="q",
|
||||
runner_spec={
|
||||
"type": "write",
|
||||
"artifact": {"question": "q", "source": "s", "answer": "a", "implication": "i", "status": "MAYBE"},
|
||||
},
|
||||
model="m",
|
||||
)
|
||||
run(
|
||||
"run_gate_failure_non_dict",
|
||||
question="q",
|
||||
runner_spec={"type": "write", "artifact": [1, 2]},
|
||||
model="m",
|
||||
)
|
||||
run(
|
||||
"run_gate_failure_status_non_string",
|
||||
question="q",
|
||||
runner_spec={
|
||||
"type": "write",
|
||||
"artifact": {"question": "q", "source": "s", "answer": "a", "implication": "i", "status": 9},
|
||||
},
|
||||
model="m",
|
||||
)
|
||||
|
||||
# ── ProbeResult.to_dict ───────────────────────────────────────────────
|
||||
def todict(name, fields):
|
||||
d = ep.ProbeResult(**fields).to_dict()
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "to_dict",
|
||||
"setup": {"fields": fields},
|
||||
"want": json.dumps(d, ensure_ascii=True),
|
||||
}
|
||||
)
|
||||
|
||||
todict(
|
||||
"to_dict_defaults",
|
||||
{
|
||||
"question": "q",
|
||||
"source": "s",
|
||||
"answer": "a",
|
||||
"implication": "i",
|
||||
"status": "GROUNDED",
|
||||
},
|
||||
)
|
||||
todict(
|
||||
"to_dict_full",
|
||||
{
|
||||
"question": "q2",
|
||||
"source": "s2",
|
||||
"answer": "a2",
|
||||
"implication": "i2",
|
||||
"status": "STILL_UNKNOWN",
|
||||
"probe_id": "probe-1791753600",
|
||||
"decided_at": "2026-10-12T04:00:00Z",
|
||||
"model": "m",
|
||||
},
|
||||
)
|
||||
todict(
|
||||
"to_dict_unicode",
|
||||
{
|
||||
"question": "Größe",
|
||||
"source": "s — dash",
|
||||
"answer": "✓",
|
||||
"implication": "🚀",
|
||||
"status": "CONTRADICTED",
|
||||
},
|
||||
)
|
||||
|
||||
# ── _default_runner (stubbed subprocess) ──────────────────────────────
|
||||
stub_path = os.path.join(base, STUB_NAME)
|
||||
with open(stub_path, "w") as f:
|
||||
f.write(
|
||||
"#!/bin/sh\n"
|
||||
'for a in "$@"; do printf \'%s\\n\' "$a" >> "$EP_CAPTURE"; done\n'
|
||||
'printf \'stub-out\\n\'\n'
|
||||
'exit "$EP_EXIT"\n'
|
||||
)
|
||||
os.chmod(stub_path, 0o755)
|
||||
|
||||
real_require_model = provider_context.require_model
|
||||
|
||||
def defrun(name, *, exit_code, agent_env, model_mode):
|
||||
cdir = os.path.join(base, name)
|
||||
os.makedirs(cdir, exist_ok=True)
|
||||
capture = os.path.join(cdir, "argv_capture.txt")
|
||||
artifact_path = os.path.join(cdir, "evidence_probe.json")
|
||||
old_env = {k: os.environ.get(k) for k in ("MIMO_BIN", "MIMO_AGENT", "MIMO_MODEL", "EP_CAPTURE", "EP_EXIT")}
|
||||
try:
|
||||
if os.path.exists(capture):
|
||||
os.unlink(capture)
|
||||
os.environ["MIMO_BIN"] = stub_path
|
||||
os.environ["EP_CAPTURE"] = capture
|
||||
os.environ["EP_EXIT"] = str(exit_code)
|
||||
if agent_env is None:
|
||||
os.environ.pop("MIMO_AGENT", None)
|
||||
else:
|
||||
os.environ["MIMO_AGENT"] = agent_env
|
||||
if model_mode == "unresolved":
|
||||
os.environ.pop("MIMO_MODEL", None)
|
||||
provider_context.require_model = real_require_model
|
||||
elif model_mode == "blank":
|
||||
os.environ["MIMO_MODEL"] = " "
|
||||
provider_context.require_model = real_require_model
|
||||
else:
|
||||
provider_context.require_model = (
|
||||
lambda env_var="MIMO_MODEL": "test-model-v9"
|
||||
)
|
||||
try:
|
||||
ep._default_runner("PROMPT-TEXT", artifact_path)
|
||||
err = None
|
||||
except Exception as e: # noqa: BLE001
|
||||
err = {"type": type(e).__name__, "msg": norm(str(e), base)}
|
||||
argv = None
|
||||
if os.path.exists(capture):
|
||||
with open(capture) as f:
|
||||
argv = [norm(line, base) for line in f.read().splitlines()]
|
||||
log_path = os.path.join(cdir, "evidence_probe.stdout.log")
|
||||
log = None
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path) as f:
|
||||
log = f.read()
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"op": "default_runner",
|
||||
"setup": {
|
||||
"exit_code": exit_code,
|
||||
"agent_env": agent_env,
|
||||
"model_mode": model_mode,
|
||||
},
|
||||
"want": {"error": err, "argv": argv, "stdout_log": log},
|
||||
}
|
||||
)
|
||||
finally:
|
||||
provider_context.require_model = real_require_model
|
||||
for k, v in old_env.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
|
||||
defrun("defrun_exit0_default_agent", exit_code=0, agent_env=None, model_mode="patched")
|
||||
defrun("defrun_exit3", exit_code=3, agent_env=None, model_mode="patched")
|
||||
defrun("defrun_custom_agent", exit_code=0, agent_env="custom-agent", model_mode="patched")
|
||||
defrun("defrun_model_unresolved", exit_code=0, agent_env=None, model_mode="unresolved")
|
||||
defrun("defrun_model_blank_env", exit_code=0, agent_env=None, model_mode="blank")
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/evidence_probe.py (live; validate_probe/count_probes via Rust evidence_provenance core)",
|
||||
"generated": "2026-10-12",
|
||||
"frozen_ts": FROZEN_TS,
|
||||
"frozen_decided_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", _real_gmtime(FROZEN_TS)),
|
||||
"rows": rows,
|
||||
}
|
||||
target = REPO / "src/go/evidence-probe" / "truth.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(rows)} rows)")
|
||||
|
||||
# determinism self-check: probe_id/decided_at must be frozen
|
||||
assert all(
|
||||
r.get("want", {}).get("result", {}).get("probe_id") in (None, f"probe-{FROZEN_TS}")
|
||||
for r in rows
|
||||
if r["op"] == "run"
|
||||
), "clock freeze failed"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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,619 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_lexicon_evolution_truth.py — freeze/replay parity truth for
|
||||
lexicon_evolution (dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/lexicon_evolution.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/lexicon_evolution/src/frozen_le_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation ACTIVE (RUNE_LE_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_LE_CORE=python BEFORE any import.
|
||||
Mutation law: case inputs are deep-copied BEFORE they reach the oracle —
|
||||
the frozen input must be the pre-call input, not a post-call mutation.
|
||||
Set law: the oracle's set-valued functions (distinctive_terms,
|
||||
shared_distinctive) are frozen and compared as SORTED lists — Python set
|
||||
iteration order is not stable across processes; the facade restores set
|
||||
types on its public surface.
|
||||
Kernel-only group: `_stem` is private to the oracle (no facade symbol);
|
||||
its truth group is replayed kernel-side only (the Python _stem body is
|
||||
frozen verbatim and covered by tests/test_lexicon_contract.py).
|
||||
"""
|
||||
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", "lexicon_evolution", "src", "frozen_le_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_LE_CORE"] = "python" # poisoned-truth law (freeze side)
|
||||
else:
|
||||
os.environ.pop("RUNE_LE_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 lexicon_evolution as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import le_loader # noqa: E402 (kernel-level replay side)
|
||||
if not le_loader.available():
|
||||
print("REPLAY ABORT: kernel not built —", le_loader.LIB_PATH)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
class Case:
|
||||
def __init__(self, name, fn):
|
||||
self.name = name
|
||||
self.fn = fn
|
||||
|
||||
|
||||
class _Unset:
|
||||
pass
|
||||
|
||||
|
||||
_UNSET = _Unset()
|
||||
|
||||
|
||||
def _input(text=_UNSET, word=_UNSET, fact=_UNSET, proposal=_UNSET,
|
||||
corpus_texts=_UNSET, existing_values=_UNSET, source_texts=_UNSET,
|
||||
min_distinct=_UNSET, min_strength=_UNSET, evidence_strength=_UNSET):
|
||||
payload = {}
|
||||
if text is not _UNSET:
|
||||
payload["text"] = text
|
||||
if word is not _UNSET:
|
||||
payload["word"] = word
|
||||
if fact is not _UNSET:
|
||||
payload["fact"] = fact
|
||||
if proposal is not _UNSET:
|
||||
payload["proposal"] = proposal
|
||||
if corpus_texts is not _UNSET:
|
||||
payload["corpus_texts"] = list(corpus_texts)
|
||||
if existing_values is not _UNSET:
|
||||
payload["existing_values"] = list(existing_values)
|
||||
if source_texts is not _UNSET:
|
||||
payload["source_texts"] = list(source_texts)
|
||||
if min_distinct is not _UNSET and min_distinct is not None:
|
||||
payload["min_distinct"] = min_distinct
|
||||
if min_strength is not _UNSET and min_strength is not None:
|
||||
payload["min_strength"] = min_strength
|
||||
if evidence_strength is not _UNSET:
|
||||
payload["evidence_strength"] = evidence_strength
|
||||
return payload
|
||||
|
||||
|
||||
def sorted_or(x):
|
||||
return sorted(x) if isinstance(x, (set, frozenset)) else x
|
||||
|
||||
|
||||
def facade_call(group, inp):
|
||||
"""Run the LIVE facade (delegates to the Rust core when built)."""
|
||||
if group == "content_words":
|
||||
return oracle_mod.content_words(inp["text"])
|
||||
if group == "distinctive_terms":
|
||||
return oracle_mod.distinctive_terms(inp["fact"], inp["corpus_texts"])
|
||||
if group == "shared_distinctive":
|
||||
return oracle_mod.shared_distinctive(
|
||||
inp["fact"], inp["existing_values"], inp["corpus_texts"])
|
||||
if group == "coherence":
|
||||
return oracle_mod.coherence(
|
||||
inp["fact"], inp["existing_values"], inp["corpus_texts"])
|
||||
if group == "grounded":
|
||||
if "min_distinct" in inp:
|
||||
return oracle_mod.grounded(inp["fact"], inp["source_texts"],
|
||||
min_distinct=inp["min_distinct"])
|
||||
return oracle_mod.grounded(inp["fact"], inp["source_texts"])
|
||||
if group == "promote_eligible":
|
||||
return oracle_mod.promote_eligible(
|
||||
inp["proposal"], inp["existing"], inp["evidence_strength"],
|
||||
min_strength=inp.get("min_strength", 3),
|
||||
corpus_texts=inp["corpus_texts"])
|
||||
raise KeyError(group)
|
||||
|
||||
|
||||
def kernel_call(group, inp):
|
||||
"""Run the Rust kernel directly through the ctypes loader."""
|
||||
return getattr(le_loader, group)(**copy.deepcopy(inp))
|
||||
|
||||
|
||||
# ── content_words ────────────────────────────────────────────────────────────
|
||||
|
||||
def cw_case(rec, name, text):
|
||||
inp = _input(text=text)
|
||||
got = oracle_mod.content_words(copy.deepcopy(text))
|
||||
rec(name, inp, got, {"words": got})
|
||||
|
||||
|
||||
def c_cw_basic(rec):
|
||||
cw_case(rec, "cw-basic-stop-framing-filtered",
|
||||
"The quick brown fox jumps over the lazy dog")
|
||||
|
||||
|
||||
def c_cw_none(rec):
|
||||
cw_case(rec, "cw-none-coerced-empty", None)
|
||||
|
||||
|
||||
def c_cw_zero(rec):
|
||||
# falsy non-string → (text or "") → ""
|
||||
cw_case(rec, "cw-zero-falsy-coerced-empty", 0)
|
||||
|
||||
|
||||
def c_cw_empty(rec):
|
||||
cw_case(rec, "cw-empty-string", "")
|
||||
|
||||
|
||||
def c_cw_uppercase(rec):
|
||||
cw_case(rec, "cw-uppercase-folded", "PREFERS Oatmilk AND Celesta")
|
||||
|
||||
|
||||
def c_cw_short(rec):
|
||||
cw_case(rec, "cw-short-words-dropped", "a an the cat dog ox")
|
||||
|
||||
|
||||
def c_cw_framing(rec):
|
||||
cw_case(rec, "cw-framing-words-dropped",
|
||||
"work always way thing value identity persona")
|
||||
|
||||
|
||||
def c_cw_punct(rec):
|
||||
cw_case(rec, "cw-punctuation-splits", "co-ffee! 1234 espresso's brew.")
|
||||
|
||||
|
||||
def c_cw_mixed(rec):
|
||||
cw_case(rec, "cw-mixed-stop-framing", "she will make the work better each day")
|
||||
|
||||
|
||||
# ── distinctive_terms ────────────────────────────────────────────────────────
|
||||
|
||||
def dt_case(rec, name, fact, corpus):
|
||||
inp = _input(fact=fact, corpus_texts=corpus)
|
||||
got = oracle_mod.distinctive_terms(copy.deepcopy(fact), copy.deepcopy(corpus))
|
||||
rec(name, inp, got, {"terms": sorted(got)})
|
||||
|
||||
|
||||
def c_dt_empty_fact(rec):
|
||||
dt_case(rec, "dt-empty-fact", "", ["some text here"])
|
||||
|
||||
|
||||
def c_dt_all_stop(rec):
|
||||
dt_case(rec, "dt-fact-all-stop-framing", "the that this work",
|
||||
["some text here"])
|
||||
|
||||
|
||||
def c_dt_rare(rec):
|
||||
dt_case(rec, "dt-rare-terms-distinctive", "she drinks oatmilk daily",
|
||||
["oatmilk is life", "coffee today", "tea later"])
|
||||
|
||||
|
||||
def c_dt_common(rec):
|
||||
dt_case(rec, "dt-common-term-not-distinctive", "physics of black holes",
|
||||
["physics paper one", "physics paper two", "more physics here",
|
||||
"unrelated text"])
|
||||
|
||||
|
||||
def _filler_corpus(target_word, doc_ids, n=40):
|
||||
corpus = []
|
||||
for i in range(n):
|
||||
if i in doc_ids:
|
||||
corpus.append(f"the {target_word} session notes")
|
||||
else:
|
||||
corpus.append("filler text here")
|
||||
return corpus
|
||||
|
||||
|
||||
def c_dt_boundary_not_distinctive(rec):
|
||||
# df 2 at total 40 → threshold max(2.0, 1.5) = 2.0 → 2 < 2 is False
|
||||
dt_case(rec, "dt-boundary-df2-not-distinctive",
|
||||
"celesta zephyr quantum", _filler_corpus("celesta", {0, 1}))
|
||||
|
||||
|
||||
def c_dt_boundary_distinctive(rec):
|
||||
# df 1 at total 40 → 1 < 2.0 → distinctive
|
||||
dt_case(rec, "dt-boundary-df1-distinctive",
|
||||
"celesta zephyr", _filler_corpus("celesta", {0}))
|
||||
|
||||
|
||||
def c_dt_boundary_df3(rec):
|
||||
dt_case(rec, "dt-boundary-df3-not-distinctive",
|
||||
"celesta zephyr quantum", _filler_corpus("quantum", {0, 1, 2}))
|
||||
|
||||
|
||||
def c_dt_stem_df(rec):
|
||||
# migrate/migration share the stem "migr" → df counts both docs
|
||||
dt_case(rec, "dt-stem-aware-document-frequency", "migrate north soon",
|
||||
["the migration was huge", "another migration happened", "calm day"])
|
||||
|
||||
|
||||
def c_dt_empty_corpus(rec):
|
||||
dt_case(rec, "dt-empty-corpus-all-distinctive", "oatmilk ritual", [])
|
||||
|
||||
|
||||
def c_dt_case_fold(rec):
|
||||
dt_case(rec, "dt-case-folding", "OATMILK Rituals", ["oatmilk bar"])
|
||||
|
||||
|
||||
# ── shared_distinctive ───────────────────────────────────────────────────────
|
||||
|
||||
def sd_case(rec, name, fact, existing, corpus):
|
||||
inp = _input(fact=fact, existing_values=existing, corpus_texts=corpus)
|
||||
got = oracle_mod.shared_distinctive(
|
||||
copy.deepcopy(fact), copy.deepcopy(existing), copy.deepcopy(corpus))
|
||||
rec(name, inp, got, {"shared": sorted(got)})
|
||||
|
||||
|
||||
def c_sd_basic(rec):
|
||||
sd_case(rec, "sd-shared-distinctive-term", "she never jokes about oatmilk",
|
||||
["i love oatmilk a lot"], ["oatmilk is life", "tea later"])
|
||||
|
||||
|
||||
def c_sd_none_shared(rec):
|
||||
sd_case(rec, "sd-nothing-shared", "celesta practice daily",
|
||||
["i love oatmilk"], ["oatmilk is life", "tea later"])
|
||||
|
||||
|
||||
def c_sd_distinct_empty(rec):
|
||||
# framing-only fact → distinctive_terms empty → short-circuit
|
||||
sd_case(rec, "sd-distinct-empty-short-circuit", "work always thing",
|
||||
["whatever corpus text"], ["whatever corpus text"])
|
||||
|
||||
|
||||
def c_sd_stem_shared(rec):
|
||||
sd_case(rec, "sd-stem-aware-shared", "migration policy debate",
|
||||
["we migrated north"], ["migration news", "calm"])
|
||||
|
||||
|
||||
def c_sd_none_value(rec):
|
||||
sd_case(rec, "sd-none-existing-value-filtered", "she never jokes about oatmilk",
|
||||
[None, "i love oatmilk"], ["oatmilk is life", "tea later"])
|
||||
|
||||
|
||||
def c_sd_empty_existing(rec):
|
||||
sd_case(rec, "sd-empty-existing", "oatmilk mornings", [], ["oatmilk is life"])
|
||||
|
||||
|
||||
# ── coherence ────────────────────────────────────────────────────────────────
|
||||
|
||||
def co_case(rec, name, fact, existing, corpus=None):
|
||||
inp = _input(fact=fact, existing_values=existing,
|
||||
corpus_texts=[] if corpus is None else corpus)
|
||||
if corpus is None:
|
||||
got = oracle_mod.coherence(copy.deepcopy(fact), copy.deepcopy(existing))
|
||||
else:
|
||||
got = oracle_mod.coherence(copy.deepcopy(fact), copy.deepcopy(existing),
|
||||
copy.deepcopy(corpus))
|
||||
rec(name, inp, got, {"verdict": got})
|
||||
|
||||
|
||||
def c_co_empty_existing(rec):
|
||||
co_case(rec, "co-empty-existing-novel", "oatmilk mornings", [])
|
||||
|
||||
|
||||
def c_co_blank_existing(rec):
|
||||
co_case(rec, "co-blank-existing-filtered-novel", "oatmilk mornings",
|
||||
["", " ", None])
|
||||
|
||||
|
||||
def c_co_conflict(rec):
|
||||
co_case(rec, "co-negation-on-shared-distinctive-conflict",
|
||||
"she never jokes about oatmilk", ["i love oatmilk"],
|
||||
["oatmilk bar", "tea"])
|
||||
|
||||
|
||||
def c_co_negation_off_shared(rec):
|
||||
# negation present but the shared term is not a standalone word in the
|
||||
# fact ("oatmilk-lover" has no trailing space) → consistent
|
||||
co_case(rec, "co-negation-not-on-shared-term-consistent",
|
||||
"oatmilk-lover never smiles", ["i love oatmilk"],
|
||||
["oatmilk bar", "tea"])
|
||||
|
||||
|
||||
def c_co_consistent(rec):
|
||||
co_case(rec, "co-shared-distinctive-consistent",
|
||||
"i enjoy oatmilk mornings", ["i love oatmilk"],
|
||||
["oatmilk bar", "tea"])
|
||||
|
||||
|
||||
def c_co_novel(rec):
|
||||
co_case(rec, "co-nothing-shared-novel", "celesta practice daily",
|
||||
["i love oatmilk"], ["oatmilk bar", "tea"])
|
||||
|
||||
|
||||
def c_co_corpus_kills_shared(rec):
|
||||
# corpus override drives oatmilk df over the threshold → shared empty → novel
|
||||
co_case(rec, "co-corpus-override-kills-shared-novel",
|
||||
"she never jokes about oatmilk", ["i love oatmilk"],
|
||||
["oatmilk"] * 30)
|
||||
|
||||
|
||||
def c_co_default_corpus(rec):
|
||||
# corpus_texts omitted → corpus = the (filtered) existing values
|
||||
co_case(rec, "co-default-corpus-is-values", "i enjoy oatmilk mornings",
|
||||
["i love oatmilk"])
|
||||
|
||||
|
||||
# ── grounded ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def gr_case(rec, name, fact, sources, min_distinct=None):
|
||||
inp = _input(fact=fact, source_texts=sources, min_distinct=min_distinct)
|
||||
if min_distinct is None:
|
||||
got = oracle_mod.grounded(copy.deepcopy(fact), copy.deepcopy(sources))
|
||||
else:
|
||||
got = oracle_mod.grounded(copy.deepcopy(fact), copy.deepcopy(sources),
|
||||
min_distinct=min_distinct)
|
||||
rec(name, inp, got, {"grounded": got})
|
||||
|
||||
|
||||
def c_gr_no_srcs(rec):
|
||||
gr_case(rec, "gr-empty-sources-false", "she drinks oatmilk daily", [])
|
||||
|
||||
|
||||
def c_gr_blank_srcs(rec):
|
||||
gr_case(rec, "gr-blank-sources-false", "she drinks oatmilk daily", ["", " "])
|
||||
|
||||
|
||||
def c_gr_no_fact_terms(rec):
|
||||
gr_case(rec, "gr-fact-no-content-words-false", "the work thing",
|
||||
["oatmilk is life"])
|
||||
|
||||
|
||||
def c_gr_direct(rec):
|
||||
gr_case(rec, "gr-direct-word-hit", "she drinks oatmilk daily",
|
||||
["oatmilk is life"])
|
||||
|
||||
|
||||
def c_gr_stem(rec):
|
||||
gr_case(rec, "gr-stem-aware-hit", "migrated north",
|
||||
["the migration of songbirds"])
|
||||
|
||||
|
||||
def c_gr_miss(rec):
|
||||
gr_case(rec, "gr-no-stem-overlap-false", "quantum flux patterns",
|
||||
["oatmilk life"])
|
||||
|
||||
|
||||
def c_gr_min2_one_hit(rec):
|
||||
gr_case(rec, "gr-min-distinct-2-one-hit-false", "she drinks oatmilk with celesta",
|
||||
["oatmilk life"], min_distinct=2)
|
||||
|
||||
|
||||
def c_gr_min2_two_hits(rec):
|
||||
gr_case(rec, "gr-min-distinct-2-two-hits-true", "she drinks oatmilk with celesta",
|
||||
["oatmilk and celesta life"], min_distinct=2)
|
||||
|
||||
|
||||
def c_gr_case(rec):
|
||||
gr_case(rec, "gr-source-case-folded", "she drinks oatmilk daily",
|
||||
["OATMILK IS LIFE"])
|
||||
|
||||
|
||||
# ── _stem (kernel-only: private in the oracle) ───────────────────────────────
|
||||
|
||||
STEM_WORDS = [
|
||||
"migration", "migrated", "migrating", "migrate", "migrates",
|
||||
"retroactively", "retroactive", "honesty", "ability", "abilities",
|
||||
"running", "kindness", "cats", "dogs", "hope", "oatmilk", "rituals",
|
||||
"nation", "national", "carefully", "careful", "berries",
|
||||
]
|
||||
|
||||
|
||||
def st_case(rec, name, word):
|
||||
inp = _input(word=word)
|
||||
got = oracle_mod._stem(copy.deepcopy(word))
|
||||
rec(name, inp, got, {"stem": got})
|
||||
|
||||
|
||||
def c_stem_table(rec):
|
||||
for w in STEM_WORDS:
|
||||
st_case(rec, f"stem-{w}", w)
|
||||
|
||||
|
||||
# ── promote_eligible ─────────────────────────────────────────────────────────
|
||||
|
||||
def pe_case(rec, name, proposal, existing, strength, min_strength=None, corpus=()):
|
||||
inp = _input(proposal=proposal, existing_values=existing,
|
||||
evidence_strength=strength, min_strength=min_strength,
|
||||
corpus_texts=corpus)
|
||||
# the oracle/loader param is `existing` (the promote signature)
|
||||
inp["existing"] = inp.pop("existing_values")
|
||||
if min_strength is None:
|
||||
got = oracle_mod.promote_eligible(copy.deepcopy(proposal),
|
||||
copy.deepcopy(existing), strength)
|
||||
else:
|
||||
got = oracle_mod.promote_eligible(
|
||||
copy.deepcopy(proposal), copy.deepcopy(existing), strength,
|
||||
min_strength=min_strength, corpus_texts=copy.deepcopy(corpus))
|
||||
rec(name, inp, got, {"eligible": got["eligible"], "reason": got["reason"]})
|
||||
|
||||
|
||||
def c_pe_below_strength(rec):
|
||||
pe_case(rec, "pe-strength-gate-first", "oatmilk mornings", ["i love oatmilk"],
|
||||
2, min_strength=3, corpus=["oatmilk bar"])
|
||||
|
||||
|
||||
def c_pe_float_strength(rec):
|
||||
pe_case(rec, "pe-float-strength-message", "oatmilk mornings", ["i love oatmilk"],
|
||||
2.5, min_strength=3, corpus=["oatmilk bar"])
|
||||
|
||||
|
||||
def c_pe_empty_block(rec):
|
||||
pe_case(rec, "pe-empty-block-first-principle", "oatmilk mornings", [], 3,
|
||||
min_strength=3)
|
||||
|
||||
|
||||
def c_pe_blank_block(rec):
|
||||
pe_case(rec, "pe-blank-block-filtered-first-principle", "oatmilk mornings",
|
||||
["", " "], 3, min_strength=3)
|
||||
|
||||
|
||||
def c_pe_shared(rec):
|
||||
pe_case(rec, "pe-shared-distinctive-eligible", "i genuinely love oatmilk rituals",
|
||||
["i love oatmilk"], 3, min_strength=3,
|
||||
corpus=["oatmilk bar", "tea later"])
|
||||
|
||||
|
||||
def c_pe_shared_multi(rec):
|
||||
pe_case(rec, "pe-shared-multiple-sorted-top3", "oatmilk celesta mornings",
|
||||
["oatmilk and celesta"], 3, min_strength=3,
|
||||
corpus=["oatmilk bar", "celesta hall"])
|
||||
|
||||
|
||||
def c_pe_no_terms(rec):
|
||||
pe_case(rec, "pe-framing-only-no-lexical-signal", "work always thing way",
|
||||
["i love oatmilk"], 3, min_strength=3, corpus=["oatmilk bar"])
|
||||
|
||||
|
||||
def c_pe_novel_grounded(rec):
|
||||
pe_case(rec, "pe-novel-axis-grounded-eligible", "celesta practice daily",
|
||||
["i love oatmilk"], 3, min_strength=3,
|
||||
corpus=["she plays celesta nightly"])
|
||||
|
||||
|
||||
def c_pe_novel_parked(rec):
|
||||
pe_case(rec, "pe-novel-axis-not-grounded-parked", "celesta practice daily",
|
||||
["i love oatmilk"], 3, min_strength=3, corpus=["oatmilk bar only"])
|
||||
|
||||
|
||||
def c_pe_min_strength_one(rec):
|
||||
pe_case(rec, "pe-min-strength-1-passes-gate", "oatmilk mornings", [], 1,
|
||||
min_strength=1)
|
||||
|
||||
|
||||
def c_pe_corpus_override_grounded(rec):
|
||||
# corpus override pushes oatmilk df over the threshold → shared empty →
|
||||
# novel axis, but the proposal still grounds in the overridden corpus
|
||||
pe_case(rec, "pe-corpus-override-novel-grounded", "oatmilk rituals",
|
||||
["i love oatmilk"], 3, min_strength=3,
|
||||
corpus=["oatmilk here", "oatmilk there", "oatmilk everywhere"])
|
||||
|
||||
|
||||
def c_pe_default_corpus(rec):
|
||||
# corpus_texts omitted → corpus = existing values
|
||||
pe_case(rec, "pe-default-corpus-is-existing", "oatmilk rituals",
|
||||
["i love oatmilk"], 3, min_strength=3)
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
CONTENT_WORDS = [c_cw_basic, c_cw_none, c_cw_zero, c_cw_empty, c_cw_uppercase,
|
||||
c_cw_short, c_cw_framing, c_cw_punct, c_cw_mixed]
|
||||
DISTINCTIVE = [c_dt_empty_fact, c_dt_all_stop, c_dt_rare, c_dt_common,
|
||||
c_dt_boundary_not_distinctive, c_dt_boundary_distinctive,
|
||||
c_dt_boundary_df3, c_dt_stem_df, c_dt_empty_corpus, c_dt_case_fold]
|
||||
SHARED = [c_sd_basic, c_sd_none_shared, c_sd_distinct_empty, c_sd_stem_shared,
|
||||
c_sd_none_value, c_sd_empty_existing]
|
||||
COHERENCE = [c_co_empty_existing, c_co_blank_existing, c_co_conflict,
|
||||
c_co_negation_off_shared, c_co_consistent, c_co_novel,
|
||||
c_co_corpus_kills_shared, c_co_default_corpus]
|
||||
GROUNDED = [c_gr_no_srcs, c_gr_blank_srcs, c_gr_no_fact_terms, c_gr_direct,
|
||||
c_gr_stem, c_gr_miss, c_gr_min2_one_hit, c_gr_min2_two_hits,
|
||||
c_gr_case]
|
||||
STEM = [c_stem_table]
|
||||
PROMOTE = [c_pe_below_strength, c_pe_float_strength, c_pe_empty_block,
|
||||
c_pe_blank_block, c_pe_shared, c_pe_shared_multi, c_pe_no_terms,
|
||||
c_pe_novel_grounded, c_pe_novel_parked, c_pe_min_strength_one,
|
||||
c_pe_corpus_override_grounded, c_pe_default_corpus]
|
||||
|
||||
# group → (cases, facade_replayable, kernel_symbol, unwrap of expected)
|
||||
GROUPS = {
|
||||
"content_words": (CONTENT_WORDS, True, "le_content_words",
|
||||
lambda w: w["words"]),
|
||||
"distinctive_terms": (DISTINCTIVE, True, "le_distinctive_terms",
|
||||
lambda w: w["terms"]),
|
||||
"shared_distinctive": (SHARED, True, "le_shared_distinctive",
|
||||
lambda w: w["shared"]),
|
||||
"coherence": (COHERENCE, True, "le_coherence", lambda w: w["verdict"]),
|
||||
"grounded": (GROUNDED, True, "le_grounded", lambda w: w["grounded"]),
|
||||
"stem": (STEM, False, "le_stem", lambda w: w["stem"]),
|
||||
"promote_eligible": (PROMOTE, True, "le_promote_eligible", 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):
|
||||
entry = {"name": name, "input": inp,
|
||||
"return": sorted_or(oracle_ret) if group in
|
||||
("distinctive_terms", "shared_distinctive")
|
||||
else oracle_ret,
|
||||
"expected": expected}
|
||||
if REPLAY and facade_replayable:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_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 = GROUPS[group][2]
|
||||
unwrap = GROUPS[group][3]
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = kernel_call(group, case["input"])
|
||||
want = unwrap(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/lexicon_evolution.py",
|
||||
"pin": "RUNE_LE_CORE=python",
|
||||
"note": "Frozen via scripts/gen_lexicon_evolution_truth.py against the "
|
||||
"live Python oracle. Battery embeds this VERBATIM. Group "
|
||||
"`stem` is kernel-only (_stem is private in the oracle).",
|
||||
"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,654 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze message_graph oracle truth for the Rust port battery (dichotomy
|
||||
wave #2479 tranche).
|
||||
|
||||
Exercises EVERY public function of src/rune/message_graph.py across every
|
||||
branch: load (missing file, corrupt JSON, wrong/missing schema, valid doc,
|
||||
duplicate ids, verbatim extra fields, non-dict doc -> AttributeError,
|
||||
non-list nodes -> TypeError), save (doc shape + file bytes incl. mkdir),
|
||||
add (default-parent chain, explicit parent with/without edge, explicit
|
||||
None -> head, falsy parent "", edge to a ghost parent, unicode, every
|
||||
kind, bad kind/edge -> ValueError), tool_call (json.dumps content shape,
|
||||
nested args, parent / default head), tool_result (link, missing call ->
|
||||
KeyError, non-tool-call parent allowed), revise (root revision picks up
|
||||
the default-head parent quirk, child revision, chained revisions, missing
|
||||
-> KeyError), branch (ok, missing -> KeyError), lineage (empty graph,
|
||||
default head, explicit id, superseded skip, missing-parent break, ghost
|
||||
superseded_by target, unknown id, parent cycle), history (single, full
|
||||
chain, from the middle, missing id), migrate_flat (chain, empty
|
||||
transcript, missing role/content keys, existing-file seed) and cue (no
|
||||
cue, replaces, branched from, superseded by, falsy superseded_by, edge
|
||||
iteration order).
|
||||
|
||||
Nondeterminism seams: the facade generates the uuid4 hex id and the
|
||||
_now() timestamp and passes them to the kernel as plain arguments — here
|
||||
both are pinned (sequential fake uuid4, frozen clock) 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_MG_CORE=python (the ORACLE). The kernel-side parity
|
||||
pass re-runs with RUNE_MG_CORE=auto and MG_TRUTH_OUT set; identical bytes
|
||||
== 100% live parity.
|
||||
|
||||
Output: /tmp/mg_truth.json — replayed VERBATIM by
|
||||
src/rust/message_graph/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("RUNE_MG_CORE", "python") # freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import message_graph as mg # noqa: E402
|
||||
|
||||
|
||||
# ── determinism seams (see module docstring) ─────────────────────────────
|
||||
class _FakeUUID:
|
||||
def __init__(self, hex_: str) -> None:
|
||||
self.hex = hex_
|
||||
|
||||
|
||||
_seq = [0]
|
||||
|
||||
|
||||
def _fake_uuid4():
|
||||
_seq[0] += 1
|
||||
return _FakeUUID(f"deadbeef{_seq[0]:04d}")
|
||||
|
||||
|
||||
uuid.uuid4 = _fake_uuid4
|
||||
CLOCK = "2031-01-01T00:00:00.000000+00:00"
|
||||
mg._now = lambda: CLOCK # noqa: E731 (the oracle's clock, pinned)
|
||||
|
||||
TMP = Path(tempfile.mkdtemp(prefix="mg_truth_"))
|
||||
|
||||
GROUPS = ("load", "save", "add", "tool_call", "tool_result", "revise",
|
||||
"branch", "lineage", "history", "migrate_flat", "cue")
|
||||
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})
|
||||
|
||||
|
||||
def wire(g: "mg.MessageGraph") -> dict:
|
||||
"""Graph state over the wire: nodes as an insertion-ordered object,
|
||||
order as the arrival list."""
|
||||
return {"nodes": copy.deepcopy(g.nodes), "order": list(g.order)}
|
||||
|
||||
|
||||
def exc_of(fn, *a, **kw) -> dict:
|
||||
try:
|
||||
fn(*a, **kw)
|
||||
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 fresh(name: str, doc_text: str | None = None) -> "mg.MessageGraph":
|
||||
p = TMP / f"{name}.json"
|
||||
if doc_text is not None:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(doc_text)
|
||||
return mg.MessageGraph(path=p)
|
||||
|
||||
|
||||
# ══ load ═════════════════════════════════════════════════════════════════
|
||||
# missing file -> empty graph
|
||||
g = fresh("load_missing")
|
||||
case("load", "load-missing-file", {"text": None}, {"graph": wire(g)})
|
||||
|
||||
# empty object: no schema -> ignored
|
||||
g = fresh("load_empty_obj", "{}")
|
||||
case("load", "load-no-schema", {"text": "{}"}, {"graph": wire(g)})
|
||||
|
||||
# wrong schema
|
||||
g = fresh("load_wrong_schema", json.dumps({"schema": 2, "nodes": []}))
|
||||
case("load", "load-wrong-schema", {"text": json.dumps({"schema": 2, "nodes": []})},
|
||||
{"graph": wire(g)})
|
||||
|
||||
# corrupt JSON
|
||||
g = fresh("load_corrupt", "not json {")
|
||||
case("load", "load-corrupt-json", {"text": "not json {"}, {"graph": wire(g)})
|
||||
|
||||
# valid doc: nodes ingested verbatim in file order
|
||||
doc = {"schema": 1, "nodes": [
|
||||
{"id": "aaa000000001", "kind": "message", "role": "user", "content": "q",
|
||||
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
||||
{"id": "bbb000000002", "kind": "reply", "role": "assistant", "content": "a",
|
||||
"ts": CLOCK, "parent": "aaa000000001", "edges": [], "superseded_by": None},
|
||||
]}
|
||||
g = fresh("load_valid", json.dumps(doc))
|
||||
case("load", "load-valid", {"text": json.dumps(doc)}, {"graph": wire(g)})
|
||||
|
||||
# duplicate ids: last wins in nodes, BOTH stay in order
|
||||
doc = {"schema": 1, "nodes": [
|
||||
{"id": "ccc000000003", "kind": "message", "role": "user", "content": "v1",
|
||||
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
||||
{"id": "ccc000000003", "kind": "message", "role": "user", "content": "v2",
|
||||
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
||||
]}
|
||||
g = fresh("load_dup", json.dumps(doc))
|
||||
case("load", "load-duplicate-ids-last-wins-order-keeps-both",
|
||||
{"text": json.dumps(doc)}, {"graph": wire(g)})
|
||||
|
||||
# extra fields ride verbatim
|
||||
doc = {"schema": 1, "nodes": [
|
||||
{"id": "ddd000000004", "kind": "message", "role": "user", "content": "x",
|
||||
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None,
|
||||
"extra": {"deep": [1, 2.5, None, True]}, "π": "unicode key"},
|
||||
]}
|
||||
g = fresh("load_extra", json.dumps(doc))
|
||||
case("load", "load-extra-fields-verbatim", {"text": json.dumps(doc)},
|
||||
{"graph": wire(g)})
|
||||
|
||||
# non-dict doc -> the oracle's doc.get raises AttributeError
|
||||
def _err_file(name: str, text: str) -> Path:
|
||||
p = TMP / f"{name}.json"
|
||||
p.write_text(text)
|
||||
return p
|
||||
|
||||
|
||||
_p = _err_file("load_list", "[1, 2]")
|
||||
case("load", "load-list-doc-attributeerror", {"text": "[1, 2]"},
|
||||
exc_of(mg.MessageGraph, path=_p))
|
||||
_p = _err_file("load_str", '"hello"')
|
||||
case("load", "load-string-doc-attributeerror", {"text": '"hello"'},
|
||||
exc_of(mg.MessageGraph, path=_p))
|
||||
_p = _err_file("load_int", "5")
|
||||
case("load", "load-int-doc-attributeerror", {"text": "5"},
|
||||
exc_of(mg.MessageGraph, path=_p))
|
||||
|
||||
# nodes present but not a list -> the for-loop raises TypeError
|
||||
_p = _err_file("load_dictnodes", json.dumps({"schema": 1, "nodes": {"aaa": {}}}))
|
||||
case("load", "load-dict-nodes-typeerror",
|
||||
{"text": json.dumps({"schema": 1, "nodes": {"aaa": {}}})},
|
||||
exc_of(mg.MessageGraph, path=_p))
|
||||
|
||||
# ══ add ══════════════════════════════════════════════════════════════════
|
||||
# default parent on an empty graph: parent None, edges []
|
||||
g = fresh("add_empty")
|
||||
node = g.add("user", "hello")
|
||||
case("add", "add-default-parent-empty-graph",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": node["id"],
|
||||
"ts": node["ts"], "role": "user", "content": "hello", "kind": "message",
|
||||
"parent": None, "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# default parent chains to the head; edges stay [] (parent arg was None)
|
||||
g = fresh("add_chain")
|
||||
g.add("user", "q1")
|
||||
before = wire(g)
|
||||
node = g.add("assistant", "a1")
|
||||
case("add", "add-default-parent-head-no-edge",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
||||
"content": "a1", "kind": "message", "parent": None, "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# explicit parent + explicit edge
|
||||
g = fresh("add_explicit")
|
||||
a = g.add("user", "q")
|
||||
before = wire(g)
|
||||
node = g.add("assistant", "a", kind="reply", parent=a["id"], edge="reply_to")
|
||||
case("add", "add-explicit-parent-edge",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
||||
"content": "a", "kind": "reply", "parent": a["id"], "edge": "reply_to"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# explicit parent, default edge "parent"
|
||||
g = fresh("add_explicit_default_edge")
|
||||
a = g.add("user", "q")
|
||||
before = wire(g)
|
||||
node = g.add("assistant", "a", parent=a["id"])
|
||||
case("add", "add-explicit-parent-default-edge",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
||||
"content": "a", "kind": "message", "parent": a["id"], "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# explicit None parent with a non-empty order: parent becomes the HEAD but
|
||||
# edges stay [] (the arg was None — falsy)
|
||||
g = fresh("add_none_parent")
|
||||
a = g.add("user", "q")
|
||||
before = wire(g)
|
||||
node = g.add("assistant", "a", parent=None)
|
||||
case("add", "add-explicit-none-parent-becomes-head",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
||||
"content": "a", "kind": "message", "parent": None, "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# falsy parent "": stored as parent, but NO edge (falsy)
|
||||
g = fresh("add_falsy_parent")
|
||||
a = g.add("user", "q")
|
||||
before = wire(g)
|
||||
node = g.add("assistant", "a", parent="")
|
||||
case("add", "add-falsy-empty-parent-no-edge",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
||||
"content": "a", "kind": "message", "parent": "", "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# edge to a ghost parent is allowed (no referential check in add)
|
||||
g = fresh("add_ghost_parent")
|
||||
a = g.add("user", "q")
|
||||
before = wire(g)
|
||||
node = g.add("assistant", "a", parent="ghost0000001", edge="reply_to")
|
||||
case("add", "add-ghost-parent-allowed",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
||||
"content": "a", "kind": "message", "parent": "ghost0000001",
|
||||
"edge": "reply_to"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# unicode content
|
||||
g = fresh("add_unicode")
|
||||
node = g.add("user", "こんにちは ☕ héllo")
|
||||
case("add", "add-unicode-content",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
||||
"role": "user", "content": "こんにちは ☕ héllo", "kind": "message",
|
||||
"parent": None, "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# every kind
|
||||
for k in ("message", "reply", "tool_call", "tool_result", "revision", "branch_head"):
|
||||
g = fresh(f"add_kind_{k}")
|
||||
node = g.add("user", f"content-{k}", kind=k)
|
||||
case("add", f"add-kind-{k}",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
||||
"role": "user", "content": f"content-{k}", "kind": k,
|
||||
"parent": None, "edge": "parent"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# bad kind / bad edge (ValueError with the oracle's repr formatting)
|
||||
g = fresh("add_bad_kind")
|
||||
case("add", "add-unknown-kind-valueerror",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
||||
"role": "user", "content": "x", "kind": "nonsense", "parent": None,
|
||||
"edge": "parent"},
|
||||
exc_of(g.add, "user", "x", kind="nonsense"))
|
||||
case("add", "add-unknown-edge-valueerror",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
||||
"role": "user", "content": "x", "kind": "message", "parent": None,
|
||||
"edge": "nope"},
|
||||
exc_of(g.add, "user", "x", edge="nope"))
|
||||
# kind checked BEFORE edge
|
||||
case("add", "add-both-bad-kind-wins",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
||||
"role": "user", "content": "x", "kind": "nonsense", "parent": None,
|
||||
"edge": "nope"},
|
||||
exc_of(g.add, "user", "x", kind="nonsense", edge="nope"))
|
||||
|
||||
# ══ tool_call ════════════════════════════════════════════════════════════
|
||||
g = fresh("tc_basic")
|
||||
node = g.tool_call("read", {"path": "a.py"})
|
||||
case("tool_call", "tool-call-basic",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
||||
"content": json.dumps({"call": "read", "args": {"path": "a.py"}}),
|
||||
"parent": None},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
g = fresh("tc_parent")
|
||||
a = g.add("assistant", "running")
|
||||
before = wire(g)
|
||||
node = g.tool_call("write", {"path": "b.py", "bytes": 3}, parent=a["id"])
|
||||
case("tool_call", "tool-call-explicit-parent",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"],
|
||||
"content": json.dumps({"call": "write", "args": {"path": "b.py", "bytes": 3}}),
|
||||
"parent": a["id"]},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
g = fresh("tc_nested")
|
||||
node = g.tool_call("search", {"q": "π", "opts": {"deep": [1, 2, {"k": None}],
|
||||
"flag": True}, "n": 0.5})
|
||||
case("tool_call", "tool-call-nested-args",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
||||
"content": json.dumps({"call": "search",
|
||||
"args": {"q": "π", "opts": {"deep": [1, 2, {"k": None}],
|
||||
"flag": True}, "n": 0.5}}),
|
||||
"parent": None},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# ══ tool_result ══════════════════════════════════════════════════════════
|
||||
g = fresh("tr_ok")
|
||||
c = g.tool_call("read", {"path": "a.py"})
|
||||
before = wire(g)
|
||||
node = g.tool_result(c["id"], "file contents")
|
||||
case("tool_result", "tool-result-links-call",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "call_id": c["id"],
|
||||
"result": "file contents"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
# no kind check on the parent: any node can be a "call"
|
||||
g = fresh("tr_any_node")
|
||||
a = g.add("user", "not a call")
|
||||
before = wire(g)
|
||||
node = g.tool_result(a["id"], "result anyway")
|
||||
case("tool_result", "tool-result-non-call-parent-allowed",
|
||||
{"graph": before, "id": node["id"], "ts": node["ts"], "call_id": a["id"],
|
||||
"result": "result anyway"},
|
||||
{"node": copy.deepcopy(node), "graph": wire(g)})
|
||||
|
||||
g = fresh("tr_missing")
|
||||
case("tool_result", "tool-result-missing-keyerror",
|
||||
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
||||
"call_id": "ghost0000001", "result": "x"},
|
||||
exc_of(g.tool_result, "ghost0000001", "x"))
|
||||
|
||||
# ══ revise ═══════════════════════════════════════════════════════════════
|
||||
# root revision: old.get("parent") is None -> add substitutes the HEAD,
|
||||
# which at that moment is the old node itself (quirk, frozen verbatim)
|
||||
g = fresh("rev_root")
|
||||
a = g.add("assistant", "v1")
|
||||
before = wire(g)
|
||||
rev = g.revise(a["id"], "v2")
|
||||
case("revise", "revise-root-head-parent-quirk",
|
||||
{"graph": before, "node_id": a["id"], "new_content": "v2",
|
||||
"id": rev["id"], "ts": rev["ts"]},
|
||||
{"node": copy.deepcopy(rev), "graph": wire(g)})
|
||||
|
||||
# child revision: keeps the old parent, edges revises + supersedes
|
||||
g = fresh("rev_child")
|
||||
a = g.add("user", "q")
|
||||
b = g.add("assistant", "v1", parent=a["id"])
|
||||
before = wire(g)
|
||||
rev = g.revise(b["id"], "v2")
|
||||
case("revise", "revise-child-keeps-parent",
|
||||
{"graph": before, "node_id": b["id"], "new_content": "v2",
|
||||
"id": rev["id"], "ts": rev["ts"]},
|
||||
{"node": copy.deepcopy(rev), "graph": wire(g)})
|
||||
|
||||
# chained revision: revise the revision
|
||||
g = fresh("rev_chain")
|
||||
a = g.add("assistant", "v1")
|
||||
r1 = g.revise(a["id"], "v2")
|
||||
before = wire(g)
|
||||
r2 = g.revise(r1["id"], "v3")
|
||||
case("revise", "revise-of-revision",
|
||||
{"graph": before, "node_id": r1["id"], "new_content": "v3",
|
||||
"id": r2["id"], "ts": r2["ts"]},
|
||||
{"node": copy.deepcopy(r2), "graph": wire(g)})
|
||||
|
||||
g = fresh("rev_missing")
|
||||
case("revise", "revise-missing-keyerror",
|
||||
{"graph": {"nodes": {}, "order": []}, "node_id": "ghost0000001",
|
||||
"new_content": "x", "id": "deadbeef_err", "ts": CLOCK},
|
||||
exc_of(g.revise, "ghost0000001", "x"))
|
||||
|
||||
# ══ branch ═══════════════════════════════════════════════════════════════
|
||||
g = fresh("branch_ok")
|
||||
a = g.add("user", "root")
|
||||
g.add("assistant", "answer", parent=a["id"])
|
||||
before = wire(g)
|
||||
head = g.branch(a["id"])
|
||||
case("branch", "branch-from-message",
|
||||
{"graph": before, "node_id": a["id"], "id": head["id"], "ts": head["ts"]},
|
||||
{"node": copy.deepcopy(head), "graph": wire(g)})
|
||||
|
||||
g = fresh("branch_missing")
|
||||
case("branch", "branch-missing-keyerror",
|
||||
{"graph": {"nodes": {}, "order": []}, "node_id": "ghost0000001",
|
||||
"id": "deadbeef_err", "ts": CLOCK},
|
||||
exc_of(g.branch, "ghost0000001"))
|
||||
|
||||
# ══ lineage ══════════════════════════════════════════════════════════════
|
||||
g = fresh("lin_empty")
|
||||
case("lineage", "lineage-empty-graph", {"graph": wire(g), "node_id": None},
|
||||
{"result": []})
|
||||
|
||||
# default head: full chain oldest-first
|
||||
g = fresh("lin_chain")
|
||||
a = g.add("user", "q")
|
||||
b = g.add("assistant", "a1", parent=a["id"])
|
||||
c = g.add("assistant", "a2", parent=b["id"])
|
||||
case("lineage", "lineage-default-head-full-chain",
|
||||
{"graph": wire(g), "node_id": None},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage()]})
|
||||
assert [n["id"] for n in g.lineage()] == [a["id"], b["id"], c["id"]]
|
||||
|
||||
# explicit id: subchain
|
||||
case("lineage", "lineage-explicit-id-subchain",
|
||||
{"graph": wire(g), "node_id": b["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage(b["id"])]})
|
||||
assert [n["id"] for n in g.lineage(b["id"])] == [a["id"], b["id"]]
|
||||
|
||||
# superseded nodes yield to their revision; unrelated branches excluded
|
||||
g = fresh("lin_superseded")
|
||||
a = g.add("user", "q")
|
||||
b = g.add("assistant", "v1", parent=a["id"])
|
||||
other = g.add("user", "unrelated")
|
||||
rev = g.revise(b["id"], "v2")
|
||||
case("lineage", "lineage-skips-superseded-default-head",
|
||||
{"graph": wire(g), "node_id": None},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage()]})
|
||||
assert [n["id"] for n in g.lineage()] == [a["id"], rev["id"]]
|
||||
case("lineage", "lineage-from-root-excludes-other-branch",
|
||||
{"graph": wire(g), "node_id": a["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage(a["id"])]})
|
||||
assert [n["id"] for n in g.lineage(a["id"])] == [a["id"]]
|
||||
|
||||
# parent chain into a ghost: walk breaks quietly
|
||||
g = fresh("lin_ghost_parent")
|
||||
a = g.add("user", "q")
|
||||
g.nodes[a["id"]]["parent"] = "ghost0000001"
|
||||
case("lineage", "lineage-ghost-parent-breaks",
|
||||
{"graph": wire(g), "node_id": a["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage(a["id"])]})
|
||||
assert [n["id"] for n in g.lineage(a["id"])] == [a["id"]]
|
||||
|
||||
# superseded_by pointing OUTSIDE the graph: node included (guard fails)
|
||||
g = fresh("lin_ghost_superseded")
|
||||
a = g.add("user", "q")
|
||||
g.nodes[a["id"]]["superseded_by"] = "ghost0000001"
|
||||
case("lineage", "lineage-ghost-superseded-by-included",
|
||||
{"graph": wire(g), "node_id": a["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage(a["id"])]})
|
||||
assert [n["id"] for n in g.lineage(a["id"])] == [a["id"]]
|
||||
|
||||
# unknown id: empty
|
||||
g = fresh("lin_unknown")
|
||||
case("lineage", "lineage-unknown-id-empty",
|
||||
{"graph": wire(g), "node_id": "ghost0000001"}, {"result": []})
|
||||
|
||||
# parent cycle: the seen-set guard stops the walk
|
||||
doc = {"schema": 1, "nodes": [
|
||||
{"id": "aaa000000001", "kind": "message", "role": "user", "content": "a",
|
||||
"ts": CLOCK, "parent": "bbb000000002", "edges": [], "superseded_by": None},
|
||||
{"id": "bbb000000002", "kind": "message", "role": "user", "content": "b",
|
||||
"ts": CLOCK, "parent": "aaa000000001", "edges": [], "superseded_by": None},
|
||||
]}
|
||||
g = fresh("lin_cycle", json.dumps(doc))
|
||||
case("lineage", "lineage-parent-cycle-guarded",
|
||||
{"graph": wire(g), "node_id": "aaa000000001"},
|
||||
{"result": [copy.deepcopy(n) for n in g.lineage("aaa000000001")]})
|
||||
assert [n["id"] for n in g.lineage("aaa000000001")] == ["bbb000000002",
|
||||
"aaa000000001"]
|
||||
|
||||
# ══ history ══════════════════════════════════════════════════════════════
|
||||
# single node: no supersedes edge -> just itself
|
||||
g = fresh("hist_single")
|
||||
a = g.add("assistant", "v1")
|
||||
case("history", "history-single-node",
|
||||
{"graph": wire(g), "node_id": a["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.history(a["id"])]})
|
||||
assert [n["content"] for n in g.history(a["id"])] == ["v1"]
|
||||
|
||||
# full chain oldest-first, asked from the tip AND from the middle
|
||||
g = fresh("hist_chain")
|
||||
a = g.add("assistant", "v1")
|
||||
r1 = g.revise(a["id"], "v2")
|
||||
r2 = g.revise(r1["id"], "v3")
|
||||
case("history", "history-chain-from-tip",
|
||||
{"graph": wire(g), "node_id": r2["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.history(r2["id"])]})
|
||||
assert [n["content"] for n in g.history(r2["id"])] == ["v1", "v2", "v3"]
|
||||
case("history", "history-chain-from-middle-full-trail",
|
||||
{"graph": wire(g), "node_id": r1["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.history(r1["id"])]})
|
||||
assert [n["content"] for n in g.history(r1["id"])] == ["v1", "v2", "v3"]
|
||||
|
||||
# unknown id: empty trail
|
||||
g = fresh("hist_missing")
|
||||
case("history", "history-unknown-id-empty",
|
||||
{"graph": wire(g), "node_id": "ghost0000001"}, {"result": []})
|
||||
|
||||
# revision whose superseded_by target is a ghost: trail stops there
|
||||
g = fresh("hist_ghost_target")
|
||||
a = g.add("assistant", "v1")
|
||||
r1 = g.revise(a["id"], "v2")
|
||||
g.nodes[r1["id"]]["superseded_by"] = "ghost0000001"
|
||||
case("history", "history-ghost-superseded-by-stops",
|
||||
{"graph": wire(g), "node_id": a["id"]},
|
||||
{"result": [copy.deepcopy(n) for n in g.history(a["id"])]})
|
||||
assert [n["content"] for n in g.history(a["id"])] == ["v1", "v2"]
|
||||
|
||||
# ══ migrate_flat ═════════════════════════════════════════════════════════
|
||||
def mig_ids(g, n):
|
||||
"""The ids the oracle's migrate consumed (the last n arrivals)."""
|
||||
return list(g.order[len(g.order) - n:]) if n else []
|
||||
|
||||
|
||||
flat = [{"role": "user", "content": "q1"}, {"role": "assistant", "content": "a1"}]
|
||||
g = mg.MessageGraph.migrate_flat(copy.deepcopy(flat), path=TMP / "mig_basic.json")
|
||||
case("migrate_flat", "migrate-basic-chain",
|
||||
{"text": None, "transcript": copy.deepcopy(flat),
|
||||
"ids": mig_ids(g, 2), "tss": [CLOCK, CLOCK]},
|
||||
{"graph": wire(g)})
|
||||
assert [g.nodes[i]["parent"] for i in g.order] == [None, g.order[0]]
|
||||
|
||||
# empty transcript
|
||||
g = mg.MessageGraph.migrate_flat([], path=TMP / "mig_empty.json")
|
||||
case("migrate_flat", "migrate-empty-transcript",
|
||||
{"text": None, "transcript": [], "ids": [], "tss": []}, {"graph": wire(g)})
|
||||
|
||||
# missing role/content keys -> system / "" defaults
|
||||
flat = [{"role": "user"}, {}, {"content": "no role"}]
|
||||
g = mg.MessageGraph.migrate_flat(copy.deepcopy(flat), path=TMP / "mig_defaults.json")
|
||||
case("migrate_flat", "migrate-missing-keys-default-system-empty",
|
||||
{"text": None, "transcript": copy.deepcopy(flat), "ids": mig_ids(g, 3),
|
||||
"tss": [CLOCK] * 3},
|
||||
{"graph": wire(g)})
|
||||
assert [g.nodes[i]["role"] for i in g.order] == ["user", "system", "system"]
|
||||
assert [g.nodes[i]["content"] for i in g.order] == ["", "", "no role"]
|
||||
|
||||
# existing file seeds the graph first, migration appends after
|
||||
seed_doc = {"schema": 1, "nodes": [
|
||||
{"id": "eee000000005", "kind": "message", "role": "user", "content": "old",
|
||||
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
||||
]}
|
||||
_seed = _err_file("mig_seed", json.dumps(seed_doc))
|
||||
g = mg.MessageGraph.migrate_flat([{"role": "user", "content": "new"}],
|
||||
path=_seed)
|
||||
case("migrate_flat", "migrate-existing-file-seed",
|
||||
{"text": json.dumps(seed_doc), "transcript": [{"role": "user", "content": "new"}],
|
||||
"ids": mig_ids(g, 1), "tss": [CLOCK]},
|
||||
{"graph": wire(g)})
|
||||
|
||||
# ══ cue ══════════════════════════════════════════════════════════════════
|
||||
g = fresh("cue_plain")
|
||||
a = g.add("assistant", "v1")
|
||||
plain = copy.deepcopy(g.nodes[a["id"]])
|
||||
case("cue", "cue-plain-node", {"node": plain}, {"result": ""})
|
||||
assert mg.MessageGraph.cue(plain) == ""
|
||||
|
||||
rev = g.revise(a["id"], "v2")
|
||||
case("cue", "cue-revision-replaces",
|
||||
{"node": copy.deepcopy(g.nodes[rev["id"]])},
|
||||
{"result": f"replaces {a['id'][:8]}"})
|
||||
assert mg.MessageGraph.cue(g.nodes[rev["id"]]) == f"replaces {a['id'][:8]}"
|
||||
|
||||
case("cue", "cue-superseded-by",
|
||||
{"node": copy.deepcopy(g.nodes[a["id"]])},
|
||||
{"result": f"superseded by {rev['id'][:8]}"})
|
||||
assert mg.MessageGraph.cue(g.nodes[a["id"]]) == f"superseded by {rev['id'][:8]}"
|
||||
|
||||
g = fresh("cue_branch")
|
||||
a = g.add("user", "root")
|
||||
head = g.branch(a["id"])
|
||||
case("cue", "cue-branch-head",
|
||||
{"node": copy.deepcopy(g.nodes[head["id"]])},
|
||||
{"result": f"branched from {a['id'][:8]}"})
|
||||
assert mg.MessageGraph.cue(g.nodes[head["id"]]) == f"branched from {a['id'][:8]}"
|
||||
|
||||
# falsy superseded_by "" is NOT a cue
|
||||
node = copy.deepcopy(plain)
|
||||
node["superseded_by"] = ""
|
||||
case("cue", "cue-falsy-superseded-by-empty", {"node": node}, {"result": ""})
|
||||
assert mg.MessageGraph.cue(node) == ""
|
||||
|
||||
# first supersedes/branches_from edge in iteration order wins
|
||||
node = copy.deepcopy(plain)
|
||||
node["edges"] = [{"type": "parent", "to": "aaa000000001"},
|
||||
{"type": "supersedes", "to": "bbb000000002"}]
|
||||
case("cue", "cue-first-supersedes-edge-wins", {"node": node},
|
||||
{"result": "replaces bbb00000"})
|
||||
assert mg.MessageGraph.cue(node) == "replaces bbb00000"
|
||||
node2 = copy.deepcopy(plain)
|
||||
node2["edges"] = [{"type": "branches_from", "to": "ccc000000003"},
|
||||
{"type": "supersedes", "to": "bbb000000002"}]
|
||||
case("cue", "cue-branches-from-beats-later-supersedes", {"node": node2},
|
||||
{"result": "branched from ccc00000"})
|
||||
assert mg.MessageGraph.cue(node2) == "branched from ccc00000"
|
||||
|
||||
# ══ save ═════════════════════════════════════════════════════════════════
|
||||
# basic save: doc shape + exact file bytes (json.dumps indent=2)
|
||||
g = fresh("save_basic")
|
||||
a = g.add("user", "persist me")
|
||||
g.save()
|
||||
doc = {"schema": mg.SCHEMA, "nodes": [g.nodes[i] for i in g.order]}
|
||||
case("save", "save-basic-doc-and-bytes",
|
||||
{"graph": {"nodes": {a["id"]: copy.deepcopy(g.nodes[a["id"]])},
|
||||
"order": [a["id"]]}},
|
||||
{"doc": copy.deepcopy(doc), "file": (TMP / "save_basic.json").read_text()})
|
||||
assert (TMP / "save_basic.json").read_text() == json.dumps(doc, indent=2)
|
||||
|
||||
# save creates missing parent dirs
|
||||
deep = TMP / "deep" / "nested" / "dir" / "g.json"
|
||||
g2 = mg.MessageGraph(path=deep)
|
||||
g2.add("user", "deep")
|
||||
g2.save()
|
||||
doc2 = {"schema": mg.SCHEMA, "nodes": [g2.nodes[i] for i in g2.order]}
|
||||
case("save", "save-creates-parent-dirs",
|
||||
{"graph": wire(g2)}, {"doc": copy.deepcopy(doc2), "file": deep.read_text()})
|
||||
assert deep.exists()
|
||||
|
||||
# round trip: save -> fresh load reproduces nodes AND order
|
||||
g3 = mg.MessageGraph(path=TMP / "save_basic.json")
|
||||
case("load", "load-save-round-trip",
|
||||
{"text": (TMP / "save_basic.json").read_text()}, {"graph": wire(g3)})
|
||||
assert g3.order == g.order
|
||||
assert g3.nodes[a["id"]]["content"] == "persist me"
|
||||
|
||||
# ══ oracle health (the repo's own battery must pass on the oracle) ══════
|
||||
import unittest # noqa: E402
|
||||
|
||||
suite = unittest.defaultTestLoader.discover(
|
||||
str(Path(__file__).resolve().parents[1] / "tests"),
|
||||
pattern="test_message_graph.py")
|
||||
runner = unittest.TextTestRunner(verbosity=0)
|
||||
if not runner.run(suite).wasSuccessful():
|
||||
raise AssertionError("oracle regression: test_message_graph.py failed")
|
||||
|
||||
# ══ write ════════════════════════════════════════════════════════════════
|
||||
truth = {
|
||||
"frozen": "message_graph load/save/add/tool_call/tool_result/revise/"
|
||||
"branch/lineage/history/migrate_flat/cue",
|
||||
"oracle": "src/rune/message_graph.py",
|
||||
"note": "Graph wire: {nodes: {id: node} insertion-ordered, order: [ids]}. "
|
||||
"Nondeterminism seams (uuid4 hex id, _now timestamp) are facade "
|
||||
"arguments — frozen CLOCK and sequential deadbeefNNNN ids replay "
|
||||
"verbatim. save() builds the doc in the kernel; the file write "
|
||||
"(mkdir + json.dumps indent=2) and tool_call's json.dumps content "
|
||||
"stay in the facade (serialization isomorphism).",
|
||||
}
|
||||
for gname in GROUPS:
|
||||
truth[gname] = groups[gname]
|
||||
|
||||
out = Path(os.environ.get("MG_TRUTH_OUT", "/tmp/mg_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,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_outcome_ledger_truth.py — outcome_ledger dichotomy wave truth battery
|
||||
(#2479 tranche). Drives the LIVE python oracle (RUNE_OL_CORE=python pinned)
|
||||
through every public function and branch, and freezes the observations to
|
||||
src/rust/outcome_ledger/src/frozen_to_truth.json.
|
||||
|
||||
Public surface exercised:
|
||||
* OutcomeLedger.__init__ (fresh db + reopen-existing branches)
|
||||
* OutcomeLedger.record (all field branches: success/fail, correction,
|
||||
floats, defaults)
|
||||
* OutcomeLedger.get_stats (no-family branch AND task_family branch,
|
||||
empty-db zero branch AND populated branch)
|
||||
* OutcomeLedger.get_model_stats (hit and miss)
|
||||
* OutcomeRecord.to_dict (full record)
|
||||
(test_ledger stays a facade-level python self-test — covered by live
|
||||
parity, not by the kernel battery.)
|
||||
|
||||
Timestamps are PINNED in the inputs, so observations are fully
|
||||
deterministic and need no masking.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
os.environ["RUNE_OL_CORE"] = "python"
|
||||
|
||||
from rune.outcome_ledger import OutcomeLedger, OutcomeRecord # noqa: E402
|
||||
|
||||
T0 = "2026-10-11T00:00:01+00:00"
|
||||
T1 = "2026-10-11T00:00:02+00:00"
|
||||
T2 = "2026-10-11T00:00:03+00:00"
|
||||
T3 = "2026-10-11T00:00:04+00:00"
|
||||
T4 = "2026-10-11T00:00:05+00:00"
|
||||
T5 = "2026-10-11T00:00:06+00:00"
|
||||
|
||||
|
||||
class Driver:
|
||||
"""Drives op scripts against the live oracle the way the truth was
|
||||
frozen — each scenario gets its own temp db path."""
|
||||
|
||||
def __init__(self, workdir: Path, n: int):
|
||||
self.path = str(workdir / f"ol_{n}.db")
|
||||
self.ledger = None
|
||||
|
||||
def step(self, op: dict):
|
||||
kind = op["op"]
|
||||
if kind == "new":
|
||||
if not op.get("reuse"):
|
||||
self.ledger = OutcomeLedger(self.path)
|
||||
else:
|
||||
# fresh instance over the SAME db (CREATE TABLE IF NOT
|
||||
# EXISTS idempotent branch)
|
||||
self.ledger = OutcomeLedger(self.path)
|
||||
return {"result": None}
|
||||
if kind == "record":
|
||||
self.ledger.record(OutcomeRecord(**op["record"]))
|
||||
return {"result": None}
|
||||
if kind == "to_dict":
|
||||
return {"result": OutcomeRecord(**op["record"]).to_dict()}
|
||||
if kind == "get_stats":
|
||||
fam = op.get("family")
|
||||
if fam is None:
|
||||
return {"result": self.ledger.get_stats()}
|
||||
return {"result": self.ledger.get_stats(fam)}
|
||||
if kind == "get_model_stats":
|
||||
return {"result": self.ledger.get_model_stats(op["model"])}
|
||||
raise ValueError(f"unknown op {kind}")
|
||||
|
||||
|
||||
def s1_empty_db(workdir):
|
||||
"""Empty-database branches of every stats path."""
|
||||
d = Driver(workdir, 1)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_stats", "family": "greeting"},
|
||||
{"op": "get_model_stats", "model": "qwen2.5:3b"},
|
||||
]
|
||||
return {"name": "s1_empty_db", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s2_record_and_stats(workdir):
|
||||
"""The oracle's own test_ledger data, plus every stats entry point."""
|
||||
d = Driver(workdir, 2)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T0, "task_family": "greeting", "model": "qwen2.5:3b",
|
||||
"provider": "ollama", "tier": "T1", "task_success": True,
|
||||
"latency_ms": 100.0}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T1, "task_family": "coding", "model": "qwen3:14b",
|
||||
"provider": "ollama", "tier": "T2", "task_success": True,
|
||||
"attempts": 2, "latency_ms": 500.0}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T2, "task_family": "coding", "model": "qwen3:14b",
|
||||
"provider": "ollama", "tier": "T2", "task_success": False,
|
||||
"user_correction": True, "latency_ms": 600.0}},
|
||||
{"op": "get_stats", "family": "greeting"},
|
||||
{"op": "get_stats", "family": "coding"},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_model_stats", "model": "qwen3:14b"},
|
||||
{"op": "get_model_stats", "model": "unknown-model"},
|
||||
]
|
||||
return {"name": "s2_record_and_stats", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s3_full_record_to_dict(workdir):
|
||||
"""OutcomeRecord.to_dict with every field set; float-heavy stats."""
|
||||
d = Driver(workdir, 3)
|
||||
full = {
|
||||
"timestamp": T3, "task_family": "review", "model": "gpt-5-mini",
|
||||
"provider": "openai", "tier": "T3", "prompt_tokens": 1200,
|
||||
"output_tokens": 340, "cached_tokens": 512, "latency_ms": 123.75,
|
||||
"attempts": 1, "verification_result": "pass", "user_correction": False,
|
||||
"task_success": True, "estimated_cost": 0.0025,
|
||||
}
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "to_dict", "record": full},
|
||||
{"op": "record", "record": full},
|
||||
{"op": "record", "record": {**full, "timestamp": T4,
|
||||
"estimated_cost": 0.0075,
|
||||
"verification_result": "fail",
|
||||
"task_success": False}},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_stats", "family": "review"},
|
||||
{"op": "get_model_stats", "model": "gpt-5-mini"},
|
||||
]
|
||||
return {"name": "s3_full_record_to_dict", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s4_persistence_reopen(workdir):
|
||||
"""A fresh OutcomeLedger over an existing db — rows survive, DDL is
|
||||
idempotent."""
|
||||
d = Driver(workdir, 4)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T0, "task_family": "port", "model": "qwen2.5:3b",
|
||||
"provider": "ollama", "tier": "T1", "task_success": True,
|
||||
"latency_ms": 42.5}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T1, "task_family": "port", "model": "qwen3:14b",
|
||||
"provider": "ollama", "tier": "T2", "task_success": False,
|
||||
"user_correction": True, "attempts": 3, "latency_ms": 900.0}},
|
||||
{"op": "new", "reuse": True},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_stats", "family": "port"},
|
||||
{"op": "get_model_stats", "model": "qwen2.5:3b"},
|
||||
]
|
||||
return {"name": "s4_persistence_reopen", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def s5_averages_and_rates(workdir):
|
||||
"""AVG/rate arithmetic: fractional averages, 0%/50%/100% rates, both
|
||||
correction branches."""
|
||||
d = Driver(workdir, 5)
|
||||
ops = [
|
||||
{"op": "new"},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T0, "task_family": "ops", "model": "m1",
|
||||
"task_success": True, "attempts": 1, "latency_ms": 10.5,
|
||||
"estimated_cost": 0.0}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T1, "task_family": "ops", "model": "m1",
|
||||
"task_success": False, "user_correction": True, "attempts": 2,
|
||||
"latency_ms": 20.25, "estimated_cost": 1.5}},
|
||||
{"op": "record", "record": {
|
||||
"timestamp": T2, "task_family": "ops", "model": "m2",
|
||||
"task_success": True, "attempts": 3, "latency_ms": 30.125,
|
||||
"user_correction": True, "estimated_cost": 2.25}},
|
||||
{"op": "get_stats"},
|
||||
{"op": "get_model_stats", "model": "m1"},
|
||||
{"op": "get_model_stats", "model": "m2"},
|
||||
{"op": "get_stats", "family": "ops"},
|
||||
]
|
||||
return {"name": "s5_averages_and_rates", "ops": ops, "obs": [d.step(o) for o in ops]}
|
||||
|
||||
|
||||
def main():
|
||||
out = {
|
||||
"module": "outcome_ledger",
|
||||
"seed": 2479,
|
||||
"oracle": "src/rune/outcome_ledger.py",
|
||||
"pin": "RUNE_OL_CORE=python",
|
||||
"generated": "2026-10-11",
|
||||
"scenarios": [],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
workdir = Path(td)
|
||||
for gen in (s1_empty_db, s2_record_and_stats, s3_full_record_to_dict,
|
||||
s4_persistence_reopen, s5_averages_and_rates):
|
||||
out["scenarios"].append(gen(workdir))
|
||||
dest = REPO / "src" / "rust" / "outcome_ledger" / "src" / "frozen_to_truth.json"
|
||||
dest.write_text(json.dumps(out, indent=1) + "\n")
|
||||
n_obs = sum(len(s["obs"]) for s in out["scenarios"])
|
||||
print(f"wrote {len(out['scenarios'])} scenarios / {n_obs} observations to {dest}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,567 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Truth generator for the prompt_sync dichotomy port (#2479 tranche).
|
||||
|
||||
Runs the LIVE Python oracle (src/rune/prompt_sync.py) in a controlled
|
||||
sandbox (RUNE_HOME/RUNE_MEMORY_DIR/RUNE_STORE_DB/HOME) across every public
|
||||
function and every branch, and freezes inputs + expected outputs to
|
||||
frozen_ps_truth.json (consumed VERBATIM by the Rust battery).
|
||||
|
||||
POISONED-TRUTH LAW: RUNE_PS_CORE=python is pinned at the top — a re-run
|
||||
after facade delegation must record oracle output, never kernel output.
|
||||
|
||||
Kernel input discipline: the Rust kernel is PURE — it receives rows /
|
||||
file contents / voice body / static strings as JSON and returns strings
|
||||
and dicts. All I/O (sqlite, rune.md, defining doc, voice_sync chain)
|
||||
stays in the Python facade. Every case records the kernel-shaped input.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["RUNE_PS_CORE"] = "python" # poisoned-truth law: pin the oracle
|
||||
|
||||
import json
|
||||
import random
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
SANDBOX = Path(tempfile.mkdtemp(prefix="ps_truth_"))
|
||||
(SANDBOX / "memory" / "store").mkdir(parents=True)
|
||||
(SANDBOX / ".mimocode" / "agents").mkdir(parents=True)
|
||||
|
||||
os.environ["RUNE_HOME"] = str(SANDBOX)
|
||||
os.environ["RUNE_MEMORY_DIR"] = str(SANDBOX / "memory")
|
||||
os.environ["RUNE_STORE_DB"] = str(SANDBOX / "memory" / "store" / "memory.db")
|
||||
os.environ["HOME"] = str(SANDBOX)
|
||||
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import prompt_sync # noqa: E402
|
||||
import voice_sync # noqa: E402
|
||||
|
||||
STORE = os.environ["RUNE_STORE_DB"]
|
||||
RUNE_MD = prompt_sync.RUNE_MD
|
||||
DEFINING = str(SANDBOX / "memory" / "rune_defining_document.md")
|
||||
|
||||
SCHEMA = """
|
||||
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
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
chunk_id TEXT PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
importance REAL DEFAULT 0.5,
|
||||
ingested_at TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
cases = []
|
||||
|
||||
|
||||
def add(name, fn, payload, expected):
|
||||
cases.append({"name": name, "fn": fn, "input": payload, "expected": expected})
|
||||
|
||||
|
||||
def fresh_db(entries=(), chunks=()):
|
||||
Path(STORE).unlink(missing_ok=True)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.executescript(SCHEMA)
|
||||
for e in entries:
|
||||
db.execute(
|
||||
"INSERT INTO entries (text, importance, confidence, created_at, topic, status, always_on)"
|
||||
" VALUES (?,?,?,?,?,?,?)",
|
||||
(
|
||||
e["text"], e.get("importance", 0.5), e.get("confidence", 0.7),
|
||||
"2026-10-11T00:00:00Z", e["topic"], e.get("status", "active"),
|
||||
e.get("always_on", 1),
|
||||
),
|
||||
)
|
||||
for c in chunks:
|
||||
db.execute(
|
||||
"INSERT INTO chunks (chunk_id, doc_id, text, content_hash, importance, ingested_at)"
|
||||
" VALUES (?,?,?,?,?,?)",
|
||||
(c["chunk_id"], c["doc_id"], c["text"], "h", 0.5, "2026-10-11T00:00:00Z"),
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def fresh_prompt(content=None):
|
||||
if content is None:
|
||||
Path(RUNE_MD).unlink(missing_ok=True)
|
||||
else:
|
||||
Path(RUNE_MD).write_text(content)
|
||||
|
||||
|
||||
def fresh_defining(content=None):
|
||||
p = Path(DEFINING)
|
||||
if content is None:
|
||||
p.unlink(missing_ok=True)
|
||||
else:
|
||||
p.write_text(content)
|
||||
|
||||
|
||||
def rows_by_topic(topics):
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
out = {}
|
||||
for t in topics:
|
||||
out[t] = [dict(r) for r in prompt_sync.get_entries_by_topic(db, t)]
|
||||
db.close()
|
||||
return out
|
||||
|
||||
|
||||
def voice_body_from_db():
|
||||
"""The voice chain exactly as build_prompt runs it (stays facade-side
|
||||
in the port — voice_sync is a separate module)."""
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
register = voice_sync.get_delivery_register_from_db(db)
|
||||
db.close()
|
||||
if not register:
|
||||
return None
|
||||
parsed = voice_sync.extract_voice_section(register)
|
||||
return voice_sync.generate_voice_section(parsed)
|
||||
|
||||
|
||||
def kernel_build_input(entries=(), chunks=(), defining=None, prompt=None):
|
||||
"""Record the kernel-shaped input for a build/sync/check/diff case."""
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(prompt)
|
||||
rt = rows_by_topic(["constitution", "identity", "operating", "human"])
|
||||
vb = voice_body_from_db()
|
||||
dc = None if defining is None else Path(DEFINING).read_text()
|
||||
cur = None if prompt is None else Path(RUNE_MD).read_text()
|
||||
return {
|
||||
"constitution_rows": rt["constitution"],
|
||||
"identity_rows": rt["identity"],
|
||||
"operating_rows": rt["operating"],
|
||||
"human_rows": rt["human"],
|
||||
"voice_body": vb,
|
||||
"defining_content": dc,
|
||||
"current": cur,
|
||||
}
|
||||
|
||||
|
||||
# ── entry fixtures ───────────────────────────────────────────────────────
|
||||
|
||||
def E(topic, text, importance=0.5, confidence=0.7, **kw):
|
||||
d = {"topic": topic, "text": text, "importance": importance, "confidence": confidence}
|
||||
d.update(kw)
|
||||
return d
|
||||
|
||||
|
||||
CONSTITUTION_SET = [
|
||||
("Never accept placeholder phase output as execution evidence.", 0.95),
|
||||
(" (_source_: some citation here) Store is source of truth.", 0.9),
|
||||
("Consent approves the finalized canaries [source: pipeline doc].", 0.85),
|
||||
("short", 0.8), # len<=15 → filtered
|
||||
("", 0.75), # empty → filtered
|
||||
("Truth first. Pressure point first.", 0.7),
|
||||
]
|
||||
OPERATING_SET = [
|
||||
(" (_source_: x) One clean sentence beats five padded ones.", 0.9),
|
||||
("No praise reflex [source: register].", 0.85),
|
||||
("tiny", 0.8),
|
||||
("Useful beats impressive. Progress beats performance.", 0.75),
|
||||
] + [(f"Operating rule number {i} for fill.", 0.5 - i * 0.01) for i in range(18)]
|
||||
IDENTITY_SET = [
|
||||
("My name is Rune — built, not borrowed.", 0.9),
|
||||
("", 0.8), # empty text skipped
|
||||
("The system is architecture, not weather.", 0.7),
|
||||
]
|
||||
HUMAN_SET = [
|
||||
("Nick prefers direct delivery with teeth.", 0.9),
|
||||
("", 0.5),
|
||||
("Arch PC: modify freely. Pi: check first.", 0.8),
|
||||
]
|
||||
|
||||
# ── get_entries_by_topic (kernel + sqlite read) ──────────────────────────
|
||||
|
||||
FIXTURE_ALL = (
|
||||
[E("constitution", t, i) for t, i in CONSTITUTION_SET]
|
||||
+ [E("identity", t, i) for t, i in IDENTITY_SET]
|
||||
+ [E("operating", t, i) for t, i in OPERATING_SET]
|
||||
+ [E("human", t, i) for t, i in HUMAN_SET]
|
||||
+ [E("constitution", "inactive but matching topic", 0.99, status="retired"),
|
||||
E("constitution", "always_on=0 excluded", 0.99, always_on=0),
|
||||
E("other", "different topic", 0.99)]
|
||||
)
|
||||
fresh_db(FIXTURE_ALL)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
for topic in ("constitution", "identity", "operating", "human", "missing-topic"):
|
||||
add(f"get_entries_by_topic:{topic}", "get_entries_by_topic",
|
||||
{"fixture": FIXTURE_ALL, "topic": topic},
|
||||
[dict(r) for r in prompt_sync.get_entries_by_topic(db, topic)])
|
||||
db.close()
|
||||
|
||||
# importance tie → stable order (insertion order preserved)
|
||||
FIXTURE_TIE = [E("tie", "first inserted", 0.5), E("tie", "second inserted", 0.5),
|
||||
E("tie", "third inserted", 0.9)]
|
||||
fresh_db(FIXTURE_TIE)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
add("get_entries_by_topic:tie-stable", "get_entries_by_topic",
|
||||
{"fixture": FIXTURE_TIE, "topic": "tie"},
|
||||
[dict(r) for r in prompt_sync.get_entries_by_topic(db, "tie")])
|
||||
db.close()
|
||||
|
||||
# NULL confidence passes through as null
|
||||
FIXTURE_NULL = [{"topic": "nullconf", "text": "some entry", "importance": 0.4, "confidence": None}]
|
||||
fresh_db(FIXTURE_NULL)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
add("get_entries_by_topic:null-confidence", "get_entries_by_topic",
|
||||
{"fixture": FIXTURE_NULL, "topic": "nullconf"},
|
||||
[dict(r) for r in prompt_sync.get_entries_by_topic(db, "nullconf")])
|
||||
db.close()
|
||||
|
||||
# ── section builders (branch cases) ──────────────────────────────────────
|
||||
# The oracle section fns call get_entries_by_topic(db, topic) internally —
|
||||
# capture through a proxy db that serves canned rows per topic.
|
||||
|
||||
|
||||
class ProxyDB:
|
||||
def __init__(self, table):
|
||||
self.table = table
|
||||
|
||||
def execute(self, sql, params):
|
||||
return _Rows(self.table.get(params[0], []))
|
||||
|
||||
|
||||
class _Rows(list):
|
||||
def fetchall(self):
|
||||
return [dict(r) for r in self]
|
||||
|
||||
|
||||
fresh_db([E("constitution", t, i) for t, i in CONSTITUTION_SET])
|
||||
tbl = rows_by_topic(["constitution"])
|
||||
add("get_constitution:full", "get_constitution", {"entries": tbl["constitution"]},
|
||||
prompt_sync.get_constitution(ProxyDB(tbl)))
|
||||
|
||||
add("get_constitution:empty", "get_constitution", {"entries": []}, None)
|
||||
add("get_identity:empty", "get_identity", {"entries": []}, None)
|
||||
add("get_operating:empty", "get_operating", {"entries": []}, None)
|
||||
add("get_human:empty", "get_human", {"entries": []}, None)
|
||||
|
||||
fresh_db([E("identity", t, i) for t, i in IDENTITY_SET])
|
||||
tbl = rows_by_topic(["identity"])
|
||||
add("get_identity:full", "get_identity", {"entries": tbl["identity"]},
|
||||
prompt_sync.get_identity(ProxyDB(tbl)))
|
||||
|
||||
fresh_db([E("operating", t, i) for t, i in OPERATING_SET])
|
||||
tbl = rows_by_topic(["operating"])
|
||||
add("get_operating:full", "get_operating", {"entries": tbl["operating"]},
|
||||
prompt_sync.get_operating(ProxyDB(tbl)))
|
||||
|
||||
fresh_db([E("human", t, i) for t, i in HUMAN_SET])
|
||||
tbl = rows_by_topic(["human"])
|
||||
add("get_human:full", "get_human", {"entries": tbl["human"]},
|
||||
prompt_sync.get_human(ProxyDB(tbl)))
|
||||
|
||||
many = [(f"Constitution entry number {i} with enough length.", 1.0 - i * 0.01) for i in range(30)]
|
||||
fresh_db([E("constitution", t, i) for t, i in many])
|
||||
tbl = rows_by_topic(["constitution"])
|
||||
add("get_constitution:top25", "get_constitution", {"entries": tbl["constitution"]},
|
||||
prompt_sync.get_constitution(ProxyDB(tbl)))
|
||||
|
||||
# ── get_defining_principles (extraction is kernel; read is facade) ───────
|
||||
|
||||
fresh_defining(None)
|
||||
add("get_defining_principles:no-file", "get_defining_principles",
|
||||
{"content": None}, prompt_sync.get_defining_principles())
|
||||
|
||||
DOC = """# Defining Document
|
||||
|
||||
## THE VISION
|
||||
A stateful assistant that remembers and grows.
|
||||
The store is the source of truth.
|
||||
|
||||
### Subsection stays out (starts with #)
|
||||
This line is under a ### — excluded by the # guard.
|
||||
|
||||
## PREAMBLE
|
||||
Not a key section — excluded even though lines follow.
|
||||
|
||||
## THE TRUST FRAMEWORK
|
||||
Consent before mutation. Never guessed referents.
|
||||
|
||||
## CURIOSITY
|
||||
Ask what the problem actually is.
|
||||
|
||||
## CURIOSITY LIMITS
|
||||
Curiosity never overrides consent.
|
||||
|
||||
|
||||
## WHAT SENTIENCE MEANS
|
||||
Synthetic does not mean simulated.
|
||||
|
||||
## WHATEVER
|
||||
Tail section, excluded.
|
||||
"""
|
||||
fresh_defining(DOC)
|
||||
add("get_defining_principles:key-sections", "get_defining_principles",
|
||||
{"content": DOC}, prompt_sync.get_defining_principles())
|
||||
|
||||
DOC_WS = "## CURIOSITY\nline one\n \nline two\n"
|
||||
fresh_defining(DOC_WS)
|
||||
add("get_defining_principles:whitespace-line", "get_defining_principles",
|
||||
{"content": DOC_WS}, prompt_sync.get_defining_principles())
|
||||
|
||||
DOC_MANY = "## THE VISION\n" + "\n".join(f"principle line {i}" for i in range(30))
|
||||
fresh_defining(DOC_MANY)
|
||||
add("get_defining_principles:top20", "get_defining_principles",
|
||||
{"content": DOC_MANY}, prompt_sync.get_defining_principles())
|
||||
|
||||
fresh_defining("## NOTHING RELEVANT\nsome text\n")
|
||||
add("get_defining_principles:no-key-sections", "get_defining_principles",
|
||||
{"content": "## NOTHING RELEVANT\nsome text\n"},
|
||||
prompt_sync.get_defining_principles())
|
||||
|
||||
fresh_defining("")
|
||||
add("get_defining_principles:empty-file", "get_defining_principles",
|
||||
{"content": ""}, prompt_sync.get_defining_principles())
|
||||
|
||||
# ── build_prompt ─────────────────────────────────────────────────────────
|
||||
|
||||
VOICE_REGISTER = (
|
||||
"THE RUNE VOICE — DELIVERY REGISTER (canonical)\n"
|
||||
"\n"
|
||||
"- **Thompson (moral economy):** legitimacy flows from lived fairness, not rules from above.\n"
|
||||
"- **Graeber:** bureaucracy is a technology of mutual mystification; name it.\n"
|
||||
"\n"
|
||||
"TECHNICAL DELIVERY\n"
|
||||
"- Precision is a form of care; tables and code get the same voice discipline.\n"
|
||||
"- No hedging stack in status reports.\n"
|
||||
"\n"
|
||||
"CALIBRATION\n"
|
||||
"- Push back harder when the stakes are higher.\n"
|
||||
"- One rhetorical question per engagement, not three.\n"
|
||||
"- Zero ornamental structure.\n"
|
||||
"- Fourth calibration note beyond the top-three cut.\n"
|
||||
"\n"
|
||||
"HOW IT BLENDS\n"
|
||||
"- The substance leads; the voice shapes HOW.\n"
|
||||
"- Direct, warm, with teeth.\n"
|
||||
"\n"
|
||||
"WHEN THE REGISTER SLIPS\n"
|
||||
"- Recalibrate against this register, not against habit.\n"
|
||||
)
|
||||
assert len(VOICE_REGISTER) > 200 and "DELIVERY REGISTER" in VOICE_REGISTER and "RUNE" in VOICE_REGISTER and "VOICE" in VOICE_REGISTER
|
||||
|
||||
ALL_ENTRIES = (
|
||||
[E("constitution", t, i) for t, i in CONSTITUTION_SET if len(t.strip()) > 15]
|
||||
+ [E("identity", t, i) for t, i in IDENTITY_SET if t.strip()]
|
||||
+ [E("operating", t, i) for t, i in OPERATING_SET if len(t.strip()) > 15]
|
||||
+ [E("human", t, i) for t, i in HUMAN_SET if t.strip()]
|
||||
)
|
||||
VOICE_CHUNKS = [{"chunk_id": "c1", "doc_id": "d1", "text": VOICE_REGISTER}]
|
||||
|
||||
|
||||
def build_case(name, entries=(), chunks=(), defining=None):
|
||||
kinput = kernel_build_input(entries, chunks, defining)
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(None)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
out = prompt_sync.build_prompt(db)
|
||||
db.close()
|
||||
add(name, "build_prompt", kinput, out)
|
||||
|
||||
|
||||
build_case("build_prompt:empty-store")
|
||||
build_case("build_prompt:full", ALL_ENTRIES, VOICE_CHUNKS, DOC)
|
||||
build_case("build_prompt:constitution-only", [E("constitution", "Never accept placeholder phase output as execution evidence.", 0.95)])
|
||||
build_case("build_prompt:voice-only", [], VOICE_CHUNKS)
|
||||
build_case("build_prompt:voice-noise", [], [{"chunk_id": "c2", "doc_id": "d1", "text": "DELIVERY REGISTER mention but too short"}])
|
||||
|
||||
# ── sync_prompt (facade I/O; recorded for live parity) ───────────────────
|
||||
|
||||
def sync_case(name, entries=(), chunks=(), defining=None, prompt=None):
|
||||
kinput = kernel_build_input(entries, chunks, defining, prompt)
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(prompt)
|
||||
out = prompt_sync.sync_prompt()
|
||||
if isinstance(out, dict) and "timestamp" in out:
|
||||
out = dict(out, timestamp="TIMESTAMP")
|
||||
add(name, "sync_prompt", kinput, out)
|
||||
|
||||
|
||||
sync_case("sync_prompt:creates-file", ALL_ENTRIES, VOICE_CHUNKS, DOC, None)
|
||||
sync_case("sync_prompt:overwrites", ALL_ENTRIES, VOICE_CHUNKS, DOC, "old content\n")
|
||||
|
||||
# ── check_sync ───────────────────────────────────────────────────────────
|
||||
|
||||
def check_case(name, entries=(), chunks=(), defining=None, prompt=None):
|
||||
kinput = kernel_build_input(entries, chunks, defining, prompt)
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(prompt)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
kinput["new_prompt"] = prompt_sync.build_prompt(db)
|
||||
db.close()
|
||||
add(name, "check_sync", kinput, prompt_sync.check_sync())
|
||||
|
||||
|
||||
check_case("check_sync:no-file", ALL_ENTRIES, VOICE_CHUNKS, DOC, None)
|
||||
fresh_db(ALL_ENTRIES, VOICE_CHUNKS)
|
||||
fresh_defining(DOC)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
synced_prompt = prompt_sync.build_prompt(db)
|
||||
db.close()
|
||||
check_case("check_sync:in-sync", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt)
|
||||
check_case("check_sync:out-of-sync", ALL_ENTRIES, VOICE_CHUNKS, DOC, "stale prompt\n")
|
||||
check_case("check_sync:strip-equal", ALL_ENTRIES, VOICE_CHUNKS, DOC, "\n\n" + synced_prompt + "\n\n ")
|
||||
|
||||
# ── diff_prompt ──────────────────────────────────────────────────────────
|
||||
|
||||
def diff_case(name, entries=(), chunks=(), defining=None, prompt=None):
|
||||
kinput = kernel_build_input(entries, chunks, defining, prompt)
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(prompt)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
kinput["new_prompt"] = prompt_sync.build_prompt(db)
|
||||
db.close()
|
||||
add(name, "diff_prompt", kinput, prompt_sync.diff_prompt())
|
||||
|
||||
|
||||
diff_case("diff_prompt:no-file", ALL_ENTRIES, VOICE_CHUNKS, DOC, None)
|
||||
diff_case("diff_prompt:no-differences", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt)
|
||||
diff_case("diff_prompt:real-diff", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt)
|
||||
|
||||
mutated = list(synced_prompt.split("\n"))
|
||||
for k in range(12):
|
||||
op = ["ins", "del", "mod"][k % 3]
|
||||
i = (k * 37 + 11) % len(mutated)
|
||||
if op == "ins":
|
||||
mutated.insert(i, f"- **inserted calibration line {k}**")
|
||||
elif op == "del" and len(mutated) > 5:
|
||||
del mutated[i]
|
||||
else:
|
||||
mutated[i] = mutated[i] + " (edited)"
|
||||
diff_case("diff_prompt:multi-hunk", ALL_ENTRIES, VOICE_CHUNKS, DOC, "\n".join(mutated))
|
||||
diff_case("diff_prompt:no-trailing-newline", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt.rstrip("\n"))
|
||||
diff_case("diff_prompt:carriage-returns", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt.replace("\n", "\r\n"))
|
||||
diff_case("diff_prompt:vertical-tab", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt.replace("\n\n", "\n\x0b\n", 1))
|
||||
|
||||
# ── handle_hook ──────────────────────────────────────────────────────────
|
||||
|
||||
def hook_case(name, entries=(), chunks=(), defining=None, prompt=None, event="on_session_end"):
|
||||
# capture the sync_result the hook WOULD produce (identical fresh state),
|
||||
# then restore the pre-hook file state so handle_hook sees the same input.
|
||||
sync_result = None
|
||||
check_synced = None
|
||||
if event == "on_session_end":
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(prompt)
|
||||
check_synced = prompt_sync.check_sync()["synced"]
|
||||
if not check_synced:
|
||||
out = prompt_sync.sync_prompt()
|
||||
if "timestamp" in out:
|
||||
out = dict(out, timestamp="TIMESTAMP")
|
||||
sync_result = out
|
||||
fresh_db(list(entries), list(chunks))
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(prompt)
|
||||
out = prompt_sync.handle_hook(event, {})
|
||||
if isinstance(out, dict) and isinstance(out.get("details"), dict) and "timestamp" in out["details"]:
|
||||
out = dict(out, details=dict(out["details"], timestamp="TIMESTAMP"))
|
||||
add(name, "handle_hook",
|
||||
{"event": event, "check_synced": check_synced, "sync_result": sync_result},
|
||||
out)
|
||||
|
||||
|
||||
hook_case("handle_hook:session-end-unsynced", ALL_ENTRIES, VOICE_CHUNKS, DOC, None)
|
||||
hook_case("handle_hook:session-end-insync", ALL_ENTRIES, VOICE_CHUNKS, DOC, synced_prompt)
|
||||
hook_case("handle_hook:other-event", ALL_ENTRIES, VOICE_CHUNKS, DOC, None, event="on_session_start")
|
||||
|
||||
# ── fuzz: random stores through build_prompt + diff (parity-hardening) ───
|
||||
|
||||
rng = random.Random(2479)
|
||||
WORDS = ["voice", "constitution", "build", "rule", "nick", "direct", "warm",
|
||||
"code", "test", "rune", "store", "prompt", "pipeline", "section",
|
||||
"the", "a", "of", "and", " (_source_: cite) ", " [source: doc] ", "x"]
|
||||
for f in range(60):
|
||||
entries = []
|
||||
for topic in ("constitution", "identity", "operating", "human"):
|
||||
for _ in range(rng.randint(0, 12)):
|
||||
body = "".join(rng.choice(WORDS) for _ in range(rng.randint(1, 25)))
|
||||
entries.append(E(topic, body, rng.choice([0.1, 0.5, 0.5, 0.9, 0.9, 1.0]),
|
||||
rng.choice([0.3, 0.7, None]) if rng.random() < 0.2 else 0.7))
|
||||
defining = None
|
||||
if rng.random() < 0.6:
|
||||
defining = "".join(
|
||||
(f"## {rng.choice(['THE VISION', 'THE TRUST FRAMEWORK', 'CURIOSITY', 'SKIP ME'])}\n"
|
||||
if rng.random() < 0.25 else rng.choice(["", " ", f"line {rng.randint(0, 99)}\n", "### deep\n"]))
|
||||
for _ in range(rng.randint(0, 40)))
|
||||
kinput = kernel_build_input(entries, [], defining)
|
||||
fresh_db(entries, [])
|
||||
fresh_defining(defining)
|
||||
fresh_prompt(None)
|
||||
db = sqlite3.connect(STORE)
|
||||
db.row_factory = sqlite3.Row
|
||||
built = prompt_sync.build_prompt(db)
|
||||
db.close()
|
||||
add(f"fuzz-build:{f}", "build_prompt", kinput, built)
|
||||
if f % 3 == 0:
|
||||
mlines = built.split("\n")
|
||||
for _ in range(rng.randint(1, 20)):
|
||||
op = rng.choice(["ins", "del", "mod"])
|
||||
i = rng.randrange(len(mlines)) if mlines else 0
|
||||
if op == "ins":
|
||||
mlines.insert(i, rng.choice(["", "- inserted **thing**", "## Inserted Section"]))
|
||||
elif op == "del" and len(mlines) > 3:
|
||||
del mlines[i]
|
||||
else:
|
||||
mlines[i] = mlines[i][::-1] if mlines[i] else "reversed-empty"
|
||||
current = "\n".join(mlines)
|
||||
fresh_prompt(current)
|
||||
kinput2 = dict(kinput, current=current, new_prompt=built)
|
||||
add(f"fuzz-diff:{f}", "diff_prompt", kinput2, prompt_sync.diff_prompt())
|
||||
|
||||
# ── write ────────────────────────────────────────────────────────────────
|
||||
|
||||
doc = {
|
||||
"frozen": "2026-10-11",
|
||||
"oracle": "src/rune/prompt_sync.py",
|
||||
"seed": 2479,
|
||||
"static": {
|
||||
"STATIC_HEADER": prompt_sync.STATIC_HEADER,
|
||||
"STATIC_FOOTER": prompt_sync.STATIC_FOOTER,
|
||||
"PROMPT_SECTION": prompt_sync.build_pipeline_policy.PROMPT_SECTION,
|
||||
},
|
||||
"cases": cases,
|
||||
}
|
||||
dest = REPO / "src" / "rust" / "prompt_sync" / "src" / "frozen_ps_truth.json"
|
||||
dest.write_text(json.dumps(doc, indent=1, sort_keys=True) + "\n")
|
||||
print(f"frozen {len(cases)} cases → {dest}")
|
||||
print("sandbox:", SANDBOX)
|
||||
@@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_self_modify_truth.py — freeze/replay parity truth for self_modify
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/self_modify.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/self_modify/src/frozen_smod_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle functions delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_SMOD_CORE=python BEFORE any import
|
||||
(the RUNE_SM_CORE pin already belongs to shard_manager's core on master).
|
||||
|
||||
No-clock law: the wall clock is frozen by patching the `time` symbol in
|
||||
self_modify, so every propose record carries the frozen `at` stamp and
|
||||
replay is deterministic regardless of run date. The oracle's internal
|
||||
tempfile.mkdtemp is likewise pinned to a case-local path.
|
||||
|
||||
Deterministic-fixture law: git repos are built with fixed identity and
|
||||
fixed author/committer dates, so the seed commit SHA is identical across
|
||||
generator runs, replay runs and the Rust battery — the SHA is frozen
|
||||
LITERALLY, no placeholder. Machine-specific paths (repo dir, worktree dir,
|
||||
stub dir) are frozen as @REPO@ / @WORKTREE@ / @STUBDIR@ placeholders and
|
||||
substituted at replay time. Git-dependent scenarios declare their setup as
|
||||
data ("worktree:<branch>", "commit_in_worktree", "dirty_repo", plus the
|
||||
"stub" flag for the PATH-stubbed default gate command) so the generator,
|
||||
the Python replay and the Rust battery run the SAME recipe.
|
||||
|
||||
Raise-parity rows freeze {"raises": "<oracle exception>"}: the Python
|
||||
facade must raise the same exception; the Rust core (envelope law) must
|
||||
surface an error (the loader raises RuntimeError — documented divergence
|
||||
in exception TYPE only).
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
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", "self_modify", "src", "frozen_smod_truth.json"
|
||||
)
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
os.environ["RUNE_SMOD_CORE"] = "python" # poisoned-truth law (freeze mode)
|
||||
os.environ["RUNE_MEMORY_CORE"] = "python"
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import self_modify as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
del os.environ["RUNE_SMOD_CORE"] # facade must delegate to the Rust core
|
||||
import smod_loader # noqa: E402 (kernel-level replay side)
|
||||
|
||||
FROZEN_AT = "2026-10-11T12:00:00Z"
|
||||
FROZEN_EPOCH = int(datetime(2026, 10, 11, 12, 0, 0, tzinfo=timezone.utc).timestamp())
|
||||
GIT_ENV_FIXED = {
|
||||
"GIT_AUTHOR_DATE": "2026-10-11T12:00:00+00:00",
|
||||
"GIT_COMMITTER_DATE": "2026-10-11T12:00:00+00:00",
|
||||
}
|
||||
MISSING_REPO = "/nonexistent-selfmod-truth/repo"
|
||||
|
||||
|
||||
class FrozenTime:
|
||||
"""time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) → FROZEN_AT."""
|
||||
|
||||
@staticmethod
|
||||
def gmtime():
|
||||
return time.gmtime(FROZEN_EPOCH)
|
||||
|
||||
@staticmethod
|
||||
def strftime(fmt, t=None):
|
||||
return time.strftime(fmt, t if t is not None else FrozenTime.gmtime())
|
||||
|
||||
|
||||
oracle_mod.time = FrozenTime
|
||||
|
||||
|
||||
class FrozenTempfile:
|
||||
"""tempfile.mkdtemp(prefix="selfmod-") → a case-local, known path."""
|
||||
|
||||
ROOT = None
|
||||
N = 0
|
||||
|
||||
@staticmethod
|
||||
def mkdtemp(prefix=""):
|
||||
FrozenTempfile.N += 1
|
||||
p = os.path.join(FrozenTempfile.ROOT, f"{prefix}{FrozenTempfile.N:02d}")
|
||||
os.makedirs(p)
|
||||
return p
|
||||
|
||||
|
||||
oracle_mod.tempfile = FrozenTempfile
|
||||
|
||||
|
||||
# ── deterministic fixture + setup interpreter (shared recipe) ───────────────
|
||||
|
||||
def _git(root, *argv):
|
||||
env = dict(os.environ, **GIT_ENV_FIXED)
|
||||
return subprocess.run(["git", "-C", root, *argv], capture_output=True,
|
||||
text=True, env=env)
|
||||
|
||||
|
||||
def make_fixture(case_root):
|
||||
"""The battery recipe — fixed identity, fixed dates, seed commit.
|
||||
The resulting master SHA is machine-independent (frozen literally)."""
|
||||
repo = os.path.join(case_root, "repo")
|
||||
os.makedirs(repo)
|
||||
_git(repo, "init", "-q", "-b", "master")
|
||||
_git(repo, "config", "user.email", "t@t")
|
||||
_git(repo, "config", "user.name", "t")
|
||||
_git(repo, "config", "commit.gpgsign", "false")
|
||||
with open(os.path.join(repo, "f.txt"), "w") as f:
|
||||
f.write("v1\n")
|
||||
_git(repo, "add", ".")
|
||||
_git(repo, "commit", "-qm", "seed")
|
||||
return repo
|
||||
|
||||
|
||||
def base_sha(repo):
|
||||
return _git(repo, "rev-parse", "master").stdout.strip()
|
||||
|
||||
|
||||
def run_setup(repo, steps):
|
||||
"""Declarative setup ops, executed identically by freeze, replay and
|
||||
the Rust battery. Returns the worktree path (or None)."""
|
||||
wt = None
|
||||
for step in steps:
|
||||
op, _, arg = step.partition(":")
|
||||
if op == "worktree":
|
||||
wt = os.path.join(os.path.dirname(repo), f"wt-{arg}")
|
||||
r = _git(repo, "worktree", "add", wt, "-b", arg, "master")
|
||||
assert r.returncode == 0, r.stderr
|
||||
elif op == "commit_in_worktree":
|
||||
with open(os.path.join(wt, "f.txt"), "w") as f:
|
||||
f.write("v2\n")
|
||||
r = _git(wt, "commit", "-qam", "c")
|
||||
assert r.returncode == 0, r.stderr
|
||||
elif op == "dirty_repo":
|
||||
with open(os.path.join(repo, "f.txt"), "w") as f:
|
||||
f.write("v9\n")
|
||||
else:
|
||||
raise ValueError(f"unknown setup op: {step}")
|
||||
return wt
|
||||
|
||||
|
||||
def make_stub_dir(case_root):
|
||||
"""PATH stub: a python3 that deterministically 'runs the gate suite'
|
||||
(the oracle's default gate command resolves through PATH)."""
|
||||
stub = os.path.join(case_root, "stub")
|
||||
os.makedirs(stub)
|
||||
path = os.path.join(stub, "python3")
|
||||
with open(path, "w") as f:
|
||||
f.write("#!/bin/sh\necho stub-gates-green\nexit 0\n")
|
||||
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
|
||||
return stub
|
||||
|
||||
|
||||
# ── scenario cases ───────────────────────────────────────────────────────────
|
||||
|
||||
class Case:
|
||||
def __init__(self, name, group, steps, inp, stub=False):
|
||||
self.name = name
|
||||
self.group = group
|
||||
self.steps = steps
|
||||
self.inp = inp # (repo, wt, stub) → kernel input dict
|
||||
self.stub = stub
|
||||
|
||||
|
||||
def propose_cases():
|
||||
return [
|
||||
Case("propose-ok", "propose_change", [],
|
||||
lambda r, w, s: {"repo": "@REPO@", "branch": "cand-ok",
|
||||
"base": "master", "now": FROZEN_AT}),
|
||||
Case("propose-base-head", "propose_change", [],
|
||||
lambda r, w, s: {"repo": "@REPO@", "branch": "cand-head",
|
||||
"base": "HEAD", "now": FROZEN_AT}),
|
||||
Case("propose-unknown-base", "propose_change", [],
|
||||
lambda r, w, s: {"repo": "@REPO@", "branch": "cand-bad",
|
||||
"base": "no-such-ref", "now": FROZEN_AT}),
|
||||
Case("propose-dup-branch", "propose_change", ["worktree:dup"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "branch": "dup",
|
||||
"base": "master", "now": FROZEN_AT}),
|
||||
Case("propose-missing-repo", "propose_change", [],
|
||||
lambda r, w, s: {"repo": MISSING_REPO, "branch": "cand-gone",
|
||||
"base": "master", "now": FROZEN_AT}),
|
||||
]
|
||||
|
||||
|
||||
def gates_cases():
|
||||
return [
|
||||
Case("gates-pass", "run_gates", [],
|
||||
lambda r, w, s: {"worktree": "@REPO@",
|
||||
"test_cmd": ["echo", "gates-green"]}),
|
||||
Case("gates-fail-stderr", "run_gates", [],
|
||||
lambda r, w, s: {"worktree": "@REPO@",
|
||||
"test_cmd": ["sh", "-c", "echo boom 1>&2; exit 1"]}),
|
||||
Case("gates-tail-2000", "run_gates", [],
|
||||
lambda r, w, s: {"worktree": "@REPO@",
|
||||
"test_cmd": ["sh", "-c", "seq 1 3000"]}),
|
||||
Case("gates-out-then-err", "run_gates", [],
|
||||
lambda r, w, s: {"worktree": "@REPO@",
|
||||
"test_cmd": ["sh", "-c",
|
||||
"echo o1; echo e1 1>&2; echo o2; exit 7"]}),
|
||||
Case("gates-default-cmd", "run_gates", [],
|
||||
lambda r, w, s: {"worktree": "@REPO@", "test_cmd": None},
|
||||
stub=True),
|
||||
]
|
||||
|
||||
|
||||
def compare_cases():
|
||||
def row(name, cand, base):
|
||||
return Case("cb-" + name, "compare_baseline", [],
|
||||
lambda r, w, s, c=cand, b=base:
|
||||
{"candidate_result": c, "baseline_result": b})
|
||||
|
||||
return [
|
||||
row("candidate-red", {"passed": False}, {"passed": True}),
|
||||
row("candidate-red-baseline-red", {"passed": False}, {"passed": False}),
|
||||
row("baseline-red", {"passed": True}, {"passed": False}),
|
||||
row("both-green", {"passed": True}, {"passed": True}),
|
||||
row("missing-passed-key", {}, {"passed": True}),
|
||||
row("truthy-coercion", {"passed": 1}, {"passed": 0}),
|
||||
row("explicit-null-passed", {"passed": None}, {"passed": True}),
|
||||
]
|
||||
|
||||
|
||||
def _proposal(branch, repo):
|
||||
return {"proposed": True, "branch": branch, "worktree": "@WORKTREE@",
|
||||
"base": "master", "rollback_ref": base_sha(repo), "at": FROZEN_AT}
|
||||
|
||||
|
||||
def pod_cases():
|
||||
return [
|
||||
Case("pod-discard", "promote_or_discard", ["worktree:discard-1"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("discard-1", r),
|
||||
"verdict": {"promote": False, "reason": "test"}}),
|
||||
Case("pod-promote-up-to-date", "promote_or_discard", ["worktree:podd-2"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("podd-2", r),
|
||||
"verdict": {"promote": True, "reason": "test"}}),
|
||||
Case("pod-promote-real-merge", "promote_or_discard",
|
||||
["worktree:podd-3", "commit_in_worktree"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("podd-3", r),
|
||||
"verdict": {"promote": True, "reason": "test"}}),
|
||||
Case("pod-merge-fail", "promote_or_discard",
|
||||
["worktree:podd-4", "commit_in_worktree", "dirty_repo"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("podd-4", r),
|
||||
"verdict": {"promote": True, "reason": "test"}}),
|
||||
Case("pod-discard-ghost", "promote_or_discard", [],
|
||||
lambda r, w, s: {"repo": "@REPO@", "proposal": _proposal("ghost-branch", r),
|
||||
"verdict": {"promote": False, "reason": "test"}}),
|
||||
Case("pod-missing-branch-key", "promote_or_discard", [],
|
||||
lambda r, w, s: {"repo": "@REPO@",
|
||||
"proposal": {k: v for k, v in _proposal("keyless", r).items()
|
||||
if k != "branch"},
|
||||
"verdict": {"promote": True}}),
|
||||
]
|
||||
|
||||
|
||||
def rollback_cases():
|
||||
return [
|
||||
Case("rb-ok-after-dirty", "rollback", ["dirty_repo"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "rollback_ref": base_sha(r)}),
|
||||
Case("rb-ref-master", "rollback", ["dirty_repo"],
|
||||
lambda r, w, s: {"repo": "@REPO@", "rollback_ref": "master"}),
|
||||
Case("rb-bad-ref", "rollback", [],
|
||||
lambda r, w, s: {"repo": "@REPO@", "rollback_ref": "no-such-ref"}),
|
||||
Case("rb-missing-repo", "rollback", [],
|
||||
lambda r, w, s: {"repo": MISSING_REPO, "rollback_ref": "master"}),
|
||||
]
|
||||
|
||||
|
||||
def monitor_cases():
|
||||
def row(name, proposal):
|
||||
return Case("mon-" + name, "monitor", [],
|
||||
lambda r, w, s, p=proposal: {"proposal": p})
|
||||
|
||||
return [
|
||||
row("full", {"branch": "b", "rollback_ref": "abc123"}),
|
||||
row("empty", {}),
|
||||
row("partial", {"branch": "only-b"}),
|
||||
row("extra-keys", {"branch": "b", "rollback_ref": "r", "extra": 1}),
|
||||
row("nonstring-branch", {"branch": 7, "rollback_ref": None}),
|
||||
]
|
||||
|
||||
|
||||
GROUPS = [
|
||||
("propose_change", propose_cases()),
|
||||
("run_gates", gates_cases()),
|
||||
("compare_baseline", compare_cases()),
|
||||
("promote_or_discard", pod_cases()),
|
||||
("rollback", rollback_cases()),
|
||||
("monitor", monitor_cases()),
|
||||
]
|
||||
|
||||
FACADE_CALL = {
|
||||
"propose_change": lambda i: oracle_mod.propose_change(
|
||||
i["repo"], i["branch"], i["base"]),
|
||||
"run_gates": lambda i: oracle_mod.run_gates(i["worktree"], i["test_cmd"]),
|
||||
"compare_baseline": lambda i: oracle_mod.compare_baseline(
|
||||
i["candidate_result"], i["baseline_result"]),
|
||||
"promote_or_discard": lambda i: oracle_mod.promote_or_discard(
|
||||
i["repo"], i["proposal"], i["verdict"]),
|
||||
"rollback": lambda i: oracle_mod.rollback(i["repo"], i["rollback_ref"]),
|
||||
"monitor": lambda i: oracle_mod.monitor(i["proposal"]),
|
||||
}
|
||||
|
||||
KERNEL_CALL = {
|
||||
"propose_change": lambda i: smod_loader.propose_change(
|
||||
i["repo"], i["branch"], i["base"], i["now"]),
|
||||
"run_gates": lambda i: smod_loader.run_gates(i["worktree"], i["test_cmd"]),
|
||||
"compare_baseline": lambda i: smod_loader.compare_baseline(
|
||||
i["candidate_result"], i["baseline_result"]),
|
||||
"promote_or_discard": lambda i: smod_loader.promote_or_discard(
|
||||
i["repo"], i["proposal"], i["verdict"]),
|
||||
"rollback": lambda i: smod_loader.rollback(i["repo"], i["rollback_ref"]),
|
||||
"monitor": lambda i: smod_loader.monitor(i["proposal"]),
|
||||
}
|
||||
|
||||
|
||||
# ── placeholder plumbing ─────────────────────────────────────────────────────
|
||||
|
||||
def substitute(value, repo, wt, stub):
|
||||
"""Machine paths → frozen placeholders (kernel input side)."""
|
||||
s = json.dumps(value)
|
||||
for actual, ph in ((repo, "@REPO@"), (wt, "@WORKTREE@"), (stub, "@STUBDIR@")):
|
||||
if actual:
|
||||
s = s.replace(actual, ph)
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def de_substitute(value, repo, wt, stub):
|
||||
"""Frozen placeholders → live paths (call side)."""
|
||||
s = json.dumps(value)
|
||||
for actual, ph in ((repo, "@REPO@"), (wt, "@WORKTREE@"), (stub, "@STUBDIR@")):
|
||||
if actual:
|
||||
s = s.replace(ph, actual)
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def normalize(out, repo, wt, stub):
|
||||
"""Actual output paths → placeholders (kernel output side). The
|
||||
worktree a propose call created internally is captured from the
|
||||
output itself — neither side can know it in advance."""
|
||||
reps = [t for t in ((wt, "@WORKTREE@") if wt else None,
|
||||
(repo, "@REPO@") if repo else None,
|
||||
(stub, "@STUBDIR@") if stub else None) if t]
|
||||
if isinstance(out, dict) and isinstance(out.get("worktree"), str):
|
||||
reps.append((out["worktree"], "@WORKTREE@"))
|
||||
s = json.dumps(out, sort_keys=True)
|
||||
for actual, ph in reps:
|
||||
if actual and actual != "/":
|
||||
s = s.replace(actual, ph)
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def _catch(fn, *a):
|
||||
try:
|
||||
return fn(*a)
|
||||
except FileNotFoundError:
|
||||
return {"__raised__": "FileNotFoundError"}
|
||||
except KeyError as e:
|
||||
return {"__raised__": f"KeyError: {e}"}
|
||||
except RuntimeError:
|
||||
return {"__raised__": "RuntimeError"}
|
||||
|
||||
|
||||
class _PathEnv:
|
||||
"""Set PATH to the stub dir for the duration of one call."""
|
||||
|
||||
def __init__(self, stub):
|
||||
self.stub = stub
|
||||
|
||||
def __enter__(self):
|
||||
self.old = os.environ.get("PATH")
|
||||
os.environ["PATH"] = self.stub + os.pathsep + (self.old or "")
|
||||
|
||||
def __exit__(self, *exc):
|
||||
if self.old is not None:
|
||||
os.environ["PATH"] = self.old
|
||||
return False
|
||||
|
||||
|
||||
def run_scenario(case, tmp_root, call):
|
||||
"""One scenario, identical for freeze and both replay tiers:
|
||||
fresh fixture → setup → substituted input → call → normalized output."""
|
||||
case_root = tempfile.mkdtemp(prefix=f"smod_{case.name}_", dir=tmp_root)
|
||||
repo = make_fixture(case_root)
|
||||
FrozenTempfile.ROOT = case_root
|
||||
FrozenTempfile.N = 0
|
||||
wt = run_setup(repo, case.steps)
|
||||
stub = make_stub_dir(case_root) if case.stub else None
|
||||
live_wt = wt or os.path.join(case_root, "wt-input")
|
||||
live_stub = stub or os.path.join(case_root, "stub-unused")
|
||||
inp = substitute(case.inp(repo, wt, stub), repo, wt, stub)
|
||||
inp_copy = copy.deepcopy(inp) # deep-copy law: inputs frozen before calls
|
||||
live = de_substitute(inp_copy, repo, live_wt, live_stub)
|
||||
if case.stub:
|
||||
with _PathEnv(live_stub):
|
||||
out = _catch(call, live)
|
||||
else:
|
||||
out = _catch(call, live)
|
||||
if isinstance(out, dict) and out.get("__raised__"):
|
||||
return inp, {"raises": out["__raised__"]}
|
||||
return inp, normalize(out, repo, live_wt, stub)
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
tmp_root = tempfile.mkdtemp(prefix="smod_truth_")
|
||||
groups = {}
|
||||
failures = 0
|
||||
total = 0
|
||||
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, expected, case=None, _r=recs, _g=group):
|
||||
entry = {"name": name, "input": inp, "expected": expected}
|
||||
if case is not None:
|
||||
entry["setup"] = case.steps
|
||||
entry["stub"] = case.stub
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[_g] if c["name"] == name)
|
||||
for key in ("input", "expected"):
|
||||
if json.dumps(entry[key], sort_keys=True) != json.dumps(
|
||||
old[key], sort_keys=True):
|
||||
print(f"FACADE DIVERGE [{_g}] {name} {key}")
|
||||
print(" got: ", json.dumps(entry[key], sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(old[key], sort_keys=True)[:600])
|
||||
_r.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for case in cases:
|
||||
total += 1
|
||||
inp, want = run_scenario(case, tmp_root,
|
||||
lambda i, c=case: _catch(
|
||||
FACADE_CALL[c.group], i))
|
||||
rec(case.name, inp, want, case)
|
||||
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[case.group] if c["name"] == case.name)
|
||||
repo, wt, stub = kernel_paths(case, tmp_root, old)
|
||||
call = lambda: KERNEL_CALL[case.group](
|
||||
de_substitute(copy.deepcopy(old["input"]), repo, wt, stub))
|
||||
if old.get("stub"):
|
||||
with _PathEnv(stub):
|
||||
kout = _catch(lambda: call())
|
||||
else:
|
||||
kout = _catch(call)
|
||||
kwant = old["expected"]
|
||||
if not (isinstance(kout, dict) and kout.get("__raised__")):
|
||||
kout = normalize(kout, repo, wt, stub)
|
||||
if isinstance(kwant, dict) and kwant.get("raises"):
|
||||
ok = kout == {"__raised__": kwant["raises"]} or (
|
||||
kout == {"__raised__": "RuntimeError"})
|
||||
else:
|
||||
ok = json.dumps(kout, sort_keys=True) == json.dumps(
|
||||
kwant, sort_keys=True)
|
||||
if not ok:
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{case.group}] {case.name}")
|
||||
print(" got: ", json.dumps(kout, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(kwant, sort_keys=True)[:600])
|
||||
|
||||
groups[group] = recs
|
||||
|
||||
if REPLAY:
|
||||
facade_div = sum(1 for v in groups.values() for c in v if "diverged" in c)
|
||||
print(f"replay: facade {total - facade_div}/{total}, "
|
||||
f"kernel {total - failures}/{total}")
|
||||
return 1 if (failures or facade_div) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/self_modify.py",
|
||||
"pin": "RUNE_SMOD_CORE=python",
|
||||
"note": "Frozen via scripts/gen_self_modify_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_AT + "; git "
|
||||
"fixtures use fixed identity+dates so the seed SHA is "
|
||||
"literal. @REPO@/@WORKTREE@/@STUBDIR@ are placeholders "
|
||||
"substituted at replay. Battery embeds this VERBATIM.",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1, sort_keys=True)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
def kernel_paths(case, tmp_root, old_entry):
|
||||
"""(repo, wt, stub) for kernel-level replay of one frozen entry."""
|
||||
case_root = tempfile.mkdtemp(prefix=f"smod_kernel_{case.name}_", dir=tmp_root)
|
||||
repo = make_fixture(case_root)
|
||||
wt = run_setup(repo, old_entry.get("setup", []))
|
||||
stub = None
|
||||
if old_entry.get("stub"):
|
||||
stub = make_stub_dir(case_root)
|
||||
live_wt = wt or os.path.join(case_root, "wt-input")
|
||||
live_stub = stub or os.path.join(case_root, "stub-unused")
|
||||
return repo, live_wt, live_stub
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,589 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_task_gate_truth.py — freeze/replay parity truth for task_gate
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/task_gate.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output, oracle_return) triples into
|
||||
src/rust/task_gate/src/frozen_tg_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle functions delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_TG_CORE=python BEFORE any import.
|
||||
No-clock law: the wall clock is frozen by patching the datetime symbol in
|
||||
task_gate, so every kernel payload carries its own `now` and replay is
|
||||
deterministic regardless of run date.
|
||||
|
||||
Boundary: the sqlite IO (connect, the row reads, UPDATE/INSERT) and the
|
||||
CLI main() stay in the Python facade — the kernel owns the verdict logic
|
||||
(pattern extraction, regex gate matching, count/threshold/status
|
||||
transitions, promotion decision, listing projection, hook echo).
|
||||
list_gates' ORDER BY is decided by sqlite in the facade; the kernel
|
||||
receives already-ordered rows.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(
|
||||
REPO, "src", "rust", "task_gate", "src", "frozen_tg_truth.json"
|
||||
)
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
if not REPLAY:
|
||||
os.environ["RUNE_TG_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import task_gate as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
import tg_loader # noqa: E402 (kernel-level replay side)
|
||||
|
||||
FROZEN_NOW = "2026-10-11T23:00:00.123456+00:00"
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
assert tz is timezone.utc or tz is None
|
||||
return datetime(2026, 10, 11, 23, 0, 0, 123456, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
oracle_mod.datetime = FrozenDatetime
|
||||
|
||||
_COLS = ("id", "instruction", "pattern", "count", "threshold", "status",
|
||||
"promoted_at", "created_at", "last_seen")
|
||||
|
||||
|
||||
def fresh_db():
|
||||
tmp = tempfile.mkdtemp(prefix="tg_truth_")
|
||||
oracle_mod.DB_PATH = os.path.join(tmp, "task_gate.db")
|
||||
return oracle_mod.connect()
|
||||
|
||||
|
||||
def seed_rows(pre_rows):
|
||||
"""Seed explicit rows (dicts with any subset of the columns; count/
|
||||
threshold/status/pattern/created_at/last_seen default like the oracle
|
||||
would for a fresh row). Returns rows as full dicts in id order."""
|
||||
db = fresh_db()
|
||||
for i, r in enumerate(pre_rows, start=1):
|
||||
db.execute(
|
||||
"INSERT INTO instructions (id, instruction, pattern, count,"
|
||||
" threshold, status, promoted_at, created_at, last_seen)"
|
||||
" VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
r.get("id", i),
|
||||
r.get("instruction", "seed instruction %d" % i),
|
||||
r.get("pattern", r"\bseed\b"),
|
||||
r.get("count", 1),
|
||||
r.get("threshold", 3),
|
||||
r.get("status", "observing"),
|
||||
r.get("promoted_at"),
|
||||
r.get("created_at", FROZEN_NOW),
|
||||
r.get("last_seen", FROZEN_NOW),
|
||||
),
|
||||
)
|
||||
db.commit()
|
||||
rows = [dict(r) for r in db.execute(
|
||||
"SELECT * FROM instructions ORDER BY id").fetchall()]
|
||||
db.close()
|
||||
return rows
|
||||
|
||||
|
||||
def read_rows():
|
||||
db = sqlite3.connect(oracle_mod.DB_PATH)
|
||||
db.row_factory = sqlite3.Row
|
||||
rows = [dict(r) for r in db.execute(
|
||||
"SELECT * FROM instructions ORDER BY id").fetchall()]
|
||||
db.close()
|
||||
return rows
|
||||
|
||||
|
||||
def kernel_input(rows, text):
|
||||
return {"instructions": rows, "text": text, "now": FROZEN_NOW}
|
||||
|
||||
|
||||
def expected_record(rows_after, result):
|
||||
"""Derive the kernel's expected doc purely from oracle behavior."""
|
||||
if result["matched"]:
|
||||
row = next(r for r in rows_after if r["id"] == result["id"])
|
||||
return {"matched": True, "id": row["id"], "count": row["count"],
|
||||
"status": row["status"], "promoted": result["promoted"],
|
||||
"last_seen": row["last_seen"]}
|
||||
row = next(r for r in rows_after if r["id"] == result["id"])
|
||||
return {"matched": False,
|
||||
"new_row": {k: row[k] for k in
|
||||
("instruction", "pattern", "count", "threshold",
|
||||
"status", "created_at", "last_seen")}}
|
||||
|
||||
|
||||
# ── record_instruction ───────────────────────────────────────────────────────
|
||||
|
||||
def r_new_keywords(rec):
|
||||
rows = seed_rows([])
|
||||
text = "Always CHECK the benchmark before building"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-keywords-pattern",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_no_keywords(rec):
|
||||
rows = seed_rows([])
|
||||
text = "do it"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-no-keywords-fallback-escape",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_fallback_truncate50(rec):
|
||||
rows = seed_rows([])
|
||||
text = " AB CD EF GH IJ KL MN OP QR ST UV WX YZ 12 34 56 "
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-fallback-truncated-50-stripped",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_regex_special(rec):
|
||||
rows = seed_rows([])
|
||||
text = "fix the bug (now)!"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-regex-special-chars",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_unicode(rec):
|
||||
rows = seed_rows([])
|
||||
text = "café über always"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-unicode-keywords",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_underscore_digit(rec):
|
||||
rows = seed_rows([])
|
||||
text = "v1_2 test_it now"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-underscore-digit-keywords",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_instruction_verbatim(rec):
|
||||
# the INSERT stores the ORIGINAL text, not the lowered/stripped form
|
||||
rows = seed_rows([])
|
||||
text = " ALWAYS check "
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-instruction-verbatim-not-stripped",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_observing(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3,
|
||||
"status": "observing"},
|
||||
])
|
||||
text = "please CHECK everything"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-observing-increment",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_promote(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 2, "threshold": 3,
|
||||
"status": "observing"},
|
||||
])
|
||||
text = "check again"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-promotes-at-threshold",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_already_active(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 5, "threshold": 3,
|
||||
"status": "active", "promoted_at": FROZEN_NOW},
|
||||
])
|
||||
text = "check once more"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-already-active-no-promote",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_first_wins(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3},
|
||||
{"pattern": r"\bcheck|everything\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "check everything"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-first-row-wins",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_skips_retired(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3,
|
||||
"status": "retired"},
|
||||
{"pattern": r"\bbenchmark\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "check the benchmark"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-skips-retired-row",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_word_boundary(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "the checks all pass"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("no-match-word-boundary-plural",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_fallback_pattern(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"do\ it", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "Do IT now"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-fallback-escaped-space-pattern",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_match_digit_threshold(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bship\b", "count": 3, "threshold": 4},
|
||||
])
|
||||
text = "ship it"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("match-count-below-higher-threshold",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
def r_new_after_nonmatch(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bdeploy\b", "count": 1, "threshold": 3},
|
||||
])
|
||||
text = "release the kraken"
|
||||
result = oracle_mod.record_instruction(text)
|
||||
after = read_rows()
|
||||
rec("new-row-after-existing-nonmatch",
|
||||
kernel_input(rows, text), result, after,
|
||||
expected_record(after, result))
|
||||
|
||||
|
||||
# ── check_context ────────────────────────────────────────────────────────────
|
||||
|
||||
def c_no_active(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 1, "threshold": 3},
|
||||
{"pattern": r"\bship\b", "count": 3, "threshold": 3,
|
||||
"status": "retired"},
|
||||
])
|
||||
context = "check the ship"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-no-active-gates",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_one_match(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "promoted_at": FROZEN_NOW,
|
||||
"instruction": "always check"},
|
||||
])
|
||||
context = "about to check the benchmark"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-one-match",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_multi_match_order(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
{"pattern": r"\bship\b", "count": 7, "threshold": 3,
|
||||
"status": "active", "instruction": "ship weekly"},
|
||||
{"pattern": r"\bbenchmark\b", "count": 4, "threshold": 3,
|
||||
"status": "active", "instruction": "run the benchmark"},
|
||||
])
|
||||
context = "check, ship, benchmark"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-multi-match-rowid-order",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_case_insensitive(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
])
|
||||
context = "CHECK EVERYTHING NOW"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-case-insensitive-context",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_no_match(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
])
|
||||
context = "deploy to production"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-no-match",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
def c_word_boundary(rec):
|
||||
rows = seed_rows([
|
||||
{"pattern": r"\bcheck\b", "count": 3, "threshold": 3,
|
||||
"status": "active", "instruction": "always check"},
|
||||
])
|
||||
context = "the checks pass"
|
||||
matches = oracle_mod.check_context(context)
|
||||
rec("ctx-word-boundary-plural-no-match",
|
||||
{"instructions": rows, "context": context},
|
||||
matches, None, {"matches": matches})
|
||||
|
||||
|
||||
# ── list_gates ───────────────────────────────────────────────────────────────
|
||||
|
||||
def l_empty(rec):
|
||||
seed_rows([])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-empty", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
def l_order_desc(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 5, "instruction": "five"},
|
||||
{"pattern": r"\bb\b", "count": 1, "instruction": "one"},
|
||||
{"pattern": r"\bc\b", "count": 3, "instruction": "three"},
|
||||
])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-ordered-count-desc", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
def l_ties(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 2, "instruction": "first"},
|
||||
{"pattern": r"\bb\b", "count": 4, "instruction": "second"},
|
||||
{"pattern": r"\bc\b", "count": 2, "instruction": "third"},
|
||||
])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-tie-order", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
def l_null_promoted_at(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 2, "status": "active",
|
||||
"promoted_at": FROZEN_NOW, "instruction": "active one"},
|
||||
{"pattern": r"\bb\b", "count": 1, "instruction": "observing one"},
|
||||
])
|
||||
gates = oracle_mod.list_gates()
|
||||
rec("list-null-and-set-promoted-at", {}, gates, read_rows(), None)
|
||||
|
||||
|
||||
# ── promote_gate ─────────────────────────────────────────────────────────────
|
||||
|
||||
def p_first(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 1, "instruction": "observing"},
|
||||
])
|
||||
ret = oracle_mod.promote_gate(1)
|
||||
rec("promote-existing-id",
|
||||
{"id": 1, "now": FROZEN_NOW}, ret, read_rows(),
|
||||
{"status": "active", "promoted_at": FROZEN_NOW})
|
||||
|
||||
|
||||
def p_unknown_id(rec):
|
||||
seed_rows([])
|
||||
ret = oracle_mod.promote_gate(42)
|
||||
rec("promote-unknown-id",
|
||||
{"id": 42, "now": FROZEN_NOW}, ret, read_rows(),
|
||||
{"status": "active", "promoted_at": FROZEN_NOW})
|
||||
|
||||
|
||||
def p_overrides_retired(rec):
|
||||
seed_rows([
|
||||
{"pattern": r"\ba\b", "count": 1, "status": "retired",
|
||||
"instruction": "retired one"},
|
||||
])
|
||||
ret = oracle_mod.promote_gate(1)
|
||||
rec("promote-retired-row-becomes-active",
|
||||
{"id": 1, "now": FROZEN_NOW}, ret, read_rows(),
|
||||
{"status": "active", "promoted_at": FROZEN_NOW})
|
||||
|
||||
|
||||
# ── handle_hook ──────────────────────────────────────────────────────────────
|
||||
|
||||
def h_default(rec):
|
||||
ret = oracle_mod.handle_hook("post-build")
|
||||
rec("hook-plain-event", {"event": "post-build"}, ret, None,
|
||||
{"status": "ok", "event": "post-build", "handler": "task_gate"})
|
||||
|
||||
|
||||
def h_with_data(rec):
|
||||
ret = oracle_mod.handle_hook("pre-commit", {"sha": "abc"})
|
||||
rec("hook-with-data", {"event": "pre-commit"}, ret, None,
|
||||
{"status": "ok", "event": "pre-commit", "handler": "task_gate"})
|
||||
|
||||
|
||||
def h_empty(rec):
|
||||
ret = oracle_mod.handle_hook("")
|
||||
rec("hook-empty-event", {"event": ""}, ret, None,
|
||||
{"status": "ok", "event": "", "handler": "task_gate"})
|
||||
|
||||
|
||||
GROUPS = [
|
||||
("record_instruction", [r_new_keywords, r_new_no_keywords,
|
||||
r_new_fallback_truncate50, r_new_regex_special,
|
||||
r_new_unicode, r_new_underscore_digit,
|
||||
r_new_instruction_verbatim, r_match_observing,
|
||||
r_match_promote, r_match_already_active,
|
||||
r_match_first_wins, r_match_skips_retired,
|
||||
r_match_word_boundary, r_match_fallback_pattern,
|
||||
r_match_digit_threshold, r_new_after_nonmatch]),
|
||||
("check_context", [c_no_active, c_one_match, c_multi_match_order,
|
||||
c_case_insensitive, c_no_match, c_word_boundary]),
|
||||
("list_gates", [l_empty, l_order_desc, l_ties, l_null_promoted_at]),
|
||||
("promote_gate", [p_first, p_unknown_id, p_overrides_retired]),
|
||||
("handle_hook", [h_default, h_with_data, h_empty]),
|
||||
]
|
||||
|
||||
# groups where the kernel is involved (list_gates' ordering is sqlite's)
|
||||
KERNEL_GROUPS = {"record_instruction", "check_context", "promote_gate",
|
||||
"handle_hook"}
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, kinput, oracle_ret, rows_after, kexpected, _r=recs):
|
||||
entry = {"name": name, "input": kinput, "return": oracle_ret,
|
||||
"expected": kexpected}
|
||||
if rows_after is not None:
|
||||
entry["rows_after"] = rows_after
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected", "rows_after"):
|
||||
if key not in entry and key not in old:
|
||||
continue
|
||||
got, want = entry.get(key), old.get(key)
|
||||
if json.dumps(got, sort_keys=True) != json.dumps(
|
||||
want, sort_keys=True):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_failures = sum(
|
||||
1 for v in groups.values() for c in v if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
for group, cases in frozen.items():
|
||||
if group not in KERNEL_GROUPS:
|
||||
continue
|
||||
for case in cases:
|
||||
inp = case["input"]
|
||||
if group == "record_instruction":
|
||||
got = tg_loader.record_instruction(
|
||||
inp["instructions"], inp["text"], inp["now"])
|
||||
elif group == "check_context":
|
||||
got = tg_loader.check_context(
|
||||
inp["instructions"], inp["context"])
|
||||
elif group == "promote_gate":
|
||||
got = tg_loader.promote_gate(inp["id"], inp["now"])
|
||||
else:
|
||||
got = tg_loader.handle_hook(inp["event"])
|
||||
if json.dumps(got, sort_keys=True) != json.dumps(
|
||||
case["expected"], sort_keys=True):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(
|
||||
case["expected"], sort_keys=True)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {total - failures}/{total}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": datetime.now(timezone.utc).isoformat(),
|
||||
"oracle": "src/rune/task_gate.py",
|
||||
"pin": "RUNE_TG_CORE=python",
|
||||
"note": "Frozen via scripts/gen_task_gate_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_NOW + ". "
|
||||
"Battery embeds this VERBATIM.",
|
||||
"cases": groups,
|
||||
}
|
||||
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
|
||||
with open(TRUTH_PATH, "w") as f:
|
||||
json.dump(doc, f, indent=1, sort_keys=True)
|
||||
f.write("\n")
|
||||
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,727 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_testing_mode_truth.py — freeze/replay parity truth for testing_mode
|
||||
(dichotomy wave, #2479 tranche).
|
||||
|
||||
Freeze mode (default): exercises EVERY public function of the LIVE Python
|
||||
oracle (src/rune/testing_mode.py) across every branch and records
|
||||
(kernel_input, expected_kernel_output) pairs into
|
||||
src/rust/testing_mode/src/frozen_tm_truth.json.
|
||||
|
||||
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
|
||||
delegation active and asserts both kernel-level (loader direct) and
|
||||
facade-level (oracle functions delegate) parity against the frozen file.
|
||||
|
||||
Poisoned-truth law: freeze pins RUNE_TM_CORE=python BEFORE any import (a
|
||||
no-op pre-port, load-bearing post-port — the facade must never delegate
|
||||
into the kernel while truth is being frozen).
|
||||
|
||||
No-clock law: the wall clock is frozen by patching the `time` symbol in
|
||||
testing_mode, so every kernel payload carries its own `now`/`now_ts` and
|
||||
replay is deterministic regardless of run date.
|
||||
|
||||
Boundary law: the session FILE IO (_load hit branch, save), the
|
||||
observations.jsonl append/rewrite, the forge adapter call, the ledger
|
||||
read (outstanding/record_real_world) and the registry IO all stay in the
|
||||
Python facade — the kernel is a pure function of their results. The
|
||||
`load`, `save` and `forge_dry_run` truth groups are facade-only (no
|
||||
kernel symbol) and are skipped by the kernel-level replay pass.
|
||||
|
||||
Ledger/registry law: behaviours and registry are stubbed with
|
||||
deterministic fakes (the oracle treats them as duck-typed boundaries);
|
||||
their canned responses are deep-copied into the kernel inputs BEFORE the
|
||||
oracle runs (the oracle mutates the session state in place).
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRUTH_PATH = os.path.join(REPO, "src", "rust", "testing_mode", "src", "frozen_tm_truth.json")
|
||||
REPLAY = "--replay" in sys.argv
|
||||
|
||||
os.environ["RUNE_TM_CORE"] = "python" # poisoned-truth law
|
||||
|
||||
sys.path.insert(0, os.path.join(REPO, "src"))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import testing_mode as oracle_mod # noqa: E402
|
||||
|
||||
if REPLAY:
|
||||
from rune import tm_loader # noqa: E402 (kernel-level replay side)
|
||||
# replay law: the facade pass must exercise the LIVE Rust delegation
|
||||
# path, so the pin is lifted AFTER import (the facade reads the env
|
||||
# at call time). The kernel pass calls the loader directly.
|
||||
del os.environ["RUNE_TM_CORE"]
|
||||
|
||||
FROZEN_TS = 1791806400 # 2026-10-12T12:00:00Z
|
||||
FROZEN_NOW = "2026-10-12T12:00:00Z"
|
||||
|
||||
|
||||
class FakeTime:
|
||||
@staticmethod
|
||||
def strftime(fmt, t):
|
||||
import time as _t
|
||||
return _t.strftime(fmt, t)
|
||||
|
||||
@staticmethod
|
||||
def gmtime():
|
||||
import time as _t
|
||||
return _t.gmtime(FROZEN_TS)
|
||||
|
||||
@staticmethod
|
||||
def time():
|
||||
return FROZEN_TS
|
||||
|
||||
|
||||
oracle_mod.time = FakeTime
|
||||
|
||||
|
||||
# ── deterministic boundary stubs ─────────────────────────────────────────────
|
||||
|
||||
class FakeBehaviours:
|
||||
"""Duck-typed BehaviourLedger stub — canned outstanding/records."""
|
||||
|
||||
def __init__(self, outstanding=None, rec=None):
|
||||
self.outstanding_value = outstanding or []
|
||||
self.rec = rec or {"behaviour_id": "behaviour:T-1", "state": "USER_CONFIRMED"}
|
||||
self.outstanding_kwargs = None
|
||||
|
||||
def outstanding(self, *, component=None, project=None):
|
||||
self.outstanding_kwargs = {"component": component, "project": project}
|
||||
return copy.deepcopy(self.outstanding_value)
|
||||
|
||||
def record_real_world(self, behaviour_id, *, passed, detail="",
|
||||
confirmed_by="user", generation=""):
|
||||
rec = dict(self.rec)
|
||||
rec["behaviour_id"] = rec.get("behaviour_id", f"behaviour:{behaviour_id}")
|
||||
return rec
|
||||
|
||||
|
||||
class FakeRegistry:
|
||||
def __init__(self, entries):
|
||||
self._entries = entries
|
||||
self.set_calls = []
|
||||
self.saved = False
|
||||
|
||||
def all_projects(self):
|
||||
return copy.deepcopy(self._entries)
|
||||
|
||||
def set_outstanding_validations(self, project_id, count):
|
||||
self.set_calls.append((project_id, count))
|
||||
|
||||
def save(self):
|
||||
self.saved = True
|
||||
|
||||
|
||||
class FakeLedger:
|
||||
"""Patched-in BehaviourLedger — summary canned per location."""
|
||||
|
||||
summaries = {}
|
||||
|
||||
def __init__(self, loc):
|
||||
self.loc = loc
|
||||
|
||||
def summary(self):
|
||||
return copy.deepcopy(FakeLedger.summaries.get(str(self.loc),
|
||||
{"outstanding": 0, "states": {}}))
|
||||
|
||||
|
||||
def fresh_session_dir():
|
||||
return tempfile.mkdtemp(prefix="tm_truth_")
|
||||
|
||||
|
||||
def fresh_state():
|
||||
s = oracle_mod.TestingSession(fresh_session_dir())
|
||||
return copy.deepcopy(s.state)
|
||||
|
||||
|
||||
# ── case helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def rec_kernel(rec, name, inp, expected, oracle_ret=None):
|
||||
"""Record a kernel case; inputs deep-copied BEFORE oracle mutation."""
|
||||
rec(name, copy.deepcopy(inp), oracle_ret, expected)
|
||||
|
||||
|
||||
def rec_facade(rec, name, inp, expected, oracle_ret=None):
|
||||
rec(name, inp, oracle_ret, expected)
|
||||
|
||||
|
||||
# ── default_state (kernel) ───────────────────────────────────────────────────
|
||||
|
||||
def _ds_case(rec, name, project, scope_component):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project=project, scope_component=scope_component)
|
||||
rec_kernel(rec, name,
|
||||
{"project": project, "scope_component": scope_component, "now": FROZEN_NOW},
|
||||
{"state": s.state})
|
||||
|
||||
|
||||
def c_ds_default(rec):
|
||||
_ds_case(rec, "default-no-scope", "p", None)
|
||||
|
||||
|
||||
def c_ds_scope(rec):
|
||||
_ds_case(rec, "default-with-scope", "p", "component:shared")
|
||||
|
||||
|
||||
def c_ds_empty_project(rec):
|
||||
_ds_case(rec, "default-empty-project", "", "component:x")
|
||||
|
||||
|
||||
# ── next_item (kernel) ───────────────────────────────────────────────────────
|
||||
|
||||
def _next_case(rec, name, state_over, outstanding, expect_item):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p")
|
||||
s.state.update(copy.deepcopy(state_over))
|
||||
state_before = copy.deepcopy(s.state)
|
||||
beh = FakeBehaviours(outstanding=outstanding)
|
||||
item = s.next_item(beh)
|
||||
assert beh.outstanding_kwargs == {"component": state_before.get("scope_component"),
|
||||
"project": state_before.get("project") or None}
|
||||
rec_kernel(rec, name,
|
||||
{"state": state_before, "outstanding": outstanding},
|
||||
{"state": s.state, "item": item},
|
||||
oracle_ret=item)
|
||||
|
||||
|
||||
def c_next_paused(rec):
|
||||
_next_case(rec, "next-paused-null", {"paused": True},
|
||||
[{"behaviour_id": "behaviour:T-1"}], None)
|
||||
|
||||
|
||||
def c_next_empty(rec):
|
||||
_next_case(rec, "next-empty-queue", {}, [], None)
|
||||
|
||||
|
||||
def c_next_first(rec):
|
||||
_next_case(rec, "next-first-of-two", {},
|
||||
[{"behaviour_id": "behaviour:T-1", "state": "UNVALIDATED"},
|
||||
{"behaviour_id": "behaviour:T-2", "state": "RETEST_REQUIRED"}],
|
||||
{"behaviour_id": "behaviour:T-1", "state": "UNVALIDATED"})
|
||||
|
||||
|
||||
def c_next_overwrites_current(rec):
|
||||
_next_case(rec, "next-overwrites-current-item", {"current_item": "behaviour:OLD"},
|
||||
[{"behaviour_id": "behaviour:T-2"}], {"behaviour_id": "behaviour:T-2"})
|
||||
|
||||
|
||||
def c_next_empty_clears_current(rec):
|
||||
_next_case(rec, "next-empty-clears-current", {"current_item": "behaviour:T-1"},
|
||||
[], None)
|
||||
|
||||
|
||||
# ── record (kernel) ──────────────────────────────────────────────────────────
|
||||
|
||||
def _record_case(rec, name, completed_pre, behaviour_id, passed, rec_state):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p")
|
||||
s.state["completed_this_session"] = copy.deepcopy(completed_pre)
|
||||
state_before = copy.deepcopy(s.state)
|
||||
beh = FakeBehaviours(rec={"behaviour_id": f"behaviour:{behaviour_id}",
|
||||
"state": rec_state})
|
||||
ret = s.record(beh, behaviour_id, passed=passed, detail="checked")
|
||||
rec_kernel(rec, name,
|
||||
{"state": state_before,
|
||||
"rec": ret, "passed": passed, "now": FROZEN_NOW},
|
||||
{"state": s.state},
|
||||
oracle_ret={"behaviour_id": ret["behaviour_id"], "state": ret["state"]})
|
||||
|
||||
|
||||
def c_record_pass(rec):
|
||||
_record_case(rec, "record-pass-appends", [], "T-1", True, "USER_CONFIRMED")
|
||||
|
||||
|
||||
def c_record_fail(rec):
|
||||
_record_case(rec, "record-fail-appends", [], "T-2", False, "TEST_FAILED")
|
||||
|
||||
|
||||
def c_record_appends_to_existing(rec):
|
||||
pre = [{"behaviour_id": "behaviour:T-0", "passed": True, "at": "2026-10-12T11:00:00Z"}]
|
||||
_record_case(rec, "record-appends-preserves-order", pre, "T-1", True, "USER_CONFIRMED")
|
||||
|
||||
|
||||
# ── pause / resume (kernel) ──────────────────────────────────────────────────
|
||||
|
||||
def _pause_case(rec, name, paused_pre):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p")
|
||||
s.state["paused"] = paused_pre
|
||||
state_before = copy.deepcopy(s.state)
|
||||
s.pause()
|
||||
rec_kernel(rec, name,
|
||||
{"state": state_before},
|
||||
{"state": s.state})
|
||||
|
||||
|
||||
def _resume_case(rec, name, paused_pre):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p")
|
||||
s.state["paused"] = paused_pre
|
||||
state_before = copy.deepcopy(s.state)
|
||||
s.resume()
|
||||
rec_kernel(rec, name,
|
||||
{"state": state_before},
|
||||
{"state": s.state})
|
||||
|
||||
|
||||
def c_pause_from_active(rec):
|
||||
_pause_case(rec, "pause-from-active", False)
|
||||
|
||||
|
||||
def c_pause_already_paused(rec):
|
||||
_pause_case(rec, "pause-idempotent", True)
|
||||
|
||||
|
||||
def c_resume_from_paused(rec):
|
||||
_resume_case(rec, "resume-from-paused", True)
|
||||
|
||||
|
||||
def c_resume_already_active(rec):
|
||||
_resume_case(rec, "resume-idempotent", False)
|
||||
|
||||
|
||||
# ── note_blocker (kernel) ────────────────────────────────────────────────────
|
||||
|
||||
def _blocker_case(rec, name, blockers_pre, note):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p")
|
||||
s.state["blockers"] = copy.deepcopy(blockers_pre)
|
||||
state_before = copy.deepcopy(s.state)
|
||||
s.note_blocker("behaviour:T-9", note)
|
||||
rec_kernel(rec, name,
|
||||
{"state": state_before,
|
||||
"behaviour_id": "behaviour:T-9", "note": note, "now": FROZEN_NOW},
|
||||
{"state": s.state})
|
||||
|
||||
|
||||
def c_blocker_normal(rec):
|
||||
_blocker_case(rec, "blocker-normal", [], "blocked on upstream")
|
||||
|
||||
|
||||
def c_blocker_truncates(rec):
|
||||
_blocker_case(rec, "blocker-note-truncated-200", [], "x" * 260)
|
||||
|
||||
|
||||
def c_blocker_appends(rec):
|
||||
pre = [{"behaviour_id": "behaviour:T-1", "note": "old", "at": "2026-10-12T11:00:00Z"}]
|
||||
_blocker_case(rec, "blocker-appends-preserves-order", pre, "new")
|
||||
|
||||
|
||||
# ── record_observation (kernel) ──────────────────────────────────────────────
|
||||
|
||||
def _obs_case(rec, name, **kw):
|
||||
d = fresh_session_dir()
|
||||
obs = oracle_mod.record_observation(d, **kw)
|
||||
inp = {"now": FROZEN_NOW, "now_ts": FROZEN_TS,
|
||||
"observed": kw.get("observed", ""),
|
||||
"expected": kw.get("expected", ""),
|
||||
"behaviour_id": kw.get("behaviour_id", ""),
|
||||
"environment": kw.get("environment", ""),
|
||||
"reproduction": kw.get("reproduction", ""),
|
||||
"candidate_areas": list(kw.get("candidate_areas") or []),
|
||||
"confidence": kw.get("confidence", "low")}
|
||||
rec_kernel(rec, name, inp, {"observation": obs}, oracle_ret=obs)
|
||||
|
||||
|
||||
def c_obs_defaults(rec):
|
||||
_obs_case(rec, "obs-defaults", observed="button dead", expected="button saves")
|
||||
|
||||
|
||||
def c_obs_full(rec):
|
||||
_obs_case(rec, "obs-full-fields", observed="500 on save", expected="201 created",
|
||||
behaviour_id="behaviour:T-1", environment="production",
|
||||
reproduction="POST /save with empty body",
|
||||
candidate_areas=["component:api"], confidence="high")
|
||||
|
||||
|
||||
def c_obs_candidate_areas_none(rec):
|
||||
_obs_case(rec, "obs-candidate-areas-none-coerced", observed="x", expected="y",
|
||||
candidate_areas=None)
|
||||
|
||||
|
||||
def c_obs_candidate_areas_copy(rec):
|
||||
src = ["component:a", "component:b"]
|
||||
_obs_case(rec, "obs-candidate-areas-hypotheses", observed="x", expected="y",
|
||||
candidate_areas=src)
|
||||
|
||||
|
||||
# ── close_observation (kernel) ───────────────────────────────────────────────
|
||||
|
||||
def _obs_lines(d, n=1):
|
||||
lines = []
|
||||
for i in range(n):
|
||||
obs = oracle_mod.record_observation(
|
||||
d, observed=f"obs-{i}", expected=f"exp-{i}",
|
||||
behaviour_id=f"behaviour:T-{i}")
|
||||
lines.append(json.loads(json.dumps(obs, sort_keys=True)))
|
||||
return lines
|
||||
|
||||
|
||||
def _close_case(rec, name, records, observation_id, retest, confirmed,
|
||||
contract_ref="", expect_raise=False):
|
||||
d = fresh_session_dir()
|
||||
with (Path(d) / "observations.jsonl").open("w") as f:
|
||||
for r in records:
|
||||
f.write(json.dumps(r, sort_keys=True) + "\n")
|
||||
try:
|
||||
result = oracle_mod.close_observation(
|
||||
d, observation_id, behaviour_retest_passed=retest,
|
||||
user_confirmed=confirmed, contract_ref=contract_ref)
|
||||
ret, expected = result, {"records": _redump(records, observation_id, retest,
|
||||
confirmed, contract_ref, FROZEN_NOW),
|
||||
"result": result}
|
||||
except ValueError as e:
|
||||
ret = f"ValueError: {e}"
|
||||
expected = {"records": [json.loads(json.dumps(r, sort_keys=True)) for r in records],
|
||||
"result": None}
|
||||
rec_kernel(rec, name,
|
||||
{"records": copy.deepcopy(records), "observation_id": observation_id,
|
||||
"behaviour_retest_passed": retest, "user_confirmed": confirmed,
|
||||
"contract_ref": contract_ref, "now": FROZEN_NOW},
|
||||
expected, oracle_ret=ret)
|
||||
|
||||
|
||||
def _redump(records, observation_id, retest, confirmed, contract_ref, now):
|
||||
out = []
|
||||
for r in records:
|
||||
r = copy.deepcopy(r)
|
||||
if r.get("observation_id") == observation_id:
|
||||
r["closure"] = {"behaviour_retest_passed": retest, "user_confirmed": confirmed,
|
||||
"contract_ref": contract_ref,
|
||||
"closed": retest and confirmed, "at": now}
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def c_close_full(rec):
|
||||
d = fresh_session_dir()
|
||||
records = _obs_lines(d, 1)
|
||||
_close_case(rec, "close-retest-and-confirmed", records,
|
||||
records[0]["observation_id"], True, True)
|
||||
|
||||
|
||||
def c_close_no_user_confirm(rec):
|
||||
d = fresh_session_dir()
|
||||
records = _obs_lines(d, 1)
|
||||
_close_case(rec, "close-green-without-user-not-closed", records,
|
||||
records[0]["observation_id"], True, False)
|
||||
|
||||
|
||||
def c_close_no_retest(rec):
|
||||
d = fresh_session_dir()
|
||||
records = _obs_lines(d, 1)
|
||||
_close_case(rec, "close-confirm-without-retest-not-closed", records,
|
||||
records[0]["observation_id"], False, True)
|
||||
|
||||
|
||||
def c_close_with_contract_ref(rec):
|
||||
d = fresh_session_dir()
|
||||
records = _obs_lines(d, 1)
|
||||
_close_case(rec, "close-with-contract-ref", records,
|
||||
records[0]["observation_id"], True, True,
|
||||
contract_ref="BUILDER-2026-10-12-factory-wave")
|
||||
|
||||
|
||||
def c_close_unknown(rec):
|
||||
d = fresh_session_dir()
|
||||
records = _obs_lines(d, 1)
|
||||
_close_case(rec, "close-unknown-id-raises", records, "obs-404", True, True,
|
||||
expect_raise=True)
|
||||
|
||||
|
||||
def c_close_preserves_others(rec):
|
||||
d = fresh_session_dir()
|
||||
records = _obs_lines(d, 3)
|
||||
_close_case(rec, "close-preserves-sibling-records", records,
|
||||
records[1]["observation_id"], True, True)
|
||||
|
||||
|
||||
# ── project_failure_to_forge (kernel: title/body; dry_run facade-only) ──────
|
||||
|
||||
def _project_case(rec, name, behaviour_id, observation):
|
||||
obs = oracle_mod.project_failure_to_forge(
|
||||
None, "conductor/rune", behaviour_id=behaviour_id,
|
||||
observation=observation, dry_run=True)
|
||||
from rune.testing_mode import _py_project
|
||||
title, body = _py_project(behaviour_id, observation)
|
||||
assert title == obs["title"]
|
||||
rec_kernel(rec, name,
|
||||
{"behaviour_id": behaviour_id, "observation": copy.deepcopy(observation)},
|
||||
{"title": obs["title"], "body": body}, oracle_ret=obs)
|
||||
|
||||
|
||||
def c_project_normal(rec):
|
||||
_project_case(rec, "project-normal", "T-1",
|
||||
{"observation_id": "obs-1", "observed": "500", "expected": "201",
|
||||
"reproduction": "POST /x"})
|
||||
|
||||
|
||||
def c_project_truncates_expected(rec):
|
||||
_project_case(rec, "project-expected-truncated-60", "T-2",
|
||||
{"observation_id": "obs-2", "observed": "o", "expected": "e" * 90,
|
||||
"reproduction": "r"})
|
||||
|
||||
|
||||
def c_project_missing_fields(rec):
|
||||
_project_case(rec, "project-missing-fields-empty-defaults", "T-3", {})
|
||||
|
||||
|
||||
def c_project_observation_id_none(rec):
|
||||
# missing observation_id renders Python str(None) → "None"
|
||||
_project_case(rec, "project-observation-id-none-string", "T-4",
|
||||
{"observed": "x", "expected": "y", "reproduction": "z"})
|
||||
|
||||
|
||||
def c_project_dry_run(rec):
|
||||
obs = {"observation_id": "obs-9", "observed": "x", "expected": "y",
|
||||
"reproduction": "z"}
|
||||
ret = oracle_mod.project_failure_to_forge(
|
||||
None, "conductor/rune", behaviour_id="T-9", observation=obs, dry_run=True)
|
||||
rec_facade(rec, "forge-dry-run-no-adapter-call",
|
||||
{"behaviour_id": "T-9", "observation": obs, "dry_run": True},
|
||||
{"projected": ret["projected"], "dry_run": ret["dry_run"],
|
||||
"title": ret["title"]}, oracle_ret=ret)
|
||||
|
||||
|
||||
# ── aggregate (kernel shaping over facade-fetched summaries) ────────────────
|
||||
|
||||
def _agg_case(rec, name, entries, ledger_flags, summaries):
|
||||
d = fresh_session_dir()
|
||||
locs = []
|
||||
for i, present in enumerate(ledger_flags):
|
||||
loc = Path(d) / f"p{i + 1}"
|
||||
loc.mkdir(exist_ok=True)
|
||||
if present:
|
||||
(loc / "behaviour_ledger.json").write_text("{}")
|
||||
locs.append(str(loc))
|
||||
reg_entries = [{"project_id": e["project_id"], "scaffold_location": loc}
|
||||
for e, loc in zip(entries, locs)]
|
||||
FakeLedger.summaries = {loc: s for loc, s in zip(locs, summaries) if s is not None}
|
||||
oracle_mod.BehaviourLedger = FakeLedger
|
||||
try:
|
||||
rows = oracle_mod.aggregate(FakeRegistry(reg_entries), Path(d))
|
||||
finally:
|
||||
from rune.behaviour_ledger import BehaviourLedger as RealLedger
|
||||
oracle_mod.BehaviourLedger = RealLedger
|
||||
kernel_entries = [{"project_id": e["project_id"],
|
||||
"summary": (copy.deepcopy(s) if present else None)}
|
||||
for e, present, s in zip(entries, ledger_flags, summaries)]
|
||||
rec_kernel(rec, name, {"entries": kernel_entries}, {"rows": rows}, oracle_ret=rows)
|
||||
|
||||
|
||||
def c_agg_no_ledger(rec):
|
||||
_agg_case(rec, "agg-no-ledger-note", [{"project_id": "p1"}], [False], [None])
|
||||
|
||||
|
||||
def c_agg_with_summary(rec):
|
||||
_agg_case(rec, "agg-with-summary", [{"project_id": "p1"}], [True],
|
||||
[{"outstanding": 2, "states": {"UNVALIDATED": 2}}])
|
||||
|
||||
|
||||
def c_agg_mixed(rec):
|
||||
_agg_case(rec, "agg-mixed-projects", [{"project_id": "p1"}, {"project_id": "p2"},
|
||||
{"project_id": "p3"}],
|
||||
[True, False, True],
|
||||
[{"outstanding": 1, "states": {"RETEST_REQUIRED": 1}}, None,
|
||||
{"outstanding": 0, "states": {}}])
|
||||
|
||||
|
||||
def c_agg_empty(rec):
|
||||
_agg_case(rec, "agg-empty-registry", [], [], [])
|
||||
|
||||
|
||||
# ── load / save (facade-only: FILE IO stays in Python) ──────────────────────
|
||||
|
||||
def c_load_missing(rec):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p", scope_component="component:c")
|
||||
rec_facade(rec, "load-missing-file-default-state", {"path": "missing"},
|
||||
{"state": s.state})
|
||||
|
||||
|
||||
def c_load_valid(rec):
|
||||
d = fresh_session_dir()
|
||||
pre = dict(fresh_state(), current_item="behaviour:T-1", paused=True)
|
||||
Path(d, "testing_session.json").write_text(json.dumps(pre, indent=1, sort_keys=True))
|
||||
s = oracle_mod.TestingSession(d)
|
||||
rec_facade(rec, "load-valid-file-roundtrip", {"path": "valid"},
|
||||
{"state": s.state})
|
||||
|
||||
|
||||
def c_save(rec):
|
||||
d = fresh_session_dir()
|
||||
s = oracle_mod.TestingSession(d, project="p")
|
||||
s.state["current_item"] = "behaviour:T-1"
|
||||
s.state["blockers"].append({"behaviour_id": "behaviour:T-2", "note": "n", "at": FROZEN_NOW})
|
||||
s.pause()
|
||||
s.save()
|
||||
text = Path(d, "testing_session.json").read_text()
|
||||
rec_facade(rec, "save-stamps-updated-at-indent1-sortkeys",
|
||||
{"state": copy.deepcopy(s.state)}, {"file_text": text},
|
||||
oracle_ret=None)
|
||||
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
DEFAULT_STATE = [c_ds_default, c_ds_scope, c_ds_empty_project]
|
||||
NEXT = [c_next_paused, c_next_empty, c_next_first, c_next_overwrites_current,
|
||||
c_next_empty_clears_current]
|
||||
RECORD = [c_record_pass, c_record_fail, c_record_appends_to_existing]
|
||||
PAUSE = [c_pause_from_active, c_pause_already_paused]
|
||||
RESUME = [c_resume_from_paused, c_resume_already_active]
|
||||
BLOCKER = [c_blocker_normal, c_blocker_truncates, c_blocker_appends]
|
||||
OBS = [c_obs_defaults, c_obs_full, c_obs_candidate_areas_none, c_obs_candidate_areas_copy]
|
||||
CLOSE = [c_close_full, c_close_no_user_confirm, c_close_no_retest,
|
||||
c_close_with_contract_ref, c_close_unknown, c_close_preserves_others]
|
||||
PROJECT = [c_project_normal, c_project_truncates_expected, c_project_missing_fields,
|
||||
c_project_observation_id_none]
|
||||
FORGE = [c_project_dry_run]
|
||||
AGG = [c_agg_no_ledger, c_agg_with_summary, c_agg_mixed, c_agg_empty]
|
||||
LOAD = [c_load_missing, c_load_valid]
|
||||
SAVE = [c_save]
|
||||
|
||||
GROUPS = [("default_state", DEFAULT_STATE), ("next_item", NEXT), ("record", RECORD),
|
||||
("pause", PAUSE), ("resume", RESUME), ("note_blocker", BLOCKER),
|
||||
("record_observation", OBS), ("close_observation", CLOSE),
|
||||
("project", PROJECT), ("forge_dry_run", FORGE), ("aggregate", AGG),
|
||||
("load", LOAD), ("save", SAVE)]
|
||||
|
||||
# kernel symbol per group (None → facade-only group, skipped kernel-side)
|
||||
KERNEL = {
|
||||
"default_state": "tm_default_state",
|
||||
"next_item": "tm_next_item",
|
||||
"record": "tm_record",
|
||||
"pause": "tm_pause",
|
||||
"resume": "tm_resume",
|
||||
"note_blocker": "tm_note_blocker",
|
||||
"record_observation": "tm_record_observation",
|
||||
"close_observation": "tm_close_observation",
|
||||
"project": "tm_project",
|
||||
"forge_dry_run": None,
|
||||
"aggregate": "tm_aggregate",
|
||||
"load": None,
|
||||
"save": None,
|
||||
}
|
||||
|
||||
# loader wrapper + slice of `expected` the unwrapped FFI value maps to
|
||||
CALLS = {
|
||||
"default_state": ("default_state", lambda w: w["state"],
|
||||
lambda i, ld: ld.default_state(project=i["project"],
|
||||
scope_component=i["scope_component"],
|
||||
now=i["now"])),
|
||||
"next_item": ("next_item", lambda w: w,
|
||||
lambda i, ld: ld.next_item(state=i["state"], outstanding=i["outstanding"])),
|
||||
"record": ("record", lambda w: w["state"],
|
||||
lambda i, ld: ld.record(state=i["state"], rec=i["rec"],
|
||||
passed=i["passed"], now=i["now"])),
|
||||
"pause": ("pause", lambda w: w["state"], lambda i, ld: ld.pause(state=i["state"])),
|
||||
"resume": ("resume", lambda w: w["state"], lambda i, ld: ld.resume(state=i["state"])),
|
||||
"note_blocker": ("note_blocker", lambda w: w["state"],
|
||||
lambda i, ld: ld.note_blocker(state=i["state"],
|
||||
behaviour_id=i["behaviour_id"],
|
||||
note=i["note"], now=i["now"])),
|
||||
"record_observation": ("record_observation", lambda w: w["observation"],
|
||||
lambda i, ld: ld.record_observation(
|
||||
now=i["now"], now_ts=i["now_ts"], observed=i["observed"],
|
||||
expected=i["expected"], behaviour_id=i["behaviour_id"],
|
||||
environment=i["environment"], reproduction=i["reproduction"],
|
||||
candidate_areas=i["candidate_areas"],
|
||||
confidence=i["confidence"])),
|
||||
"close_observation": ("close_observation", lambda w: w,
|
||||
lambda i, ld: ld.close_observation(
|
||||
records=i["records"], observation_id=i["observation_id"],
|
||||
behaviour_retest_passed=i["behaviour_retest_passed"],
|
||||
user_confirmed=i["user_confirmed"],
|
||||
contract_ref=i["contract_ref"], now=i["now"])),
|
||||
"project": ("project", lambda w: w,
|
||||
lambda i, ld: ld.project(behaviour_id=i["behaviour_id"],
|
||||
observation=i["observation"])),
|
||||
"aggregate": ("aggregate", lambda w: w["rows"],
|
||||
lambda i, ld: ld.aggregate(entries=i["entries"])),
|
||||
}
|
||||
|
||||
|
||||
def canon(x) -> str:
|
||||
"""Content canonicalization: a sorted dump makes the comparison
|
||||
key-order independent (int keys never occur here)."""
|
||||
return json.dumps(json.loads(json.dumps(x)), sort_keys=True)
|
||||
|
||||
|
||||
def main():
|
||||
frozen = None
|
||||
if REPLAY:
|
||||
with open(TRUTH_PATH) as f:
|
||||
frozen = json.load(f)["cases"]
|
||||
|
||||
groups = {}
|
||||
for group, cases in GROUPS:
|
||||
recs = []
|
||||
|
||||
def rec(name, inp, oracle_ret, expected, _r=recs):
|
||||
entry = {"name": name, "input": inp, "return": oracle_ret,
|
||||
"expected": expected}
|
||||
if REPLAY:
|
||||
old = next(c for c in frozen[group] if c["name"] == name)
|
||||
for key in ("input", "return", "expected"):
|
||||
got, want = entry[key], old[key]
|
||||
if canon(got) != canon(want):
|
||||
print(f"FACADE DIVERGE [{group}] {name} {key}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
recs.append({"name": name, "diverged": key})
|
||||
return
|
||||
_r.append(entry)
|
||||
|
||||
for c in cases:
|
||||
c(rec)
|
||||
groups[group] = recs
|
||||
|
||||
total = sum(len(v) for v in groups.values())
|
||||
if REPLAY:
|
||||
facade_failures = sum(1 for v in groups.values() for c in v if "diverged" in c)
|
||||
|
||||
failures = 0
|
||||
checked = 0
|
||||
for group, cases in frozen.items():
|
||||
spec = CALLS.get(group)
|
||||
if spec is None:
|
||||
continue
|
||||
_, unwrap, call = spec
|
||||
for case in cases:
|
||||
checked += 1
|
||||
got = call(copy.deepcopy(case["input"]), tm_loader)
|
||||
want = unwrap(case["expected"])
|
||||
if canon(got) != canon(want):
|
||||
failures += 1
|
||||
print(f"KERNEL DIVERGE [{group}] {case['name']}")
|
||||
print(" got: ", json.dumps(got, sort_keys=True)[:600])
|
||||
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
|
||||
print(f"replay: facade {total - facade_failures}/{total}, "
|
||||
f"kernel {checked - failures}/{checked}")
|
||||
return 1 if (failures or facade_failures) else 0
|
||||
|
||||
doc = {
|
||||
"frozen": "2026-10-12T12:00:00Z",
|
||||
"oracle": "src/rune/testing_mode.py",
|
||||
"pin": "RUNE_TM_CORE=python",
|
||||
"note": "Frozen via scripts/gen_testing_mode_truth.py against the live "
|
||||
"Python oracle; wall clock frozen at " + FROZEN_NOW +
|
||||
" (FakeTime patched into testing_mode.time; epoch " + str(FROZEN_TS) +
|
||||
"). Battery embeds this VERBATIM. Groups `forge_dry_run`, `load` "
|
||||
"and `save` are facade-only (FILE IO + adapter call stay in Python).",
|
||||
"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,397 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the time_awareness truth corpus (dichotomy wave, #2479 tranche).
|
||||
|
||||
Drives the LIVE python oracle (src/rune/time_awareness.py) with the clock
|
||||
pinned (fake datetime.now + fake monotonic — RUNE_TA_CORE=python is pinned
|
||||
at the top, poisoned-truth law) and freezes every observation into
|
||||
src/rust/time_awareness/src/frozen_ta_truth.json.
|
||||
|
||||
Coverage: ALL public functions and every branch —
|
||||
* get_context: 14-point time_of_day × circadian grid (weekend + weekday),
|
||||
update_interaction True/False, urgency branches (>24h, ==24h, >4h,
|
||||
==4h, 0.1h..4h, ==0.1h, <0.1h, zero, future), save-debounce fire/no-fire
|
||||
* parse_temporal_reference: all 10 patterns, priority order, word-boundary
|
||||
positives/negatives, case, punctuation, unicode, whitespace runs
|
||||
* get_status (read-only), flush, _mark_dirty, _save_if_dirty
|
||||
* _load: valid aware/no-fraction/Z/offset/naive ISO, null, missing key,
|
||||
corrupt JSON, non-dict JSON, invalid ISO (blanket except → None)
|
||||
* module singleton fns (get_time_awareness/get_context/get_status/
|
||||
parse_temporal_reference) + main() CLI — facade-level, live parity
|
||||
|
||||
Timestamps need NO masking: the clock is pinned, so every field
|
||||
(current_time, dates, hours_since, last_interaction, last_save_time) is
|
||||
deterministic and frozen verbatim.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, os.path.join(_ROOT, "src"))
|
||||
|
||||
os.environ["RUNE_TA_CORE"] = "python" # poisoned-truth law: pin the fallback
|
||||
os.environ["RUNE_MEMORY_DIR"] = "/tmp/ta_truth_memory"
|
||||
|
||||
import rune.time_awareness as ta # noqa: E402
|
||||
|
||||
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class _FakeNow:
|
||||
"""Stands in for the module's `datetime` name — .now(tz) is pinned;
|
||||
everything else delegates to the real class (fromisoformat in _load)."""
|
||||
|
||||
fixed = None
|
||||
|
||||
@staticmethod
|
||||
def now(tz=None):
|
||||
return _FakeNow.fixed
|
||||
|
||||
@staticmethod
|
||||
def fromisoformat(s):
|
||||
return datetime.fromisoformat(s)
|
||||
|
||||
|
||||
class _FakeTime:
|
||||
mono = 0.0
|
||||
|
||||
@staticmethod
|
||||
def monotonic():
|
||||
return _FakeTime.mono
|
||||
|
||||
|
||||
ta.datetime = _FakeNow
|
||||
ta._time = _FakeTime
|
||||
|
||||
|
||||
def us_of(dt):
|
||||
return (dt - EPOCH) // timedelta(microseconds=1)
|
||||
|
||||
|
||||
def dt_of(us, micro=None):
|
||||
dt = EPOCH + timedelta(microseconds=us)
|
||||
return dt
|
||||
|
||||
|
||||
def set_clock(op):
|
||||
us = op.get("now_us")
|
||||
_FakeNow.fixed = dt_of(us) if us is not None else _FakeNow.fixed
|
||||
if "mono" in op:
|
||||
_FakeTime.mono = op["mono"]
|
||||
|
||||
|
||||
def iso_of(us):
|
||||
return dt_of(us).isoformat()
|
||||
|
||||
|
||||
BASE_US = us_of(datetime(2026, 10, 11, 12, 0, 0, 123456, tzinfo=timezone.utc))
|
||||
HOUR_US = 3600 * 1_000_000
|
||||
|
||||
|
||||
def _mem_file():
|
||||
"""The singleton's default storage path, resolved at op time — other
|
||||
tests may repoint RUNE_MEMORY_DIR mid-session (env hygiene leaks)."""
|
||||
import memory_env
|
||||
|
||||
return os.path.join(memory_env.memory_dir(), "time_awareness.json")
|
||||
|
||||
|
||||
def state_obs(instance):
|
||||
return {
|
||||
"last_interaction": instance.last_interaction.isoformat()
|
||||
if instance.last_interaction
|
||||
else None,
|
||||
"dirty": instance._dirty,
|
||||
"last_save_time": instance._last_save_time,
|
||||
"file_exists": os.path.exists(instance.storage_path),
|
||||
}
|
||||
|
||||
|
||||
def file_obs(path):
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
with open(path) as f:
|
||||
raw = f.read()
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except ValueError: # corrupt content — deterministic sentinel, both pins
|
||||
return {"__unparseable__": raw}
|
||||
|
||||
|
||||
class Driver:
|
||||
def __init__(self, workdir):
|
||||
self.workdir = workdir
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
self.instance = None
|
||||
self.n = 0
|
||||
|
||||
def step(self, op):
|
||||
kind = op["op"]
|
||||
set_clock(op)
|
||||
if kind == "new":
|
||||
self.n += 1
|
||||
path = os.path.join(self.workdir, f"sto_{self.n}.json")
|
||||
seed = op.get("seed_file")
|
||||
if seed is None:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
else:
|
||||
with open(path, "w") as f:
|
||||
f.write(seed)
|
||||
self.instance = ta.TimeAwareness(storage_path=path)
|
||||
return {"result": None, "state": state_obs(self.instance)}
|
||||
if kind == "seed_last":
|
||||
li = op.get("last_interaction")
|
||||
self.instance.last_interaction = (
|
||||
datetime.fromisoformat(li) if li is not None else None
|
||||
)
|
||||
return {"result": None, "state": state_obs(self.instance)}
|
||||
if kind == "ctx":
|
||||
update = op.get("update", True)
|
||||
prev_ls = self.instance._last_save_time
|
||||
try:
|
||||
r = self.instance.get_context(update_interaction=update).to_dict()
|
||||
except Exception as e: # noqa: BLE001 — the oracle raising IS the truth
|
||||
return {
|
||||
"result": {"error": type(e).__name__},
|
||||
"state": state_obs(self.instance),
|
||||
}
|
||||
ob = {"result": r, "state": state_obs(self.instance)}
|
||||
# exact _save_if_dirty decision: fired → the file obs carries it
|
||||
if update and (op["mono"] - prev_ls >= ta.TimeAwareness._SAVE_DEBOUNCE_SECS):
|
||||
ob["file"] = file_obs(self.instance.storage_path)
|
||||
return ob
|
||||
if kind == "status":
|
||||
try:
|
||||
r = self.instance.get_status()
|
||||
except Exception as e: # noqa: BLE001 — naive last_interaction raises; that IS the truth
|
||||
r = {"error": type(e).__name__}
|
||||
return {
|
||||
"result": r,
|
||||
"state": state_obs(self.instance),
|
||||
}
|
||||
if kind == "parse":
|
||||
return {
|
||||
"result": self.instance.parse_temporal_reference(op["text"]),
|
||||
"state": state_obs(self.instance),
|
||||
}
|
||||
if kind == "flush":
|
||||
self.instance.flush()
|
||||
return {
|
||||
"result": None,
|
||||
"state": state_obs(self.instance),
|
||||
"file": file_obs(self.instance.storage_path),
|
||||
}
|
||||
if kind == "mark_dirty":
|
||||
self.instance._mark_dirty()
|
||||
return {"result": None, "state": state_obs(self.instance)}
|
||||
if kind == "save_if_dirty":
|
||||
self.instance._save_if_dirty()
|
||||
return {
|
||||
"result": None,
|
||||
"state": state_obs(self.instance),
|
||||
"file": file_obs(self.instance.storage_path),
|
||||
}
|
||||
if kind == "mod_ctx" or kind == "mod_status" or kind == "mod_parse":
|
||||
mem_file = _mem_file()
|
||||
seed = op.get("seed_file")
|
||||
if seed is None:
|
||||
if os.path.exists(mem_file):
|
||||
os.remove(mem_file)
|
||||
else:
|
||||
with open(mem_file, "w") as f:
|
||||
f.write(seed)
|
||||
ta._time_awareness = None
|
||||
if kind == "mod_ctx":
|
||||
r = ta.get_context().to_dict()
|
||||
elif kind == "mod_status":
|
||||
r = ta.get_status()
|
||||
else:
|
||||
r = ta.parse_temporal_reference(op["text"])
|
||||
inst = ta._time_awareness
|
||||
ob = {"result": r, "state": state_obs(inst)}
|
||||
if kind == "mod_ctx":
|
||||
ob["file"] = file_obs(mem_file)
|
||||
return ob
|
||||
if kind == "cli":
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
ta._time_awareness = self.instance
|
||||
buf = io.StringIO()
|
||||
old_argv = sys.argv
|
||||
sys.argv = ["time_awareness.py"] + op.get("argv", [])
|
||||
try:
|
||||
with contextlib.redirect_stdout(buf):
|
||||
ta.main()
|
||||
finally:
|
||||
sys.argv = old_argv
|
||||
return {
|
||||
"result": buf.getvalue(),
|
||||
"state": state_obs(self.instance),
|
||||
"file": file_obs(self.instance.storage_path),
|
||||
}
|
||||
raise ValueError(f"unknown op {kind}")
|
||||
|
||||
def run(self, ops):
|
||||
return [self.step(op) for op in ops]
|
||||
|
||||
|
||||
def ctx_case(now_us, update, seed=None, hour=None):
|
||||
ops = [{"op": "new", "mono": 0.0}]
|
||||
if seed is not None:
|
||||
ops[0]["seed_file"] = seed
|
||||
ops.append({"op": "ctx", "update": update, "now_us": now_us, "mono": 0.0})
|
||||
return ops
|
||||
|
||||
|
||||
def scenarios():
|
||||
out = []
|
||||
|
||||
# S1 — time_of_day × circadian grid, weekend (2026-10-11 = Sunday) + weekday
|
||||
ops = []
|
||||
for h, m in [(4, 0), (5, 0), (6, 0), (8, 30), (9, 0), (11, 59), (12, 0),
|
||||
(16, 59), (17, 0), (18, 0), (20, 59), (21, 0), (22, 0), (23, 59)]:
|
||||
ops += [
|
||||
{"op": "new", "mono": 0.0},
|
||||
{"op": "ctx", "update": False,
|
||||
"now_us": us_of(datetime(2026, 10, 11, h, m, 0, 0, tzinfo=timezone.utc)),
|
||||
"mono": 0.0},
|
||||
]
|
||||
for h in (10, 13):
|
||||
ops += [
|
||||
{"op": "new", "mono": 0.0},
|
||||
{"op": "ctx", "update": False,
|
||||
"now_us": us_of(datetime(2026, 10, 13, h, 15, 0, 0, tzinfo=timezone.utc)),
|
||||
"mono": 0.0},
|
||||
]
|
||||
out.append({"name": "tod_circadian_grid", "ops": ops})
|
||||
|
||||
# S2 — urgency branches (update=False, seeded last_interaction)
|
||||
ops = []
|
||||
offsets_h = [25.0, 24.0, 4.0 + 1.0 / 3600.0, 4.0, 2.0, 0.1, 5.0 / 60.0, 0.0, -1.0]
|
||||
for i, oh in enumerate(offsets_h):
|
||||
li_us = BASE_US - int(oh * HOUR_US)
|
||||
seed = json.dumps(
|
||||
{"last_interaction": iso_of(li_us)}, indent=2
|
||||
) if i % 2 == 0 else None
|
||||
ops += [{"op": "new", "mono": 0.0}]
|
||||
if seed is None:
|
||||
ops += [{"op": "seed_last", "last_interaction": iso_of(li_us), "mono": 0.0}]
|
||||
ops += [{"op": "ctx", "update": False, "now_us": BASE_US, "mono": 0.0}]
|
||||
out.append({"name": "urgency_branches", "ops": ops})
|
||||
|
||||
# S3 — parse_temporal_reference: every pattern + boundaries
|
||||
texts = [
|
||||
"yesterday", "today", "tomorrow", "last week", "next week",
|
||||
"last month", "next month", "this morning", "this afternoon", "tonight",
|
||||
"no temporal refs here", "YESTERDAY we said TODAY", "yesterdays news today",
|
||||
"see you tomorrow.", "last week", "last\tweek", "lastweek",
|
||||
"xlast weeky", "café yesterday", "yesterdayé",
|
||||
"the last week of tomorrow", "next month and last month",
|
||||
"Monday morning blues this morning", "tonight, tomorrow",
|
||||
]
|
||||
ops = [{"op": "new", "mono": 0.0}] + [
|
||||
{"op": "parse", "text": t, "now_us": BASE_US, "mono": 0.0} for t in texts
|
||||
]
|
||||
out.append({"name": "parse_references", "ops": ops})
|
||||
|
||||
# S4 — save/debounce policy across a call sequence
|
||||
out.append({
|
||||
"name": "save_policy",
|
||||
"ops": [
|
||||
{"op": "new", "mono": 0.0},
|
||||
{"op": "ctx", "update": True, "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "ctx", "update": False, "now_us": BASE_US, "mono": 10.0},
|
||||
{"op": "mark_dirty", "mono": 10.0},
|
||||
{"op": "save_if_dirty", "mono": 20.0},
|
||||
{"op": "save_if_dirty", "mono": 40.0},
|
||||
{"op": "ctx", "update": True, "now_us": BASE_US + HOUR_US, "mono": 45.0},
|
||||
{"op": "flush", "mono": 45.0},
|
||||
{"op": "flush", "mono": 45.0},
|
||||
{"op": "status", "now_us": BASE_US + HOUR_US, "mono": 45.0},
|
||||
],
|
||||
})
|
||||
|
||||
# S5 — _load file-seed matrix
|
||||
seeds = [
|
||||
'{"last_interaction": "2026-10-10T10:00:00.000000+00:00"}',
|
||||
'{"last_interaction": "2026-10-10T10:00:00+00:00"}',
|
||||
'{"last_interaction": null}',
|
||||
"{}",
|
||||
"corrupt{",
|
||||
"[1, 2]",
|
||||
'{"last_interaction": "garbage"}',
|
||||
'{"last_interaction": "2026-10-10T10:00:00"}',
|
||||
'{"last_interaction": "2026-10-10T05:00:00-05:00"}',
|
||||
'{"last_interaction": "2026-10-10T10:00:00Z"}',
|
||||
]
|
||||
ops = []
|
||||
for i, s in enumerate(seeds):
|
||||
ops.append({"op": "new", "mono": 0.0, "seed_file": s})
|
||||
if i in (0, 7, 8):
|
||||
ops.append({"op": "ctx", "update": False, "now_us": BASE_US, "mono": 0.0})
|
||||
out.append({"name": "load_matrix", "ops": ops})
|
||||
|
||||
# S6 — update=True sequence: debounce fire across calls, status, flush, parse
|
||||
out.append({
|
||||
"name": "update_sequence",
|
||||
"ops": [
|
||||
{"op": "new", "mono": 0.0},
|
||||
{"op": "ctx", "update": True, "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "ctx", "update": True, "now_us": BASE_US + HOUR_US, "mono": 40.0},
|
||||
{"op": "status", "now_us": BASE_US + HOUR_US, "mono": 40.0},
|
||||
{"op": "flush", "mono": 40.0},
|
||||
{"op": "parse", "text": "yesterday", "now_us": BASE_US + HOUR_US, "mono": 40.0},
|
||||
{"op": "seed_last", "last_interaction": None, "mono": 40.0},
|
||||
{"op": "ctx", "update": True, "now_us": BASE_US + HOUR_US, "mono": 90.0},
|
||||
],
|
||||
})
|
||||
|
||||
# S7 — module singleton + CLI (facade-level; battery replays the kernel ops)
|
||||
out.append({
|
||||
"name": "module_and_cli",
|
||||
"ops": [
|
||||
{"op": "mod_ctx", "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "mod_status", "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "mod_parse", "text": "next week", "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "mod_ctx", "now_us": BASE_US, "mono": 0.0,
|
||||
"seed_file": '{"last_interaction": "2026-10-10T10:00:00+00:00"}'},
|
||||
{"op": "new", "mono": 0.0},
|
||||
{"op": "cli", "argv": ["status"], "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "cli", "argv": ["context"], "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "cli", "argv": ["bogus"], "now_us": BASE_US, "mono": 0.0},
|
||||
{"op": "cli", "argv": [], "now_us": BASE_US, "mono": 0.0},
|
||||
],
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
shutil.rmtree("/tmp/ta_truth_memory", ignore_errors=True)
|
||||
os.makedirs("/tmp/ta_truth_memory", exist_ok=True)
|
||||
scenarios_doc = scenarios()
|
||||
driver = Driver("/tmp/ta_truth_work")
|
||||
for s in scenarios_doc:
|
||||
s["obs"] = [driver.step(op) for op in s["ops"]]
|
||||
doc = {
|
||||
"module": "src/rune/time_awareness.py",
|
||||
"pinned": "RUNE_TA_CORE=python (poisoned-truth law); clock pinned via fake datetime.now + monotonic",
|
||||
"scenarios": scenarios_doc,
|
||||
}
|
||||
out_path = os.path.join(_ROOT, "src", "rust", "time_awareness", "src", "frozen_ta_truth.json")
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(doc, f, indent=1)
|
||||
f.write("\n")
|
||||
n_obs = sum(len(s["obs"]) for s in scenarios_doc)
|
||||
print(f"frozen {n_obs} observations across {len(scenarios_doc)} scenarios → {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze toolchain oracle truth for the Rust port battery (dichotomy
|
||||
wave #2479 tranche, batch: toolchain — RUST side verdict).
|
||||
|
||||
Exercises EVERY public function across EVERY branch:
|
||||
ToolchainConstraints (known/unknown key classification, get with and
|
||||
without default, to_dict), DecisionRecord (list normalization: sorting
|
||||
of libraries/decisive_constraints/evidence_refs, None-list defaults,
|
||||
alternatives copy, confidence), CONSTRAINT_KEYS (frozen vocabulary),
|
||||
_major_change (existing-language equality branches), resolve_toolchain
|
||||
(INSUFFICIENT_EVIDENCE short path, KEEP_EXISTING with/without hard
|
||||
factors and performance evidence, same-language request, CHANGE_MAJOR
|
||||
with and without significant evidence, greenfield recommendation,
|
||||
greenfield INSUFFICIENT_EVIDENCE, falsy existing_language edges).
|
||||
|
||||
Output: src/rust/toolchain/src/frozen_tc_truth.json — replayed VERBATIM
|
||||
by src/rust/toolchain/src/branch_battery.rs.
|
||||
|
||||
The module is fully deterministic (no wall-clock fields) — every case
|
||||
compares exactly, no masking.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_TC_CORE"] = "python" # POISONED-TRUTH LAW: freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import toolchain as tc # noqa: E402
|
||||
|
||||
cases: dict[str, list] = {}
|
||||
|
||||
|
||||
def run(fn: str, name: str, *args, **kwargs):
|
||||
out = getattr(tc, fn)(**kwargs) if args == () else getattr(tc, fn)(*args, **kwargs)
|
||||
if hasattr(out, "to_dict"):
|
||||
out = out.to_dict()
|
||||
cases.setdefault(fn, []).append(
|
||||
{"name": name, "input": kwargs if args == () else {"args": args, "kwargs": kwargs},
|
||||
"expected": out})
|
||||
|
||||
|
||||
# ── CONSTRAINT_KEYS: the frozen vocabulary ────────────────────────────
|
||||
cases["constraint_keys"] = [{"name": "vocabulary",
|
||||
"input": {},
|
||||
"expected": sorted(tc.CONSTRAINT_KEYS)}]
|
||||
|
||||
# ── ToolchainConstraints: classification, get, to_dict ────────────────
|
||||
for nm, kw in (
|
||||
("known-only", dict(existing_language="python", target_platform="linux")),
|
||||
("unknown-only", dict(wibble="future-thing", flurb=42)),
|
||||
("mixed", dict(existing_language="rust", existing_runtime="tokio",
|
||||
not_a_key=[1, 2], performance_sensitive=True, none_val=None)),
|
||||
("empty", {}),
|
||||
("every-known-key", {k: f"v-{k}" for k in tc.CONSTRAINT_KEYS}),
|
||||
("falsy-values", dict(existing_language="", performance_sensitive=False,
|
||||
memory_constrained=0)),
|
||||
):
|
||||
c = tc.ToolchainConstraints(**kw)
|
||||
cases.setdefault("constraints", []).append(
|
||||
{"name": nm, "input": {"kwargs": kw}, "expected": c.to_dict()})
|
||||
|
||||
for nm, kw, key, default, has_default in (
|
||||
("get-known", dict(existing_language="python"), "existing_language", None, False),
|
||||
("get-missing-no-default", dict(existing_language="python"), "existing_runtime", None, False),
|
||||
("get-missing-with-default", dict(existing_language="python"), "existing_runtime", "", True),
|
||||
("get-unknown-key-stored", dict(wibble="x"), "wibble", None, False),
|
||||
("get-known-falsy-value", dict(existing_language=""), "existing_language", "dflt", True),
|
||||
("get-default-none-explicit", dict(a="1"), "b", None, True),
|
||||
):
|
||||
c = tc.ToolchainConstraints(**kw)
|
||||
val = c.get(key, default) if has_default else c.get(key)
|
||||
cases.setdefault("constraints_get", []).append(
|
||||
{"name": nm, "input": {"known": c.known, "key": key,
|
||||
"default": default if has_default else None},
|
||||
"expected": val})
|
||||
|
||||
# ── DecisionRecord: normalization branches ────────────────────────────
|
||||
for nm, kw in (
|
||||
("full-fields", dict(
|
||||
selected_language="rust", runtime="tokio", framework="axum",
|
||||
libraries=["zlib", "openssl", "a-lib"], build_system="cargo",
|
||||
alternatives=[{"language": "go", "rejected_because": "no evidence"}],
|
||||
decisive_constraints=["requested_language", "existing_language"],
|
||||
rationale="measured benefit", evidence_refs=["bench-2", "bench-1"],
|
||||
confidence=0.8, user_approval_required=True, resolution="CHANGE_MAJOR")),
|
||||
("defaults", dict(selected_language="python")),
|
||||
("none-lists-become-sorted-empty", dict(
|
||||
selected_language="python", libraries=None, decisive_constraints=None,
|
||||
evidence_refs=None, alternatives=None)),
|
||||
("unsorted-normalized", dict(
|
||||
selected_language="python", libraries=["b", "a", "c"],
|
||||
decisive_constraints=["z", "a"], evidence_refs=["e2", "e1", "e0"],
|
||||
confidence=0.85)),
|
||||
("empty-lists-kept", dict(
|
||||
selected_language="python", libraries=[], alternatives=[],
|
||||
decisive_constraints=[], evidence_refs=[], resolution="KEEP_EXISTING")),
|
||||
):
|
||||
d = tc.DecisionRecord(**kw)
|
||||
cases.setdefault("decision_record", []).append(
|
||||
{"name": nm, "input": kw, "expected": d.to_dict()})
|
||||
|
||||
# ── _major_change ─────────────────────────────────────────────────────
|
||||
for nm, kw, language in (
|
||||
("different-language", dict(existing_language="python"), "rust"),
|
||||
("same-language", dict(existing_language="python"), "python"),
|
||||
("no-existing", {}, "rust"),
|
||||
("falsy-existing", dict(existing_language=""), "rust"),
|
||||
("non-str-existing", dict(existing_language=None), "rust"),
|
||||
):
|
||||
c = tc.ToolchainConstraints(**kw)
|
||||
cases.setdefault("major_change", []).append(
|
||||
{"name": nm, "input": {"known": c.known, "language": language},
|
||||
"expected": tc._major_change(c, language)})
|
||||
|
||||
# ── resolve_toolchain: every branch ───────────────────────────────────
|
||||
def res(nm, ckw, **kw):
|
||||
c = tc.ToolchainConstraints(**ckw)
|
||||
d = tc.resolve_toolchain(c, **kw)
|
||||
cases.setdefault("resolve_toolchain", []).append(
|
||||
{"name": nm,
|
||||
"input": {"known": c.known, "unknown": c.unknown, **kw},
|
||||
"expected": d.to_dict()})
|
||||
|
||||
# INSUFFICIENT short path: no existing, no request, <2 hard factors
|
||||
res("insufficient-no-factors", dict(existing_runtime="tokio"))
|
||||
res("insufficient-one-factor", dict(target_platform="linux"))
|
||||
# falls through to greenfield INSUFFICIENT: >=2 hard factors, approval required
|
||||
res("greenfield-insufficient-two-factors", dict(target_platform="linux", deployment="server"))
|
||||
res("greenfield-insufficient-bools-count", dict(performance_sensitive=True, memory_constrained=True))
|
||||
# KEEP_EXISTING branches
|
||||
res("keep-existing-no-hard-factors", dict(existing_language="python"))
|
||||
res("keep-existing-with-hard-factors", dict(existing_language="rust",
|
||||
performance_sensitive=True))
|
||||
res("keep-existing-with-runtime-framework", dict(
|
||||
existing_language="typescript", existing_runtime="node",
|
||||
existing_framework="svelte", target_platform="web"))
|
||||
res("keep-existing-performance-evidence-rejected", dict(
|
||||
existing_language="python", performance_sensitive=True),
|
||||
performance_evidence={"significant_benefit": True, "basis": "measured 10x"})
|
||||
res("keep-existing-empty-perf-evidence", dict(existing_language="python"),
|
||||
performance_evidence={})
|
||||
res("keep-existing-same-language-request", dict(existing_language="python"),
|
||||
requested_language="python")
|
||||
res("keep-existing-with-evidence-refs", dict(existing_language="go"),
|
||||
evidence_refs=["ref-b", "ref-a", "ref-c"])
|
||||
# CHANGE_MAJOR branches
|
||||
res("change-major-no-evidence", dict(existing_language="python"),
|
||||
requested_language="rust")
|
||||
res("change-major-significant", dict(existing_language="python",
|
||||
performance_sensitive=True),
|
||||
requested_language="rust",
|
||||
performance_evidence={"significant_benefit": True, "basis": "measured"})
|
||||
res("change-major-falsy-significant", dict(existing_language="python"),
|
||||
requested_language="rust",
|
||||
performance_evidence={"significant_benefit": False})
|
||||
res("change-major-null-significant", dict(existing_language="python"),
|
||||
requested_language="zig", performance_evidence={"significant_benefit": None})
|
||||
# greenfield recommendation
|
||||
res("greenfield-requested", dict(target_platform="linux", deployment="server"),
|
||||
requested_language="zig")
|
||||
res("greenfield-requested-no-factors", dict(), requested_language="go")
|
||||
# falsy existing_language edges: "" behaves as absent
|
||||
res("falsy-existing-no-request", dict(existing_language=""))
|
||||
res("falsy-existing-with-request", dict(existing_language=""),
|
||||
requested_language="rust")
|
||||
# unknown keys ride along but do not affect the decision
|
||||
res("unknown-keys-ignored", dict(existing_language="python", wibble="x"),
|
||||
requested_language="python")
|
||||
|
||||
truth = {
|
||||
"frozen": "toolchain full oracle truth (dichotomy wave #2479)",
|
||||
"oracle": "src/rune/toolchain.py",
|
||||
"note": ("The module is fully deterministic — no wall-clock fields, "
|
||||
"every case compares exactly. error = the exact Python "
|
||||
"exception message where applicable."),
|
||||
"cases": cases,
|
||||
}
|
||||
out = Path(__file__).resolve().parents[1] / "src" / "rust" / "toolchain" / "src" / "frozen_tc_truth.json"
|
||||
out.write_text(json.dumps(truth, indent=1, sort_keys=True) + "\n")
|
||||
total = sum(len(v) for v in cases.values())
|
||||
print(f"frozen {total} cases across {len(cases)} functions -> {out}")
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open the #2479 dichotomy-wave construction contract for capability_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(
|
||||
"Port src/rune/capability_ledger.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_LEDGER_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": "capability_ledger parity port — evidence attached at close "
|
||||
"(SHAs reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/capability_ledger.py",
|
||||
"src/rune/cl_loader.py",
|
||||
"src/rust/capability_ledger/src/lib.rs",
|
||||
"src/rust/capability_ledger/src/branch_battery.rs",
|
||||
"scripts/gen_capability_ledger_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 lexicon_evolution.
|
||||
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/lexicon_evolution.py to FULL Rust (dichotomy factory, "
|
||||
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
|
||||
"delegation with RUNE_LE_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": "lexicon_evolution parity port — evidence attached at close "
|
||||
"(SHAs reported in the builder report)",
|
||||
}], scope_files=[
|
||||
"src/rune/lexicon_evolution.py",
|
||||
"src/rune/le_loader.py",
|
||||
"src/rust/lexicon_evolution/src/lib.rs",
|
||||
"src/rust/lexicon_evolution/src/branch_battery.rs",
|
||||
"scripts/gen_lexicon_evolution_truth.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:400])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/auto_context
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,864 @@
|
||||
// Package main — the FULL GO port of src/rune/auto_context.py (#2479
|
||||
// dichotomy tranche, GO side verdict). Every public Python function has a
|
||||
// name-preserving counterpart: run_warm_router → runWarmRouter,
|
||||
// run_memory_recall → runMemoryRecall, run_always_digest → runAlwaysDigest,
|
||||
// load_active_tasks → loadActiveTasks, build_auto_context →
|
||||
// buildAutoContext, save_context → saveContext, handle_hook → handleHook,
|
||||
// and the module CLI main() (argparse-compatible: build/show/hook, --event,
|
||||
// --data, --json; invalid choices exit 2). The PLUGIN manifest rides along
|
||||
// as pluginManifest().
|
||||
//
|
||||
// IO (subprocess runs, memory paths, wall clock) stays behind injectable
|
||||
// seams so the frozen truth battery replays deterministically: RunScript,
|
||||
// MemoryDir, ScriptsDir, PythonBin and ClockNow are swappable vars, swapped
|
||||
// per truth row by replay.go and restored after each row. The oracle's
|
||||
// subprocess payloads (warm_router.py route/always, memory_store.py recall)
|
||||
// arrive through the RunScript seam; the frozen truth pins the exact
|
||||
// message/query strings the port hands to that seam.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ── configuration ────────────────────────────────────────────────────────
|
||||
|
||||
// stopwordSet — the oracle's recall stopword table, 1:1.
|
||||
var stopwordSet = map[string]struct{}{
|
||||
"what": {}, "when": {}, "where": {}, "which": {}, "that": {}, "this": {},
|
||||
"with": {}, "from": {}, "have": {}, "been": {}, "were": {}, "they": {},
|
||||
"their": {}, "about": {}, "would": {}, "could": {}, "should": {},
|
||||
"will": {}, "into": {}, "your": {}, "just": {}, "like": {}, "know": {},
|
||||
"think": {}, "also": {}, "back": {}, "then": {}, "than": {}, "more": {},
|
||||
"some": {}, "only": {}, "very": {}, "does": {}, "done": {}, "make": {},
|
||||
"made": {}, "want": {}, "need": {}, "give": {}, "tell": {}, "there": {},
|
||||
"these": {}, "those": {}, "because": {}, "between": {}, "after": {},
|
||||
"before": {}, "under": {}, "over": {}, "again": {}, "once": {},
|
||||
"here": {}, "each": {}, "every": {}, "both": {}, "few": {}, "most": {},
|
||||
"other": {}, "such": {}, "same": {}, "being": {}, "having": {},
|
||||
}
|
||||
|
||||
// ── seams (swapped per truth row by replay.go) ───────────────────────────
|
||||
|
||||
// ExecResult — the slice of subprocess.run(capture_output=True, text=True)
|
||||
// the oracle consumes: stdout text, return code, and a non-nil Err when the
|
||||
// run failed to start, was killed, or hit its timeout (subprocess raised).
|
||||
type ExecResult struct {
|
||||
Stdout string
|
||||
ReturnCode int
|
||||
Err error
|
||||
}
|
||||
|
||||
var (
|
||||
// defaultPythonBin — memory_env.python_bin(): sys.executable of the
|
||||
// parent process (same venv, portable by construction).
|
||||
defaultPythonBin = func() string {
|
||||
if p := os.Getenv("RUNE_PYTHON"); p != "" {
|
||||
return p
|
||||
}
|
||||
return "python3"
|
||||
}
|
||||
// defaultScriptsDir — memory_env.scripts_dir() (RUNE_SCRIPTS_DIR).
|
||||
defaultScriptsDir = func() string {
|
||||
if d := os.Getenv("RUNE_SCRIPTS_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
return "src/rune"
|
||||
}
|
||||
// defaultMemoryDir — memory_env.memory_dir() (RUNE_MEMORY_DIR else
|
||||
// RUNE_HOME/memory else the default rune home).
|
||||
defaultMemoryDir = func() string {
|
||||
if d := os.Getenv("RUNE_MEMORY_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
home := os.Getenv("RUNE_HOME")
|
||||
if home == "" {
|
||||
home = "/mnt/workdrive/rune"
|
||||
}
|
||||
return filepath.Join(home, "memory")
|
||||
}
|
||||
// defaultClockNow — datetime.now(timezone.utc); build_auto_context
|
||||
// calls it once per build.
|
||||
defaultClockNow = func() time.Time { return time.Now().UTC() }
|
||||
// defaultRunScript — subprocess.run([python, script, ...], timeout=N):
|
||||
// timeout expiry surfaces as Err (subprocess.TimeoutExpired), a
|
||||
// nonzero exit keeps Stdout and sets ReturnCode.
|
||||
defaultRunScript = func(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, exe, args...)
|
||||
out, err := cmd.Output()
|
||||
res := ExecResult{Stdout: string(out)}
|
||||
if err != nil {
|
||||
res.Err = err
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
res.ReturnCode = ee.ExitCode()
|
||||
} else {
|
||||
res.ReturnCode = -1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
PythonBin = defaultPythonBin
|
||||
ScriptsDir = defaultScriptsDir
|
||||
MemoryDir = defaultMemoryDir
|
||||
)
|
||||
|
||||
// CommandRunner — the subprocess seam as an interface (Phase B): the
|
||||
// runner is swappable in embedded callers and tests (fake runners,
|
||||
// counting/pooled runners) without touching port code; the default
|
||||
// implementation is compile-time asserted against the contract.
|
||||
type CommandRunner interface {
|
||||
Run(exe string, args []string, timeout time.Duration) ExecResult
|
||||
}
|
||||
|
||||
// RunnerFunc adapts a plain function to CommandRunner.
|
||||
type RunnerFunc func(exe string, args []string, timeout time.Duration) ExecResult
|
||||
|
||||
// Run satisfies CommandRunner.
|
||||
func (f RunnerFunc) Run(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
return f(exe, args, timeout)
|
||||
}
|
||||
|
||||
// compile-time: the default subprocess implementation satisfies the seam
|
||||
var _ CommandRunner = RunnerFunc(defaultRunScript)
|
||||
|
||||
// Clock — the wall-clock seam as an interface (Phase B): build_auto_context
|
||||
// reads it exactly once per build, like the oracle's datetime.now call.
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// ClockFunc adapts a plain function to Clock.
|
||||
type ClockFunc func() time.Time
|
||||
|
||||
// Now satisfies Clock.
|
||||
func (f ClockFunc) Now() time.Time { return f() }
|
||||
|
||||
// compile-time: the default clock satisfies the seam
|
||||
var _ Clock = ClockFunc(defaultClockNow)
|
||||
|
||||
// defaultRunner / defaultClock — the real implementations, kept as named
|
||||
// seams so per-row swaps in replay.go can restore them.
|
||||
var defaultRunner CommandRunner = RunnerFunc(defaultRunScript)
|
||||
var defaultClock Clock = ClockFunc(defaultClockNow)
|
||||
|
||||
// Runner — the swapped-per-truth-row subprocess seam.
|
||||
var Runner CommandRunner = defaultRunner
|
||||
|
||||
// ClockNow — the swapped-per-truth-row clock seam.
|
||||
var ClockNow Clock = defaultClock
|
||||
|
||||
func warmRouterPath() string { return filepath.Join(ScriptsDir(), "warm_router.py") }
|
||||
func memoryStorePath() string {
|
||||
return filepath.Join(ScriptsDir(), "memory_store.py")
|
||||
}
|
||||
func contextFilePath() string {
|
||||
return filepath.Join(MemoryDir(), "index", "auto_context.json")
|
||||
}
|
||||
func tasksFilePath() string {
|
||||
return filepath.Join(MemoryDir(), "tasks", "persisted.json")
|
||||
}
|
||||
|
||||
// isoformat — datetime.isoformat() on an aware UTC datetime: fractional
|
||||
// part only when microseconds are nonzero, offset always "+00:00" shape.
|
||||
func isoformat(t time.Time) string {
|
||||
base := t.Format("2006-01-02T15:04:05")
|
||||
if us := t.Nanosecond() / 1000; us != 0 {
|
||||
base += fmt.Sprintf(".%06d", us)
|
||||
}
|
||||
return base + t.Format("-07:00")
|
||||
}
|
||||
|
||||
// ── Python value semantics helpers ───────────────────────────────────────
|
||||
|
||||
// pyTruthy — Python truthiness for JSON-decoded values: None, False, 0,
|
||||
// "" and every empty container are falsy.
|
||||
func pyTruthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
case string:
|
||||
return t != ""
|
||||
case float64:
|
||||
return t != 0
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f != 0
|
||||
case []any:
|
||||
return len(t) > 0
|
||||
case []*OrderedMap:
|
||||
return len(t) > 0
|
||||
case *OrderedMap:
|
||||
return t != nil && t.Len() > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// pyTypeName — Python type() name for JSON-decoded values (integral
|
||||
// floats decode as 'int', mirroring json.loads producing ints).
|
||||
func pyTypeName(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "NoneType"
|
||||
case bool:
|
||||
return "bool"
|
||||
case string:
|
||||
return "str"
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return "int"
|
||||
}
|
||||
return "float"
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
if f == float64(int64(f)) && !strings.ContainsAny(t.String(), ".eE") {
|
||||
return "int"
|
||||
}
|
||||
return "float"
|
||||
case []any, []*OrderedMap:
|
||||
return "list"
|
||||
case *OrderedMap:
|
||||
return "dict"
|
||||
default:
|
||||
return "object"
|
||||
}
|
||||
}
|
||||
|
||||
// pyIsAlpha — str.isalpha(): non-empty and every rune a Unicode letter.
|
||||
func pyIsAlpha(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if !unicode.IsLetter(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// runeSlice — Python's s[:n] (codepoint slice).
|
||||
func runeSlice(s string, n int) string {
|
||||
if utf8.RuneCountInString(s) <= n {
|
||||
return s
|
||||
}
|
||||
return string([]rune(s)[:n])
|
||||
}
|
||||
|
||||
func truncateRunes(s string, n int) string { return runeSlice(s, n) }
|
||||
|
||||
// pyParse — json.loads with order-preserving objects and json.Number
|
||||
// numerals (so ints survive byte-parity re-encoding).
|
||||
func pyParse(s string) (any, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(s))
|
||||
dec.UseNumber()
|
||||
return decodeJSONValue(dec)
|
||||
}
|
||||
|
||||
// pythonRepr — repr() of a JSON-decoded value (the handle_hook
|
||||
// str(data) fallback), Python quoting rules included.
|
||||
func pythonRepr(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case string:
|
||||
return pythonReprString(t)
|
||||
case json.Number:
|
||||
if !strings.ContainsAny(t.String(), ".eE") {
|
||||
return t.String()
|
||||
}
|
||||
f, _ := t.Float64()
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'g', -1, 64)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = pythonRepr(item)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case *OrderedMap:
|
||||
entries := t.Entries()
|
||||
parts := make([]string, 0, len(entries))
|
||||
for _, kv := range entries {
|
||||
parts = append(parts, pythonReprString(kv.Key)+": "+pythonRepr(kv.Value))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// pythonReprString — repr() of a str: single quotes preferred, double
|
||||
// quotes when the string contains ' but not ", backslash and quote
|
||||
// escapes, \n \r \t shortcuts.
|
||||
func pythonReprString(s string) string {
|
||||
escape := func(quote byte) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte(quote)
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\'':
|
||||
if quote == '\'' {
|
||||
sb.WriteString(`\'`)
|
||||
} else {
|
||||
sb.WriteByte('\'')
|
||||
}
|
||||
case '"':
|
||||
if quote == '"' {
|
||||
sb.WriteString(`\"`)
|
||||
} else {
|
||||
sb.WriteByte('"')
|
||||
}
|
||||
default:
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
sb.WriteByte(quote)
|
||||
return sb.String()
|
||||
}
|
||||
if strings.Contains(s, "'") && !strings.Contains(s, "\"") {
|
||||
return escape('"')
|
||||
}
|
||||
return escape('\'')
|
||||
}
|
||||
|
||||
// ── the port (1:1 with the oracle) ───────────────────────────────────────
|
||||
|
||||
// runWarmRouter — run_warm_router: route the message through the warm
|
||||
// router subprocess and return the fired-neurons payload; ANY failure
|
||||
// (nonzero exit, timeout, bad JSON, empty stdout) degrades to None.
|
||||
func runWarmRouter(message string) any {
|
||||
res := Runner.Run(PythonBin(), []string{warmRouterPath(), "route", message, "--json"}, 10*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMemoryRecall — run_memory_recall: recall relevant entries; a
|
||||
// non-list payload degrades to [], a list is capped at budget entries.
|
||||
func runMemoryRecall(query string, budget int) []any {
|
||||
res := Runner.Run(PythonBin(), []string{memoryStorePath(), "recall", query, "--json"}, 15*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
if list, ok := v.([]any); ok {
|
||||
if budget >= 0 && budget < len(list) {
|
||||
return list[:budget]
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
}
|
||||
return []any{}
|
||||
}
|
||||
|
||||
// runAlwaysDigest — run_always_digest: the always-on digest payload or
|
||||
// None on any failure.
|
||||
func runAlwaysDigest() any {
|
||||
res := Runner.Run(PythonBin(), []string{warmRouterPath(), "always", "--json"}, 10*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadActiveTasks — load_active_tasks: read the persistence ledger and
|
||||
// keep open/in_progress tasks; missing file, malformed JSON, a non-dict
|
||||
// ledger, a non-dict task entry — every failure degrades to [].
|
||||
func loadActiveTasks() []any {
|
||||
raw, err := os.ReadFile(tasksFilePath())
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
v, err := pyParse(string(raw))
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
ledger, ok := v.(*OrderedMap)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
tasks, ok := ledger.Get("tasks")
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
list, ok := tasks.([]any)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
active := []any{}
|
||||
for _, t := range list {
|
||||
tom, ok := t.(*OrderedMap)
|
||||
if !ok {
|
||||
return []any{} // t.get on a non-dict raises → the oracle's except
|
||||
}
|
||||
status, _ := tom.Get2("status").(string)
|
||||
if status == "open" || status == "in_progress" {
|
||||
active = append(active, tom)
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
// recallQueryWords — the oracle's key-term extraction: lowercase, split on
|
||||
// whitespace, keep words longer than 4 chars that are pure alphabetic and
|
||||
// not stopwords.
|
||||
func recallQueryWords(message string) []string {
|
||||
words := []string{}
|
||||
for _, w := range strings.Fields(strings.ToLower(message)) {
|
||||
if utf8.RuneCountInString(w) > 4 && pyIsAlpha(w) {
|
||||
if _, stop := stopwordSet[w]; !stop {
|
||||
words = append(words, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// buildAutoContext — build_auto_context: warm neurons, active tasks,
|
||||
// memory recall (substantive messages only), always-on digest.
|
||||
//
|
||||
// Phase B (language advantage): the oracle's four IO calls are mutually
|
||||
// independent (pure reads of external state), so they fan out to
|
||||
// goroutines — wall time is the slowest call, not the sum of four
|
||||
// subprocesses. Results are assembled in the oracle's exact field order,
|
||||
// so output is byte-identical to the serial Python semantics (the battery
|
||||
// replays the frozen truth against THIS parallel path).
|
||||
func buildAutoContext(message string) *OrderedMap {
|
||||
context := NewOrderedMap(
|
||||
KV{"timestamp", isoformat(ClockNow.Now().UTC())},
|
||||
KV{"message_preview", runeSlice(message, 200)},
|
||||
KV{"warm_neurons", nil},
|
||||
KV{"memory_recall", nil},
|
||||
KV{"always_digest", nil},
|
||||
KV{"active_tasks", nil},
|
||||
)
|
||||
|
||||
// cheap, CPU-only: key-term extraction (no IO) stays on the caller
|
||||
// goroutine so the fan-out carries the exact query the oracle computes
|
||||
query := ""
|
||||
if message != "" && utf8.RuneCountInString(strings.TrimSpace(message)) > 10 {
|
||||
words := recallQueryWords(message)
|
||||
if len(words) > 0 {
|
||||
n := 5
|
||||
if len(words) < n {
|
||||
n = len(words)
|
||||
}
|
||||
query = strings.Join(words[:n], " ")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
warm any
|
||||
digest any
|
||||
tasks []any
|
||||
recall []any
|
||||
)
|
||||
launch := func(f func()) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
launch(func() { warm = runWarmRouter(message) })
|
||||
launch(func() { tasks = loadActiveTasks() })
|
||||
if query != "" {
|
||||
launch(func() { recall = runMemoryRecall(query, 5) })
|
||||
}
|
||||
launch(func() { digest = runAlwaysDigest() })
|
||||
wg.Wait()
|
||||
|
||||
if pyTruthy(warm) {
|
||||
context.Set("warm_neurons", warm)
|
||||
}
|
||||
if len(tasks) > 0 {
|
||||
context.Set("active_tasks", tasks)
|
||||
}
|
||||
if len(recall) > 0 {
|
||||
context.Set("memory_recall", recall)
|
||||
}
|
||||
if pyTruthy(digest) {
|
||||
context.Set("always_digest", digest)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
// saveContext — save_context: atomic write (tmp file + rename) of the
|
||||
// context JSON, indent=2, ensure_ascii.
|
||||
//
|
||||
// Phase B: the tmp file is per-writer (os.CreateTemp in the target dir),
|
||||
// not the oracle's fixed CONTEXT_FILE+".tmp" — the rename semantics are
|
||||
// identical, but concurrent hooks no longer clobber each other's tmp
|
||||
// (the oracle's shared-tmp path races ENOENT under concurrency).
|
||||
func saveContext(context *OrderedMap) error {
|
||||
dir := filepath.Dir(contextFilePath())
|
||||
if err := os.MkdirAll(dir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(dir, ".auto_context-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := tmp.Name()
|
||||
defer os.Remove(name) // no-op after a successful rename
|
||||
if _, err := tmp.Write([]byte(encodePythonJSON(context, 0))); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(name, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, contextFilePath())
|
||||
}
|
||||
|
||||
// handleHook — handle_hook: the on_message plugin hook. The truthiness
|
||||
// guard, the message/text/str(data) fallback chain and the two nested
|
||||
// count expressions reproduce the oracle exactly, including its
|
||||
// best-effort error dicts (truncated to 120 runes).
|
||||
func handleHook(event string, data *OrderedMap) *OrderedMap {
|
||||
errorDict := func(msg string) *OrderedMap {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "error"},
|
||||
KV{"event", event},
|
||||
KV{"error", truncateRunes(msg, 120)},
|
||||
)
|
||||
}
|
||||
if event == "on_message" && pyTruthy(data) {
|
||||
message := any("")
|
||||
for _, key := range []string{"message", "text"} {
|
||||
v, ok := data.Get(key)
|
||||
if !ok {
|
||||
v = ""
|
||||
}
|
||||
if pyTruthy(v) {
|
||||
message = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if !pyTruthy(message) {
|
||||
message = pythonRepr(data)
|
||||
}
|
||||
msgStr, isStr := message.(string)
|
||||
if !isStr {
|
||||
return errorDict(fmt.Sprintf("'%s' object has no attribute 'strip'", pyTypeName(message)))
|
||||
}
|
||||
if msgStr != "" && utf8.RuneCountInString(strings.TrimSpace(msgStr)) > 2 {
|
||||
context := buildAutoContext(msgStr)
|
||||
if err := saveContext(context); err != nil {
|
||||
return errorDict(err.Error())
|
||||
}
|
||||
neurons := context.Get2("warm_neurons")
|
||||
recall := context.Get2("memory_recall")
|
||||
tasks := context.Get2("active_tasks")
|
||||
neuronsFired := 0
|
||||
if pyTruthy(neurons) {
|
||||
om, ok := neurons.(*OrderedMap)
|
||||
if !ok {
|
||||
return errorDict(fmt.Sprintf("'%s' object has no attribute 'get'", pyTypeName(neurons)))
|
||||
}
|
||||
fired, _ := om.Get("fired")
|
||||
// .get("fired", []) — a MISSING key defaults to [], a
|
||||
// present-but-None key does not.
|
||||
if fired == nil {
|
||||
if _, present := om.Get("fired"); !present {
|
||||
fired = []any{}
|
||||
}
|
||||
}
|
||||
n, err := pyLen(fired)
|
||||
if err != nil {
|
||||
return errorDict(err.Error())
|
||||
}
|
||||
neuronsFired = n
|
||||
}
|
||||
memoriesRecalled := 0
|
||||
if pyTruthy(recall) {
|
||||
if n, err := pyLen(recall); err != nil {
|
||||
return errorDict(err.Error())
|
||||
} else {
|
||||
memoriesRecalled = n
|
||||
}
|
||||
}
|
||||
activeTasks := 0
|
||||
if pyTruthy(tasks) {
|
||||
if n, err := pyLen(tasks); err != nil {
|
||||
return errorDict(err.Error())
|
||||
} else {
|
||||
activeTasks = n
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_context"},
|
||||
KV{"neurons_fired", neuronsFired},
|
||||
KV{"memories_recalled", memoriesRecalled},
|
||||
KV{"active_tasks", activeTasks},
|
||||
KV{"context_file", contextFilePath()},
|
||||
)
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_context"},
|
||||
)
|
||||
}
|
||||
|
||||
// pyLen — len() for JSON-decoded values; non-sized types raise TypeError
|
||||
// in Python ("object of type 'int' has no len()").
|
||||
func pyLen(v any) (int, error) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return 0, fmt.Errorf("object of type 'NoneType' has no len()")
|
||||
case string:
|
||||
return utf8.RuneCountInString(t), nil
|
||||
case []any:
|
||||
return len(t), nil
|
||||
case []*OrderedMap:
|
||||
return len(t), nil
|
||||
case *OrderedMap:
|
||||
return t.Len(), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("object of type '%s' has no len()", pyTypeName(v))
|
||||
}
|
||||
}
|
||||
|
||||
// pluginManifest — the module-level PLUGIN dict (microkernel discovery
|
||||
// metadata), key order preserved.
|
||||
func pluginManifest() *OrderedMap {
|
||||
return NewOrderedMap(
|
||||
KV{"name", "auto_context"},
|
||||
KV{"version", "1.0.0"},
|
||||
KV{"description", "Auto-context — automatic memory and warm neuron activation on every message."},
|
||||
KV{"capabilities", []any{"context", "memory", "activation"}},
|
||||
KV{"hooks", NewOrderedMap(KV{"on_message", "handle_hook"})},
|
||||
KV{"dependencies", []any{"memory_store", "warm_router"}},
|
||||
KV{"provides", []any{"auto_context"}},
|
||||
KV{"priority", 5},
|
||||
KV{"auto_activate", true},
|
||||
)
|
||||
}
|
||||
|
||||
// ── CLI (auto_context.py main(), byte-parity stdout) ─────────────────────
|
||||
|
||||
func argparseUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage: auto_context.py [-h] [--event EVENT] [--data DATA] [--json] {build,show,hook} [arg]")
|
||||
}
|
||||
|
||||
// RunCLI executes auto_context.py main() against argv (program name at
|
||||
// index 0) and returns the process exit code. argparse failures print
|
||||
// usage to stderr and exit 2.
|
||||
func RunCLI(argv []string, stdout io.Writer) int {
|
||||
stderr := os.Stderr
|
||||
var positionals []string
|
||||
event, hasEvent := "", false
|
||||
dataStr, hasData := "", false
|
||||
jsonFlag := false
|
||||
i := 1
|
||||
for i < len(argv) {
|
||||
a := argv[i]
|
||||
switch {
|
||||
case a == "--json":
|
||||
jsonFlag = true
|
||||
case a == "--event":
|
||||
i++
|
||||
if i >= len(argv) {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument --event: expected one argument\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
event, hasEvent = argv[i], true
|
||||
case strings.HasPrefix(a, "--event="):
|
||||
event, hasEvent = strings.TrimPrefix(a, "--event="), true
|
||||
case a == "--data":
|
||||
i++
|
||||
if i >= len(argv) {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument --data: expected one argument\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
dataStr, hasData = argv[i], true
|
||||
case strings.HasPrefix(a, "--data="):
|
||||
dataStr, hasData = strings.TrimPrefix(a, "--data="), true
|
||||
case a == "-h" || a == "--help":
|
||||
argparseUsage(stdout)
|
||||
return 0
|
||||
case strings.HasPrefix(a, "-") && a != "-":
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: unrecognized arguments: %s\n", a)
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
default:
|
||||
positionals = append(positionals, a)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: the following arguments are required: cmd\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
if len(positionals) > 2 {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: unrecognized arguments: %s\n", strings.Join(positionals[2:], " "))
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
cmd, arg := positionals[0], ""
|
||||
if len(positionals) == 2 {
|
||||
arg = positionals[1]
|
||||
}
|
||||
switch cmd {
|
||||
case "build":
|
||||
message := arg
|
||||
if message == "" {
|
||||
message = "general context"
|
||||
}
|
||||
context := buildAutoContext(message)
|
||||
if err := saveContext(context); err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(context, 0))
|
||||
} else {
|
||||
n := 0
|
||||
if warm := context.Get2("warm_neurons"); pyTruthy(warm) {
|
||||
if om, ok := warm.(*OrderedMap); ok {
|
||||
fired, _ := om.Get("fired")
|
||||
if fired == nil {
|
||||
if _, present := om.Get("fired"); !present {
|
||||
fired = []any{}
|
||||
}
|
||||
}
|
||||
if c, err := pyLen(fired); err == nil {
|
||||
n = c
|
||||
}
|
||||
}
|
||||
}
|
||||
m := 0
|
||||
if recall, ok := context.Get2("memory_recall").([]any); ok {
|
||||
m = len(recall)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Auto-context built: %d neurons, %d memories\n", n, m)
|
||||
}
|
||||
|
||||
case "show":
|
||||
raw, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
fmt.Fprintln(stdout, "No auto-context file found")
|
||||
return 0
|
||||
}
|
||||
v, err := pyParse(string(raw))
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintln(stdout, encodePythonJSON(v, 0))
|
||||
|
||||
case "hook":
|
||||
event2 := "unknown"
|
||||
if hasEvent && event != "" {
|
||||
event2 = event
|
||||
}
|
||||
var data *OrderedMap
|
||||
if hasData && dataStr != "" {
|
||||
v, err := pyParse(dataStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if om, ok := v.(*OrderedMap); ok {
|
||||
data = om
|
||||
} else {
|
||||
// json.loads gave a non-object; data.get would raise
|
||||
// AttributeError outside handle_hook's try
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): '%s' object has no attribute 'get'\n", pyTypeName(v))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
result := handleHook(event2, data)
|
||||
fmt.Fprintln(stdout, encodePythonJSONCompact(result))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument cmd: invalid choice: '%s' (choose from 'build', 'show', 'hook')\n", cmd)
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ── entry point: CLI + truth replay subcommand for the HARD CI gate ─────
|
||||
|
||||
func truthRowCount(path string) int {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Fprintln(os.Stderr, f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d truth rows exact-equal vs the Python oracle\n", truthRowCount(path))
|
||||
return
|
||||
}
|
||||
os.Exit(RunCLI(os.Args, os.Stdout))
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
// Phase B battery — pins the language-advantage guarantees: the subprocess
|
||||
// and clock seams are interfaces (compile-time asserted in main.go),
|
||||
// build_auto_context fans the oracle's four independent IO calls out to
|
||||
// goroutines with byte-identical results, and concurrent builds are
|
||||
// race-clean (run under -race in CI).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// countingRunner — a CommandRunner decorator that counts invocations: the
|
||||
// seam swaps in without touching port code.
|
||||
type countingRunner struct {
|
||||
mu sync.Mutex
|
||||
inner CommandRunner
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *countingRunner) Run(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
return c.inner.Run(exe, args, timeout)
|
||||
}
|
||||
|
||||
// swapSeams — install the frozen seams for one test; restore on cleanup.
|
||||
func swapSeams(t *testing.T, routeMode, alwaysMode, recallMode string) string {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
memdir := filepath.Join(home, "memory")
|
||||
if err := os.MkdirAll(memdir, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
MemoryDir = func() string { return memdir }
|
||||
ScriptsDir = func() string { return "/tmp/actx_stubs" }
|
||||
PythonBin = func() string { return "python3" }
|
||||
Runner = buildFakeRunner(routeMode, alwaysMode, recallMode)
|
||||
base, _ := time.Parse(time.RFC3339, "2026-10-11T12:00:00.123456+00:00")
|
||||
ClockNow = ClockFunc(func() time.Time { return base })
|
||||
t.Cleanup(func() {
|
||||
MemoryDir = defaultMemoryDir
|
||||
ScriptsDir = defaultScriptsDir
|
||||
PythonBin = defaultPythonBin
|
||||
Runner = defaultRunner
|
||||
ClockNow = defaultClock
|
||||
})
|
||||
return memdir
|
||||
}
|
||||
|
||||
// TestPhaseBParallelBuildParity — the frozen build truth replays green
|
||||
// against the GOROUTINE fan-out path (results assembled in oracle order
|
||||
// are byte-identical to the serial Python semantics).
|
||||
func TestPhaseBParallelBuildParity(t *testing.T) {
|
||||
swapSeams(t, "ok", "ok", "ok")
|
||||
doc := loadTruth(t)
|
||||
for _, name := range []string{"build:full", "build:unicode-preview", "build:stopwords", "build:short-message"} {
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("parallel build diverged on %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBRunnerSeamIsAnInterface — a counting decorator rides the
|
||||
// CommandRunner seam; a full build issues exactly 3 subprocess runs (route,
|
||||
// recall, always — the ledger read is file IO, not a subprocess).
|
||||
func TestPhaseBRunnerSeamIsAnInterface(t *testing.T) {
|
||||
swapSeams(t, "ok", "ok", "ok")
|
||||
counter := &countingRunner{inner: Runner}
|
||||
Runner = counter
|
||||
ctx := buildAutoContext("remember the config parser override discussion tests")
|
||||
if counter.count != 3 {
|
||||
t.Fatalf("expected 3 subprocess runs per build, got %d", counter.count)
|
||||
}
|
||||
if warm := ctx.Get2("warm_neurons"); !pyTruthy(warm) {
|
||||
t.Fatal("warm payload lost through the decorated seam")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBConcurrentBuilds — 8 goroutines building contexts through the
|
||||
// shared fake runner must all agree (and stay race-clean).
|
||||
func TestPhaseBConcurrentBuilds(t *testing.T) {
|
||||
swapSeams(t, "ok", "ok", "ok")
|
||||
const n = 8
|
||||
results := make([]string, n)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
results[i] = mustJSON(buildAutoContext("remember the config parser override discussion tests"))
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 1; i < n; i++ {
|
||||
if results[i] != results[0] {
|
||||
t.Fatalf("concurrent build %d diverged:\n first: %s\n got: %s", i, results[0], results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBConcurrentHooks — concurrent handle_hook calls write the
|
||||
// context file through per-writer tmp + atomic rename: every rename
|
||||
// succeeds (no shared-tmp ENOENT race, which the oracle's fixed
|
||||
// CONTEXT_FILE+".tmp" would hit) and every reader sees complete JSON.
|
||||
func TestPhaseBConcurrentHooks(t *testing.T) {
|
||||
memdir := swapSeams(t, "ok", "ok", "ok")
|
||||
data := NewOrderedMap(KV{"message", "remember the config parser override discussion tests"})
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if r := handleHook("on_message", data); r.Get2("status") != "ok" {
|
||||
t.Errorf("hook failed: %v", mustJSON(r))
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
raw, err := os.ReadFile(filepath.Join(memdir, "index", "auto_context.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pyParse(string(raw)); err != nil {
|
||||
t.Fatalf("context file torn or invalid: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent=2 and the default compact (", ", ": ") separators,
|
||||
// ensure_ascii=True) so save_context, show and hook CLI output are
|
||||
// byte-parity with the oracle (adapted from the message_queue port's
|
||||
// pyjson.go; the float renderer follows Python repr, which keeps ".0" on
|
||||
// integral floats).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// KV — one ordered key/value pair.
|
||||
type KV struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
// OrderedMap — a dict with preserved insertion order.
|
||||
type OrderedMap struct {
|
||||
Keys []string
|
||||
Vals map[string]any
|
||||
}
|
||||
|
||||
// NewOrderedMap builds an ordered map from pairs.
|
||||
func NewOrderedMap(pairs ...KV) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for _, kv := range pairs {
|
||||
om.Set(kv.Key, kv.Value)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// OrderedMapFromMap wraps a plain map (order unspecified — canonical
|
||||
// comparisons sort keys anyway).
|
||||
func OrderedMapFromMap(m map[string]any) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// Set inserts or replaces, preserving first-insertion order.
|
||||
func (o *OrderedMap) Set(key string, value any) {
|
||||
if _, ok := o.Vals[key]; !ok {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.Vals[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a value.
|
||||
func (o *OrderedMap) Get(key string) (any, bool) {
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Get2 returns the value at key (nil when absent) for direct type asserts.
|
||||
func (o *OrderedMap) Get2(key string) any {
|
||||
return o.Vals[key]
|
||||
}
|
||||
|
||||
// Entries returns key/value pairs in insertion order.
|
||||
func (o *OrderedMap) Entries() []KV {
|
||||
out := make([]KV, 0, len(o.Keys))
|
||||
for _, k := range o.Keys {
|
||||
out = append(out, KV{k, o.Vals[k]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of entries.
|
||||
func (o *OrderedMap) Len() int { return len(o.Keys) }
|
||||
|
||||
// decodeJSONValue decodes one JSON value from the decoder, decoding objects
|
||||
// as *OrderedMap (order-preserving) and numbers as float64.
|
||||
func decodeJSONValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeFromToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeFromToken(dec *json.Decoder, tok json.Token) (any, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
om.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume '}'
|
||||
return nil, err
|
||||
}
|
||||
return om, nil
|
||||
case '[':
|
||||
arr := []any{}
|
||||
for dec.More() {
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume ']'
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
default:
|
||||
return tok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSON renders v exactly like Python json.dumps(v, indent=N)
|
||||
// with default ensure_ascii=True and (',', ': ') separators.
|
||||
func encodePythonJSON(v any, indent int) string {
|
||||
var sb strings.Builder
|
||||
encodeValue(&sb, v, indent)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeValue(sb *strings.Builder, v any, indent int) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
if t.Len() == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
writeIndent(sb, indent+1)
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, indent+1)
|
||||
if i < len(entries)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeValue(sb, OrderedMapFromMap(t), indent)
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func writeIndent(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSONCompact renders v exactly like Python json.dumps(v) with
|
||||
// default separators (", ", ": ") and ensure_ascii=True — the compact form
|
||||
// the CLI hook path prints.
|
||||
func encodePythonJSONCompact(v any) string {
|
||||
var sb strings.Builder
|
||||
encodeCompactValue(&sb, v)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeCompactValue(sb *strings.Builder, v any) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
sb.WriteString("{")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeCompactValue(sb, kv.Value)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeCompactValue(sb, OrderedMapFromMap(t))
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr (json.dumps uses
|
||||
// float.__repr__): shortest round-trip digits, and integral floats keep
|
||||
// one decimal place (1.0 stays "1.0", not "1").
|
||||
func pythonFloat(f float64) string {
|
||||
s := strconv.FormatFloat(f, 'g', -1, 64)
|
||||
if !strings.ContainsAny(s, ".eE") &&
|
||||
!strings.Contains(s, "Inf") && !strings.Contains(s, "NaN") {
|
||||
s += ".0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// pythonEscapeString — Python json.dumps string escaping with
|
||||
// ensure_ascii=True: control chars via \uXXXX (with \b \f \n \r \t
|
||||
// shortcuts), non-ASCII via \uXXXX / surrogate pairs, lowercase hex.
|
||||
func pythonEscapeString(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
sb.WriteString(`\"`)
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\b':
|
||||
sb.WriteString(`\b`)
|
||||
case '\f':
|
||||
sb.WriteString(`\f`)
|
||||
default:
|
||||
if r < 0x20 || r > 0x7e {
|
||||
if r > 0xffff {
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
fmt.Fprintf(&sb, `\u%04x\u%04x`, r1, r2)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, `\u%04x`, r)
|
||||
}
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// /tmp/gen_auto_context_truth.py against the live Python oracle with the
|
||||
// clock frozen and env-driven stub scripts pinned) and re-executes every
|
||||
// row through this Go port, comparing results. Exact rows (context-file
|
||||
// bytes, CLI stdout) compare byte-for-byte; everything else compares
|
||||
// canonically (sorted-key JSON). Absolute memory-dir paths are normalized
|
||||
// to @MEMDIR@ on both sides.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
Oracle string `json:"oracle"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on 100%).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(&doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
// compareOne replays one row and returns "" on parity, else a divergence
|
||||
// description. Shared by RunTruth and the per-function battery tests.
|
||||
func compareOne(doc *TruthDoc, row *TruthRow) string {
|
||||
got, exact, err := execRow(doc, row)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: %v", err)
|
||||
}
|
||||
var wantStr, gotStr string
|
||||
if exact {
|
||||
var w string
|
||||
if err := json.Unmarshal(row.Want, &w); err != nil {
|
||||
return fmt.Sprintf("want not a string: %v", err)
|
||||
}
|
||||
wantStr, gotStr = w, got.(string)
|
||||
} else {
|
||||
wantStr, err = canonicalFromRaw(row.Want)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize want: %v", err)
|
||||
}
|
||||
gotStr, err = canonicalFromRaw([]byte(mustJSON(got)))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize got: %v", err)
|
||||
}
|
||||
}
|
||||
if wantStr != gotStr {
|
||||
return fmt.Sprintf("\n want: %s\n got: %s", wantStr, gotStr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// execRow executes one truth row: returns got (any) and, for exact rows,
|
||||
// the exact string in got.
|
||||
func execRow(doc *TruthDoc, row *TruthRow) (got any, exact bool, err error) {
|
||||
setup := NewOrderedMap()
|
||||
if len(row.Setup) > 0 {
|
||||
dec := json.NewDecoder(bytes.NewReader(row.Setup))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return nil, false, fmt.Errorf("setup not an object")
|
||||
}
|
||||
v, err := decodeFromToken(dec, tok)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
setup = v.(*OrderedMap)
|
||||
}
|
||||
|
||||
// freeze the seams per row: fresh temp memory dir, frozen clock,
|
||||
// env-driven fake runner mirroring /tmp/actx_stubs payloads
|
||||
home, err := os.MkdirTemp("", "actx_truth_row_")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer os.RemoveAll(home)
|
||||
|
||||
defer func() {
|
||||
MemoryDir = defaultMemoryDir
|
||||
Runner = defaultRunner
|
||||
ClockNow = defaultClock
|
||||
PythonBin = defaultPythonBin
|
||||
ScriptsDir = defaultScriptsDir
|
||||
}()
|
||||
memdir := filepath.Join(home, "memory")
|
||||
if err := os.MkdirAll(memdir, 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
MemoryDir = func() string { return memdir }
|
||||
ScriptsDir = func() string { return "/tmp/actx_stubs" }
|
||||
PythonBin = func() string { return "python3" }
|
||||
base, err := time.Parse(time.RFC3339, doc.FrozenTS)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
ClockNow = ClockFunc(func() time.Time { return base })
|
||||
|
||||
// file seeds from setup
|
||||
if f, ok := setup.Get("tasks_file"); ok {
|
||||
s, _ := f.(string)
|
||||
if err := os.MkdirAll(filepath.Join(memdir, "tasks"), 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memdir, "tasks", "persisted.json"), []byte(s), 0o644); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
if f, ok := setup.Get("seed_context"); ok {
|
||||
s, _ := f.(string)
|
||||
if err := os.MkdirAll(filepath.Join(memdir, "index"), 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memdir, "index", "auto_context.json"), []byte(s), 0o644); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// stub modes (default "ok", like the stub scripts' env defaults)
|
||||
mode := func(key string) string {
|
||||
if v, ok := setup.Get(key); ok {
|
||||
if s, ok2 := v.(string); ok2 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
Runner = buildFakeRunner(mode("stub_route"), mode("stub_always"), mode("stub_recall"))
|
||||
|
||||
scrub := func(v any) any {
|
||||
return scrubPaths(v, memdir)
|
||||
}
|
||||
|
||||
strSetup := func(key string) string {
|
||||
s, _ := setup.Get2(key).(string)
|
||||
return s
|
||||
}
|
||||
|
||||
switch row.Op {
|
||||
case "warm_router":
|
||||
return scrub(runWarmRouter(strSetup("message"))), false, nil
|
||||
|
||||
case "recall":
|
||||
budget := 5
|
||||
if b, ok := setup.Get("budget"); ok {
|
||||
switch n := b.(type) {
|
||||
case float64:
|
||||
budget = int(n)
|
||||
case json.Number:
|
||||
f, _ := n.Float64()
|
||||
budget = int(f)
|
||||
}
|
||||
}
|
||||
return scrub(runMemoryRecall(strSetup("query"), budget)), false, nil
|
||||
|
||||
case "always":
|
||||
return scrub(runAlwaysDigest()), false, nil
|
||||
|
||||
case "load_tasks":
|
||||
return scrub(loadActiveTasks()), false, nil
|
||||
|
||||
case "build":
|
||||
return scrub(buildAutoContext(strSetup("message"))), false, nil
|
||||
|
||||
case "save":
|
||||
om, ok := setup.Get2("context").(*OrderedMap)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("save: no context")
|
||||
}
|
||||
if err := saveContext(om); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reads := []any{scrubPaths(string(raw), memdir)}
|
||||
if om2, ok2 := setup.Get2("context2").(*OrderedMap); ok2 {
|
||||
// overwrite semantics: second save replaces the first atomically
|
||||
if err := saveContext(om2); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw2, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reads = append(reads, scrubPaths(string(raw2), memdir))
|
||||
}
|
||||
_, statErr := os.Stat(contextFilePath() + ".tmp")
|
||||
return NewOrderedMap(
|
||||
KV{"reads", reads},
|
||||
KV{"tmp_gone", os.IsNotExist(statErr)},
|
||||
), false, nil
|
||||
|
||||
case "hook":
|
||||
event := strSetup("event")
|
||||
var data *OrderedMap
|
||||
if v, ok := setup.Get("data"); ok && v != nil {
|
||||
om, ok2 := v.(*OrderedMap)
|
||||
if !ok2 {
|
||||
return nil, false, fmt.Errorf("hook: data not an object")
|
||||
}
|
||||
data = om
|
||||
}
|
||||
return scrub(handleHook(event, data)), false, nil
|
||||
|
||||
case "cli":
|
||||
argvAny, _ := setup.Get2("argv").([]any)
|
||||
argv := []string{}
|
||||
for _, a := range argvAny {
|
||||
s, _ := a.(string)
|
||||
argv = append(argv, s)
|
||||
}
|
||||
var sb strings.Builder
|
||||
code := RunCLI(argv, &sb)
|
||||
out := strings.ReplaceAll(sb.String(), memdir, "@MEMDIR@")
|
||||
return NewOrderedMap(KV{"stdout", out}, KV{"exit", code}), false, nil
|
||||
|
||||
case "plugin":
|
||||
return pluginManifest(), false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("unknown row op %s (%s)", row.Op, row.Name)
|
||||
}
|
||||
|
||||
// buildFakeRunner — the env-driven stub scripts' behavior as a
|
||||
// CommandRunner (mirrors /tmp/actx_stubs byte-for-byte): payloads ECHO the
|
||||
// caller's message/query so the frozen truth pins the exact strings the
|
||||
// port hands to the subprocess seam. Reused by the Phase B battery.
|
||||
func buildFakeRunner(routeMode, alwaysMode, recallMode string) RunnerFunc {
|
||||
return func(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
var out string
|
||||
code := 0
|
||||
base := filepath.Base(args[0])
|
||||
if base == "warm_router.py" {
|
||||
if len(args) > 1 && args[1] == "always" {
|
||||
switch alwaysMode {
|
||||
case "ok":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"digest", "daily-summary"}, KV{"items", 7},
|
||||
KV{"topics", []any{"memory", "tasks", "recall"}}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "not-json{{{"
|
||||
case "empty":
|
||||
// print nothing
|
||||
}
|
||||
} else {
|
||||
msg := ""
|
||||
if len(args) > 2 {
|
||||
msg = args[2]
|
||||
}
|
||||
switch routeMode {
|
||||
case "ok":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"echo_message", msg},
|
||||
KV{"fired", []any{"warm-alpha", "warm-beta", "warm-gamma"}},
|
||||
KV{"matched", 3}, KV{"route", "focus"}))
|
||||
case "list":
|
||||
out = "[1, 2]"
|
||||
case "emptylist":
|
||||
out = "[]"
|
||||
case "firedint":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"fired", 3}, KV{"echo_message", msg}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "not-json{{{"
|
||||
case "empty":
|
||||
// print nothing
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := ""
|
||||
if len(args) > 2 {
|
||||
query = args[2]
|
||||
}
|
||||
switch recallMode {
|
||||
case "ok":
|
||||
items := []any{NewOrderedMap(KV{"echo_query", query})}
|
||||
for _, e := range recallEntries {
|
||||
items = append(items, e)
|
||||
}
|
||||
out = encodePythonJSONCompact(items)
|
||||
case "nonlist":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"query", query}, KV{"note", "not a list"}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "<xml?>"
|
||||
case "emptyout":
|
||||
// print nothing
|
||||
}
|
||||
}
|
||||
return ExecResult{Stdout: out, ReturnCode: code}
|
||||
}
|
||||
}
|
||||
|
||||
// scrubPaths — normalize the row's memory-dir prefix to @MEMDIR@ so the
|
||||
// frozen want (recorded with the generator's temp dirs) compares equal.
|
||||
func scrubPaths(v any, memdir string) any {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.ReplaceAll(t, memdir, "@MEMDIR@")
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = scrubPaths(item, memdir)
|
||||
}
|
||||
return out
|
||||
case *OrderedMap:
|
||||
out := NewOrderedMap()
|
||||
for _, kv := range t.Entries() {
|
||||
out.Set(kv.Key, scrubPaths(kv.Value, memdir))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// recallEntries — the memory_store.py stub's fixed entry list (m2's title
|
||||
// pins ensure_ascii round-tripping).
|
||||
var recallEntries = []any{
|
||||
NewOrderedMap(KV{"id", "m1"}, KV{"score", 0.91},
|
||||
KV{"tags", []any{"alpha", "beta"}}, KV{"title", "memory-entry-one"}),
|
||||
NewOrderedMap(KV{"id", "m2"}, KV{"score", 0.84},
|
||||
KV{"tags", []any{}}, KV{"title", "mémoire—café"}),
|
||||
NewOrderedMap(KV{"id", "m3"}, KV{"score", 0.77},
|
||||
KV{"tags", []any{"gamma"}}, KV{"title", "memory-entry-three"}),
|
||||
NewOrderedMap(KV{"id", "m4"}, KV{"score", 0.7},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-four"}),
|
||||
NewOrderedMap(KV{"id", "m5"}, KV{"score", 0.62},
|
||||
KV{"tags", []any{"delta"}}, KV{"title", "memory-entry-five"}),
|
||||
NewOrderedMap(KV{"id", "m6"}, KV{"score", 0.55},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-six"}),
|
||||
NewOrderedMap(KV{"id", "m7"}, KV{"score", 0.41},
|
||||
KV{"tags", []any{"epsilon"}}, KV{"title", "memory-entry-seven"}),
|
||||
NewOrderedMap(KV{"id", "m8"}, KV{"score", 0.2},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-eight"}),
|
||||
}
|
||||
|
||||
// canonicalFromRaw — canonical string of a raw JSON document.
|
||||
func canonicalFromRaw(raw []byte) (string, error) {
|
||||
var v any
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonical(normalize(v)), nil
|
||||
}
|
||||
|
||||
// canonical — sorted-key deterministic rendering of a decoded JSON value.
|
||||
func canonical(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case string:
|
||||
return pythonEscapeString(t)
|
||||
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 {
|
||||
parts = append(parts, pythonEscapeString(k)+": "+canonical(normalize(t[k])))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = kv.Value
|
||||
}
|
||||
return canonical(m)
|
||||
default:
|
||||
return fmt.Sprintf("«%v»", t)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize — convert OrderedMap/map trees into plain map[string]any with
|
||||
// json.Number→float64 so canonical() sees one representation.
|
||||
func normalize(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = normalize(kv.Value)
|
||||
}
|
||||
return m
|
||||
case map[string]any:
|
||||
m := map[string]any{}
|
||||
for k, val := range t {
|
||||
m[k] = normalize(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = normalize(item)
|
||||
}
|
||||
return out
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(normalize(v))
|
||||
if err != nil {
|
||||
return "null"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,264 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate plus per-function batteries replaying the
|
||||
// frozen oracle truth (truth.json, generated against the LIVE Python
|
||||
// oracle with the clock frozen and env-driven stub scripts pinned)
|
||||
// VERBATIM. Any divergence fails; the truth file rides the repo so the
|
||||
// gate holds in CI without /tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) *TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth.json: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth.json contained no rows")
|
||||
}
|
||||
return &doc
|
||||
}
|
||||
|
||||
func findRow(t *testing.T, name string) TruthRow {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
for _, row := range doc.Rows {
|
||||
if row.Name == name {
|
||||
return row
|
||||
}
|
||||
}
|
||||
t.Fatalf("frozen row %q missing from truth.json — battery/truth drift", name)
|
||||
return TruthRow{}
|
||||
}
|
||||
|
||||
func mustReplay(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func replayOp(t *testing.T, op string) int {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
n := 0
|
||||
for _, row := range doc.Rows {
|
||||
if row.Op != op {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("no %s rows in truth.json", op)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// rowWant decodes a row's want JSON into a generic map.
|
||||
func rowWant(t *testing.T, name string) map[string]any {
|
||||
t.Helper()
|
||||
row := findRow(t, name)
|
||||
var want map[string]any
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
t.Fatalf("decode want of %s: %v", name, err)
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// TestTruthParity — the whole frozen battery, one shot.
|
||||
func TestTruthParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
t.Fatalf("parity replay diverged after %d clean rows:\n%s",
|
||||
len(doc.Rows)-len(fails), strings.Join(fails, "\n"))
|
||||
}
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", len(doc.Rows))
|
||||
}
|
||||
|
||||
// TestBatteryRunWarmRouter — ok / nonzero-exit / bad-json / empty-stdout /
|
||||
// JSON-list payload / empty-list payload; the ok row pins the exact
|
||||
// message the port hands to the seam.
|
||||
func TestBatteryRunWarmRouter(t *testing.T) {
|
||||
if n := replayOp(t, "warm_router"); n != 6 {
|
||||
t.Fatalf("expected 6 frozen warm_router rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "warm:ok")
|
||||
if want["echo_message"] != "hello warm router" || want["matched"] != float64(3) {
|
||||
t.Fatalf("frozen echo row changed: %v", want)
|
||||
}
|
||||
if got := len(want["fired"].([]any)); got != 3 {
|
||||
t.Fatalf("fired payload changed: %v", want["fired"])
|
||||
}
|
||||
mustReplay(t, "warm:list")
|
||||
mustReplay(t, "warm:emptylist")
|
||||
}
|
||||
|
||||
// TestBatteryRunMemoryRecall — ok (echo pins the query string), budget
|
||||
// truncation, non-list payload, and every degrade branch.
|
||||
func TestBatteryRunMemoryRecall(t *testing.T) {
|
||||
if n := replayOp(t, "recall"); n != 6 {
|
||||
t.Fatalf("expected 6 frozen recall rows, got %d", n)
|
||||
}
|
||||
// recall:ok's want is the returned LIST — element 0 echoes the query
|
||||
rawWant := findRow(t, "recall:ok").Want
|
||||
var list []map[string]any
|
||||
if err := json.Unmarshal(rawWant, &list); err != nil {
|
||||
t.Fatalf("decode recall:ok want: %v", err)
|
||||
}
|
||||
if len(list) != 5 || list[0]["echo_query"] != "alpha beta gamma" {
|
||||
t.Fatalf("frozen recall:ok changed: %v", list)
|
||||
}
|
||||
mustReplay(t, "recall:budget2")
|
||||
mustReplay(t, "recall:nonlist")
|
||||
mustReplay(t, "recall:emptyout")
|
||||
}
|
||||
|
||||
// TestBatteryRunAlwaysDigest — ok / fail / bad-json / empty.
|
||||
func TestBatteryRunAlwaysDigest(t *testing.T) {
|
||||
if n := replayOp(t, "always"); n != 4 {
|
||||
t.Fatalf("expected 4 frozen always rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "always:ok")
|
||||
if want["digest"] != "daily-summary" || want["items"] != float64(7) {
|
||||
t.Fatalf("frozen digest changed: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryLoadActiveTasks — missing file / mixed statuses (only
|
||||
// open+in_progress survive) / malformed / empty / no-tasks-key.
|
||||
func TestBatteryLoadActiveTasks(t *testing.T) {
|
||||
if n := replayOp(t, "load_tasks"); n != 5 {
|
||||
t.Fatalf("expected 5 frozen load_tasks rows, got %d", n)
|
||||
}
|
||||
mustReplay(t, "tasks:missing")
|
||||
mustReplay(t, "tasks:malformed")
|
||||
}
|
||||
|
||||
// TestBatteryBuild — every build_auto_context branch: full context (the
|
||||
// recall echo pins stopword filtering + isalpha + first-5 join), short
|
||||
// message, stopwords-only, non-alpha, unicode 200-rune preview truncation,
|
||||
// empty message, recall-fail, warm-emptylist, warm-fail; frozen timestamp.
|
||||
func TestBatteryBuild(t *testing.T) {
|
||||
if n := replayOp(t, "build"); n != 9 {
|
||||
t.Fatalf("expected 9 frozen build rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "build:full")
|
||||
recallList, _ := want["memory_recall"].([]any)
|
||||
echo, _ := recallList[0].(map[string]any)
|
||||
if echo["echo_query"] != "remember config parser override discussion" {
|
||||
t.Fatalf("recall query extraction changed: %v", echo["echo_query"])
|
||||
}
|
||||
if want["timestamp"] != "2026-10-11T12:00:00.123456+00:00" {
|
||||
t.Fatalf("frozen timestamp changed: %v", want["timestamp"])
|
||||
}
|
||||
want = rowWant(t, "build:unicode-preview")
|
||||
preview, _ := want["message_preview"].(string)
|
||||
if n := len([]rune(preview)); n != 200 {
|
||||
t.Fatalf("preview must be 200 runes, got %d", n)
|
||||
}
|
||||
mustReplay(t, "build:short-message")
|
||||
mustReplay(t, "build:stopwords")
|
||||
mustReplay(t, "build:nonalpha")
|
||||
mustReplay(t, "build:empty")
|
||||
mustReplay(t, "build:recall-fail")
|
||||
mustReplay(t, "build:warm-emptylist")
|
||||
mustReplay(t, "build:warm-fail")
|
||||
}
|
||||
|
||||
// TestBatterySave — save_context byte-parity: ensure_ascii escapes,
|
||||
// indent=2 layout, and the tmp+rename overwrite (no .tmp leftover).
|
||||
func TestBatterySave(t *testing.T) {
|
||||
if n := replayOp(t, "save"); n != 2 {
|
||||
t.Fatalf("expected 2 frozen save rows, got %d", n)
|
||||
}
|
||||
mustReplay(t, "save:full")
|
||||
mustReplay(t, "save:overwrite")
|
||||
}
|
||||
|
||||
// TestBatteryHandleHook — full counts, text fallback, str(data) fallback
|
||||
// (Python dict repr!), short message, other event, no data, zero counts,
|
||||
// and both best-effort error dicts verbatim.
|
||||
func TestBatteryHandleHook(t *testing.T) {
|
||||
if n := replayOp(t, "hook"); n != 9 {
|
||||
t.Fatalf("expected 9 frozen hook rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "hook:full")
|
||||
if want["neurons_fired"] != float64(3) || want["memories_recalled"] != float64(5) ||
|
||||
want["active_tasks"] != float64(2) {
|
||||
t.Fatalf("frozen hook counts changed: %v", want)
|
||||
}
|
||||
if cf, _ := want["context_file"].(string); !strings.HasSuffix(cf, "@MEMDIR@/index/auto_context.json") {
|
||||
t.Fatalf("context_file normalization changed: %v", cf)
|
||||
}
|
||||
errWant := rowWant(t, "hook:warm-list-error")
|
||||
if errWant["error"] != "'list' object has no attribute 'get'" {
|
||||
t.Fatalf("list.get error message changed: %v", errWant["error"])
|
||||
}
|
||||
errWant = rowWant(t, "hook:fired-int-error")
|
||||
if errWant["error"] != "object of type 'int' has no len()" {
|
||||
t.Fatalf("len(int) error message changed: %v", errWant["error"])
|
||||
}
|
||||
mustReplay(t, "hook:text-fallback")
|
||||
mustReplay(t, "hook:str-fallback")
|
||||
mustReplay(t, "hook:short-message")
|
||||
mustReplay(t, "hook:other-event")
|
||||
mustReplay(t, "hook:no-data")
|
||||
mustReplay(t, "hook:counts-zero")
|
||||
}
|
||||
|
||||
// TestBatteryCLI — main() byte-parity stdout + exit codes: build (plain +
|
||||
// --json + default message), show (hit + missing), hook (event+data /
|
||||
// defaults / error dict), argparse exits.
|
||||
func TestBatteryCLI(t *testing.T) {
|
||||
if n := replayOp(t, "cli"); n != 10 {
|
||||
t.Fatalf("expected 10 frozen cli rows, got %d", n)
|
||||
}
|
||||
for name, wantExit := range map[string]float64{
|
||||
"cli:build": 0,
|
||||
"cli:build-json": 0,
|
||||
"cli:build-default": 0,
|
||||
"cli:show-missing": 0,
|
||||
"cli:show-hit": 0,
|
||||
"cli:hook": 0,
|
||||
"cli:hook-default": 0,
|
||||
"cli:hook-error": 0,
|
||||
"cli:bad-cmd": 2,
|
||||
"cli:no-cmd": 2,
|
||||
} {
|
||||
want := rowWant(t, name)
|
||||
if want["exit"] != wantExit {
|
||||
t.Fatalf("%s: frozen exit code %v != %v", name, want["exit"], wantExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryPlugin — the PLUGIN manifest row.
|
||||
func TestBatteryPlugin(t *testing.T) {
|
||||
mustReplay(t, "plugin")
|
||||
want := rowWant(t, "plugin")
|
||||
if want["name"] != "auto_context" || want["priority"] != float64(5) ||
|
||||
want["auto_activate"] != true {
|
||||
t.Fatalf("frozen PLUGIN manifest changed: %v", want)
|
||||
}
|
||||
}
|
||||
+512
-77
@@ -1,110 +1,545 @@
|
||||
// Package main — evidence-probe: the GO port of
|
||||
// src/rune/evidence_probe.py's build_probe_prompt + budget constant
|
||||
// (Oracle-Battery Parity Pattern, #2479 GO batch 31). The probe runner
|
||||
// stays Python (reactive — it invokes the LLM and writes artifacts).
|
||||
// Package main — evidence_probe: the FULL GO port of
|
||||
// src/rune/evidence_probe.py (dichotomy wave, issue #2479 tranche).
|
||||
//
|
||||
// IMPROVEMENT (defect fixed): the oracle's build_probe_prompt doesn't
|
||||
// sanitize the question or artifact_path — a question containing the
|
||||
// artifact path string could confuse the LLM about which file to write.
|
||||
// The Go version embeds them but marks them with explicit delimiters.
|
||||
// The Python oracle's schema gate (validate_probe) and budget count
|
||||
// (count_probes) are themselves served by the Rust evidence_provenance
|
||||
// core through ep_loader; those semantics are ported 1:1 here (probe.rs
|
||||
// is the byte-parity reference, including Python repr rendering of the
|
||||
// status value and sorted-list error shapes).
|
||||
//
|
||||
// Orchestration (run_probe, the worker subprocess, exception semantics),
|
||||
// persistence bytes (probe.json indent=2 / probe.jsonl append) and the
|
||||
// bounded prompt are ported with IO behind injectable seams:
|
||||
//
|
||||
// Runner — the probe worker seam (default: DefaultRunner subprocess)
|
||||
// Clock — probe_id / decided_at source (default: wall clock)
|
||||
// ModelSource — the provider layer's model selection (default: MIMO_MODEL)
|
||||
// CmdRunner — the subprocess exec seam (default: os/exec)
|
||||
//
|
||||
// Parity: truth.json (frozen vs the LIVE oracle) replays verbatim through
|
||||
// replay.go; `go run . replay truth.json` and the battery in truth_test.go
|
||||
// gate every public function and branch.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── Constants (oracle surface) ──────────────────────────────────────────────
|
||||
|
||||
var ProbeStatuses = []string{"GROUNDED", "STILL_UNKNOWN", "CONTRADICTED"}
|
||||
var ProbeFields = []string{"question", "source", "answer", "implication", "status"}
|
||||
|
||||
// MetaFields — to_dict's bookkeeping keys; excluded from the extra-field gate.
|
||||
var MetaFields = []string{"probe_id", "decided_at", "model"}
|
||||
|
||||
// MaxProbesPerTask — no recursive probing: a task may resolve at most this
|
||||
// many unknowns via probes before escalation to deep research is mandatory.
|
||||
const MaxProbesPerTask = 2
|
||||
|
||||
// buildProbePrompt — Python-identical f-string.
|
||||
func buildProbePrompt(question, artifactPath string) string {
|
||||
return fmt.Sprintf(`TARGETED EVIDENCE PROBE (bounded — not full research)
|
||||
// ── Errors (Python exception semantics) ─────────────────────────────────────
|
||||
|
||||
You are resolving exactly ONE material unknown. Do NOT research the wider
|
||||
task, do NOT design anything, do NOT modify code.
|
||||
// ValueError mirrors the oracle's ValueError raises.
|
||||
type ValueError struct{ Msg string }
|
||||
|
||||
UNKNOWN:
|
||||
%s
|
||||
func (e *ValueError) Error() string { return e.Msg }
|
||||
|
||||
Find authoritative, CURRENT evidence that resolves this unknown (official
|
||||
docs, source code of the dependency, release notes, standards). One or two
|
||||
focused lookups are enough — this is a probe, not a survey.
|
||||
// ProbeBudgetExceeded — raised when probing would recurse past the budget.
|
||||
type ProbeBudgetExceeded struct{ Msg string }
|
||||
|
||||
OUTPUT: write JSON to
|
||||
%s (EXACT path — write to this exact absolute path, no other)
|
||||
with EXACTLY these keys:
|
||||
question: the unknown you were asked to resolve (copy verbatim)
|
||||
source: what authoritative evidence you consulted (URLs/paths)
|
||||
answer: what the evidence establishes
|
||||
implication: what this changes about the implementation
|
||||
status: "GROUNDED" (unknown resolved with cited evidence)
|
||||
| "STILL_UNKNOWN" (evidence not found/insufficient)
|
||||
| "CONTRADICTED" (evidence contradicts the task's assumption)
|
||||
func (e *ProbeBudgetExceeded) Error() string { return e.Msg }
|
||||
|
||||
MUST NOT: modify any file except the artifact path; continue into further
|
||||
research after the unknown is resolved; guess without a source.`, question, artifactPath)
|
||||
// ProviderUnresolvedError — the provider layer's model-selection gap
|
||||
// (require_model: no silent default permitted, T16 §16F).
|
||||
type ProviderUnresolvedError struct{ Msg string }
|
||||
|
||||
func (e *ProviderUnresolvedError) Error() string { return e.Msg }
|
||||
|
||||
// MissingArtifact — the oracle's _missing_artifact: the worker did not write
|
||||
// the declared artifact file (ADAPT-9 authority — no stdout rescue).
|
||||
func MissingArtifact() error {
|
||||
return &ValueError{
|
||||
Msg: "probe failed: worker did not write the declared artifact file " +
|
||||
"(ADAPT-9 authority — no stdout rescue for probes)",
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: evidence-probe replay <truth.json>")
|
||||
os.Exit(2)
|
||||
// ── ProbeResult ─────────────────────────────────────────────────────────────
|
||||
|
||||
// ProbeResult — the recorded probe (field order is the oracle's dataclass
|
||||
// order and to_dict key order).
|
||||
type ProbeResult struct {
|
||||
Question string
|
||||
Source string
|
||||
Answer string
|
||||
Implication string
|
||||
Status string
|
||||
ProbeID string
|
||||
DecidedAt string
|
||||
Model string
|
||||
}
|
||||
|
||||
// ToDict — oracle dict shape (insertion-ordered).
|
||||
func (r ProbeResult) ToDict() *PyObj {
|
||||
return &PyObj{
|
||||
Keys: []string{"question", "source", "answer", "implication", "status", "probe_id", "decided_at", "model"},
|
||||
Vals: map[string]PyValue{
|
||||
"question": r.Question,
|
||||
"source": r.Source,
|
||||
"answer": r.Answer,
|
||||
"implication": r.Implication,
|
||||
"status": r.Status,
|
||||
"probe_id": r.ProbeID,
|
||||
"decided_at": r.DecidedAt,
|
||||
"model": r.Model,
|
||||
},
|
||||
}
|
||||
raw, err := os.ReadFile(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// ── build_probe_prompt ──────────────────────────────────────────────────────
|
||||
|
||||
// BuildProbePrompt — bounded prompt: research THE UNKNOWN, write the 5-key
|
||||
// JSON file. Byte-identical to the oracle f-string.
|
||||
func BuildProbePrompt(question string, artifactPath string) string {
|
||||
return "TARGETED EVIDENCE PROBE (bounded — not full research)\n" +
|
||||
"\n" +
|
||||
"You are resolving exactly ONE material unknown. Do NOT research the wider\n" +
|
||||
"task, do NOT design anything, do NOT modify code.\n" +
|
||||
"\n" +
|
||||
"UNKNOWN:\n" +
|
||||
question + "\n" +
|
||||
"\n" +
|
||||
"Find authoritative, CURRENT evidence that resolves this unknown (official\n" +
|
||||
"docs, source code of the dependency, release notes, standards). One or two\n" +
|
||||
"focused lookups are enough — this is a probe, not a survey.\n" +
|
||||
"\n" +
|
||||
"OUTPUT: write JSON to\n" +
|
||||
" " + artifactPath + " (EXACT path — write to this exact absolute path, no other)\n" +
|
||||
"with EXACTLY these keys:\n" +
|
||||
" question: the unknown you were asked to resolve (copy verbatim)\n" +
|
||||
" source: what authoritative evidence you consulted (URLs/paths)\n" +
|
||||
" answer: what the evidence establishes\n" +
|
||||
" implication: what this changes about the implementation\n" +
|
||||
" status: \"GROUNDED\" (unknown resolved with cited evidence)\n" +
|
||||
" | \"STILL_UNKNOWN\" (evidence not found/insufficient)\n" +
|
||||
" | \"CONTRADICTED\" (evidence contradicts the task's assumption)\n" +
|
||||
"\n" +
|
||||
"MUST NOT: modify any file except the artifact path; continue into further\n" +
|
||||
"research after the unknown is resolved; guess without a source."
|
||||
}
|
||||
|
||||
// ── validate_probe (Rust core semantics, byte-parity) ───────────────────────
|
||||
|
||||
// ValidateProbe — the 5-key schema gate; empty result = pass. Errors append
|
||||
// in PROBE_FIELDS order, then the status-set check, then the GROUNDED source
|
||||
// check, then the sorted extra-field check — exactly like the Rust core.
|
||||
func ValidateProbe(artifact PyValue) []string {
|
||||
obj, ok := artifact.(*PyObj)
|
||||
if !ok {
|
||||
return []string{"probe artifact is not a JSON object"}
|
||||
}
|
||||
var cases []map[string]any
|
||||
if err := json.Unmarshal(raw, &cases); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
||||
var errs []string
|
||||
for _, f := range ProbeFields {
|
||||
v, present := obj.Get(f)
|
||||
switch {
|
||||
case !present:
|
||||
errs = append(errs, fmt.Sprintf("missing required field: %s", f))
|
||||
case IsPyStr(v):
|
||||
if f != "source" && PyIsBlank(v.(string)) {
|
||||
errs = append(errs, fmt.Sprintf("field %s must be a non-empty string", f))
|
||||
}
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("field %s must be a string", f))
|
||||
}
|
||||
}
|
||||
fails := 0
|
||||
for i, c := range cases {
|
||||
op, _ := c["op"].(string)
|
||||
switch op {
|
||||
case "prompt":
|
||||
q, _ := c["question"].(string)
|
||||
ap, _ := c["artifact_path"].(string)
|
||||
want, _ := c["want"].(string)
|
||||
if got := buildProbePrompt(q, ap); got != want {
|
||||
// find the first divergence for the report
|
||||
for j := range got {
|
||||
if j >= len(want) || got[j] != want[j] {
|
||||
fails++
|
||||
fmt.Printf("case %d prompt: first divergence at char %d\n got: %q\n want: %q\n", i, j, got[min(j, len(got)-1):min(j+40, len(got))], want[min(j, len(want)-1):min(j+40, len(want))])
|
||||
break
|
||||
}
|
||||
|
||||
// Status check runs on the RAW value regardless of type: a non-string
|
||||
// status fires the field error above AND the "must be one of" error here.
|
||||
if statusVal, present := obj.Get("status"); present {
|
||||
statusOK := false
|
||||
if s, isStr := statusVal.(string); isStr {
|
||||
for _, ok := range ProbeStatuses {
|
||||
if s == ok {
|
||||
statusOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
case "max_probes":
|
||||
want := int(c["want"].(float64))
|
||||
if MaxProbesPerTask != want {
|
||||
fails++
|
||||
fmt.Printf("case %d max_probes: got %d want %d\n", i, MaxProbesPerTask, want)
|
||||
}
|
||||
if !statusOK {
|
||||
sorted := append([]string(nil), ProbeStatuses...)
|
||||
sort.Strings(sorted)
|
||||
var rendered string
|
||||
switch v := statusVal.(type) {
|
||||
case string:
|
||||
rendered = PyRepr(v)
|
||||
case json.Number:
|
||||
rendered = PyFmtNum(v)
|
||||
case bool:
|
||||
if v {
|
||||
rendered = "True"
|
||||
} else {
|
||||
rendered = "False"
|
||||
}
|
||||
case nil:
|
||||
rendered = "None"
|
||||
default:
|
||||
rendered = PyDumpsCompact(v)
|
||||
}
|
||||
errs = append(errs, fmt.Sprintf(
|
||||
"status must be one of %s, got %s", PyListRepr(sorted), rendered))
|
||||
}
|
||||
}
|
||||
|
||||
// GROUNDED requires a concrete source citation (>= 8 chars).
|
||||
if s, present := obj.Get("status"); present {
|
||||
if str, isStr := s.(string); isStr && str == "GROUNDED" {
|
||||
sourceLen := 0
|
||||
if sv, present := obj.Get("source"); present {
|
||||
if ss, isStr := sv.(string); isStr {
|
||||
sourceLen = pyStrlen(ss)
|
||||
}
|
||||
}
|
||||
if sourceLen < 8 {
|
||||
errs = append(errs, "GROUNDED probe requires a concrete source citation")
|
||||
}
|
||||
}
|
||||
}
|
||||
// IMPROVEMENT pin: the delimiter markers (UNKNOWN:/OUTPUT:) make the
|
||||
// question and artifact path unambiguous to the LLM
|
||||
p := buildProbePrompt("test question", "/tmp/probe.json")
|
||||
if !strings.Contains(p, "test question") || !strings.Contains(p, "/tmp/probe.json") {
|
||||
fails++
|
||||
fmt.Println("IMPROVEMENT FAILED: question/path not embedded")
|
||||
|
||||
// Extra fields: artifact keys − PROBE_FIELDS − meta fields, sorted.
|
||||
var extra []string
|
||||
inList := func(list []string, k string) bool {
|
||||
for _, x := range list {
|
||||
if x == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if fails > 0 {
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", fails)
|
||||
os.Exit(1)
|
||||
for _, k := range obj.Keys {
|
||||
if !inList(ProbeFields, k) && !inList(MetaFields, k) {
|
||||
extra = append(extra, k)
|
||||
}
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases vs the Python oracle\n", len(cases))
|
||||
sort.Strings(extra)
|
||||
if len(extra) > 0 {
|
||||
errs = append(errs, fmt.Sprintf("unexpected extra fields: %s", PyListRepr(extra)))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
// IsPyStr reports whether the value is a JSON string.
|
||||
func IsPyStr(v PyValue) bool {
|
||||
_, ok := v.(string)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ── count_probes / check_budget ─────────────────────────────────────────────
|
||||
|
||||
// CountProbes — non-empty-line count of <dir>/probe.jsonl (0 if absent).
|
||||
// Line splitting is the Rust core's str::lines(): \n-split, trailing \r
|
||||
// stripped per line, no phantom line after a trailing newline.
|
||||
func CountProbes(dir string) int {
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "probe.jsonl"))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return b
|
||||
content := string(raw)
|
||||
if content == "" {
|
||||
return 0
|
||||
}
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
n := 0
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
if strings.TrimSpace(line) != "" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// CheckBudget — ProbeBudgetExceeded once the no-recursion budget is spent.
|
||||
func CheckBudget(artifactDir string) error {
|
||||
if CountProbes(artifactDir) >= MaxProbesPerTask {
|
||||
return &ProbeBudgetExceeded{
|
||||
Msg: fmt.Sprintf("probe budget exhausted (%d); unresolved unknowns must escalate to deep research", MaxProbesPerTask),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── record_probe ────────────────────────────────────────────────────────────
|
||||
|
||||
// RecordProbe — persist the probe: probe.json (indent=2 artifact) and
|
||||
// probe.jsonl (append-only compact line). The two writes touch different
|
||||
// files with no ordering contract between them, so they fan out to
|
||||
// goroutines (the Go-advantage pass); the persisted bytes are identical to
|
||||
// the serial oracle (the frozen battery replays this exact path).
|
||||
func RecordProbe(result ProbeResult, artifactDir string) (string, error) {
|
||||
if err := os.MkdirAll(artifactDir, 0o777); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
firstErr error
|
||||
jsonBytes = []byte(PyDumpsIndent2(result.ToDict()))
|
||||
jsonlBytes = []byte(PyDumpsCompact(result.ToDict()) + "\n")
|
||||
jsonPath = filepath.Join(artifactDir, "probe.json")
|
||||
jsonlPath = filepath.Join(artifactDir, "probe.jsonl")
|
||||
)
|
||||
fail := func(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
fail(os.WriteFile(jsonPath, jsonBytes, 0o666))
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f, err := os.OpenFile(jsonlPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o666)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
return
|
||||
}
|
||||
_, err = f.Write(jsonlBytes)
|
||||
fail(err)
|
||||
fail(f.Close())
|
||||
}()
|
||||
wg.Wait()
|
||||
if firstErr != nil {
|
||||
return "", firstErr
|
||||
}
|
||||
return artifactDir + "/probe.json", nil
|
||||
}
|
||||
|
||||
// ── run_probe ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Runner — the probe worker seam: receives the bounded prompt and the exact
|
||||
// absolute artifact path it must write (ADAPT-9: the file is the authority).
|
||||
type Runner interface {
|
||||
Run(prompt, artifactPath string) error
|
||||
}
|
||||
|
||||
// RunnerFunc adapts a function to Runner.
|
||||
type RunnerFunc func(prompt, artifactPath string) error
|
||||
|
||||
func (f RunnerFunc) Run(prompt, artifactPath string) error { return f(prompt, artifactPath) }
|
||||
|
||||
// Clock — the probe_id/decided_at source.
|
||||
type Clock interface{ Now() time.Time }
|
||||
|
||||
// ClockFunc adapts a function to Clock.
|
||||
type ClockFunc func() time.Time
|
||||
|
||||
func (f ClockFunc) Now() time.Time { return f() }
|
||||
|
||||
type wallClock struct{}
|
||||
|
||||
func (wallClock) Now() time.Time { return time.Now() }
|
||||
|
||||
// ClockNow — the active clock (overridable in tests/battery).
|
||||
var ClockNow Clock = wallClock{}
|
||||
|
||||
// RunProbe — one bounded probe. runner == nil selects the default worker
|
||||
// channel (DefaultRunner). model == "" falls back to $MIMO_MODEL, then the
|
||||
// literal "UNKNOWN" default only when the variable is unset.
|
||||
func RunProbe(question string, artifactDir string, runner Runner, model string) (ProbeResult, error) {
|
||||
if err := CheckBudget(artifactDir); err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
artifactPath, err := filepath.Abs(filepath.Join(artifactDir, "evidence_probe.json"))
|
||||
if err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
|
||||
if runner == nil {
|
||||
runner = RunnerFunc(DefaultRunner)
|
||||
}
|
||||
if err := runner.Run(BuildProbePrompt(question, artifactPath), artifactPath); err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(artifactPath); err != nil {
|
||||
return ProbeResult{}, MissingArtifact()
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(artifactPath)
|
||||
if err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
artifact, err := PyParse(raw)
|
||||
if err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
|
||||
if errs := ValidateProbe(artifact); len(errs) > 0 {
|
||||
return ProbeResult{}, &ValueError{Msg: "probe gate failed: " + strings.Join(errs, "; ")}
|
||||
}
|
||||
|
||||
obj := artifact.(*PyObj)
|
||||
getStr := func(key string) string {
|
||||
if v, ok := obj.Get(key); ok {
|
||||
if s, isStr := v.(string); isStr {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
resolvedModel := model
|
||||
if resolvedModel == "" {
|
||||
if env, ok := os.LookupEnv("MIMO_MODEL"); ok {
|
||||
resolvedModel = env
|
||||
} else {
|
||||
resolvedModel = "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
now := ClockNow.Now()
|
||||
result := ProbeResult{
|
||||
Question: getStr("question"),
|
||||
Source: getStr("source"),
|
||||
Answer: getStr("answer"),
|
||||
Implication: getStr("implication"),
|
||||
Status: getStr("status"),
|
||||
ProbeID: fmt.Sprintf("probe-%d", now.Unix()),
|
||||
DecidedAt: now.UTC().Format("2006-01-02T15:04:05Z"),
|
||||
Model: resolvedModel,
|
||||
}
|
||||
if _, err := RecordProbe(result, artifactDir); err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ── _default_runner (the worker subprocess channel) ─────────────────────────
|
||||
|
||||
// ModelSource — the provider layer owns model selection (require_model);
|
||||
// a missing value is a configuration gap, never papered over.
|
||||
type ModelSource interface{ RequireModel() (string, error) }
|
||||
|
||||
// ModelSourceFunc adapts a function to ModelSource.
|
||||
type ModelSourceFunc func() (string, error)
|
||||
|
||||
func (f ModelSourceFunc) RequireModel() (string, error) { return f() }
|
||||
|
||||
// EnvModelSource — the default require_model: $MIMO_MODEL (stripped), no
|
||||
// silent default (T16 §16F).
|
||||
var EnvModelSource ModelSource = ModelSourceFunc(func() (string, error) {
|
||||
const envVar = "MIMO_MODEL"
|
||||
model := strings.TrimSpace(os.Getenv(envVar))
|
||||
if model == "" {
|
||||
return "", &ProviderUnresolvedError{
|
||||
Msg: "PROVIDER_UNRESOLVED: " + envVar + " is not set — provider/model " +
|
||||
"selection belongs to the provider layer (set " + envVar + " in " +
|
||||
"the calling environment); no silent default is permitted " +
|
||||
"(T16 §16F)",
|
||||
}
|
||||
}
|
||||
return model, nil
|
||||
})
|
||||
|
||||
// ModelSelect — the active model source (overridable in tests/battery).
|
||||
var ModelSelect = EnvModelSource
|
||||
|
||||
// CmdRunner — the subprocess exec seam.
|
||||
type CmdRunner interface {
|
||||
// Run executes bin with args; returns captured stdout and a non-nil
|
||||
// error whose ExitCode() is the child's exit status (exec.ExitError).
|
||||
Run(bin string, args []string) ([]byte, error)
|
||||
}
|
||||
|
||||
type osExecRunner struct{}
|
||||
|
||||
func (osExecRunner) Run(bin string, args []string) ([]byte, error) {
|
||||
return exec.Command(bin, args...).Output()
|
||||
}
|
||||
|
||||
// ExecCmd — the active command runner (overridable in tests/battery).
|
||||
var ExecCmd CmdRunner = osExecRunner{}
|
||||
|
||||
// DefaultRunner — same worker channel as canonical stages: local MiMo,
|
||||
// JSON format. stdout is observability only; the artifact file is the
|
||||
// authority.
|
||||
func DefaultRunner(prompt string, artifactPath string) error {
|
||||
mimoBin := os.Getenv("MIMO_BIN")
|
||||
if mimoBin == "" {
|
||||
mimoBin = "/var/lib/gitea-desktop-runner/bin/mimo"
|
||||
}
|
||||
mimoModel, err := ModelSelect.RequireModel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mimoAgent := os.Getenv("MIMO_AGENT")
|
||||
if mimoAgent == "" {
|
||||
mimoAgent = "build"
|
||||
}
|
||||
cmd := []string{
|
||||
"run",
|
||||
"--agent",
|
||||
mimoAgent,
|
||||
"-m",
|
||||
mimoModel,
|
||||
"--dangerously-skip-permissions",
|
||||
"--format",
|
||||
"json",
|
||||
prompt,
|
||||
}
|
||||
out, runErr := ExecCmd.Run(mimoBin, cmd)
|
||||
// Python only reaches the log write when the child actually ran
|
||||
// (launch failures raise before it).
|
||||
var exitErr *exec.ExitError
|
||||
launched := runErr == nil || errors.As(runErr, &exitErr)
|
||||
if launched {
|
||||
logPath := filepath.Dir(artifactPath) + "/evidence_probe.stdout.log"
|
||||
if writeErr := os.WriteFile(logPath, out, 0o666); writeErr != nil && runErr == nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
if runErr != nil {
|
||||
if launched {
|
||||
return &ValueError{Msg: fmt.Sprintf("probe worker exited %d", exitErr.ExitCode())}
|
||||
}
|
||||
return runErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: evidence-probe replay <truth.json>")
|
||||
os.Exit(2)
|
||||
}
|
||||
if fails := RunTruth(os.Args[2]); len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Println("DIVERGED:", f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("PARITY OK: frozen oracle battery replayed verbatim")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// phaseb.go — the language-advantage pass (dichotomy wave):
|
||||
//
|
||||
// - RecordProbe fans its two independent file writes to goroutines
|
||||
// (main.go) — the serial oracle's only IO seam where parallelism is
|
||||
// honest; 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 probe run.
|
||||
package main
|
||||
|
||||
// Compile-time seam safety: the default implementations and the Func
|
||||
// adapters satisfy their interfaces, and the error types implement error.
|
||||
// A seam drifting from its contract fails the build, not a probe run.
|
||||
var (
|
||||
_ Runner = RunnerFunc(nil)
|
||||
_ Runner = RunnerFunc(DefaultRunner)
|
||||
_ Clock = ClockFunc(nil)
|
||||
_ Clock = wallClock{}
|
||||
_ ModelSource = ModelSourceFunc(nil)
|
||||
_ ModelSource = EnvModelSource
|
||||
_ CmdRunner = CmdRunnerFunc(nil)
|
||||
_ CmdRunner = osExecRunner{}
|
||||
|
||||
_ error = (*ValueError)(nil)
|
||||
_ error = (*ProbeBudgetExceeded)(nil)
|
||||
_ error = (*ProviderUnresolvedError)(nil)
|
||||
)
|
||||
|
||||
// CmdRunnerFunc adapts a function to CmdRunner.
|
||||
type CmdRunnerFunc func(bin string, args []string) ([]byte, error)
|
||||
|
||||
func (f CmdRunnerFunc) Run(bin string, args []string) ([]byte, error) {
|
||||
return f(bin, args)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
// Phase B battery — the language-advantage pass. The parallel RecordProbe
|
||||
// path must produce byte-identical persistence to the frozen serial oracle
|
||||
// (the truth battery already replays it; these tests hammer it directly),
|
||||
// and the seams must actually be consumed by RunProbe.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// exitErrOf — a real *exec.ExitError with the given exit code.
|
||||
func exitErrOf(code int) error {
|
||||
_, err := exec.Command("sh", "-c", fmt.Sprintf("exit %d", code)).Output()
|
||||
return err
|
||||
}
|
||||
|
||||
func TestRecordProbeParallelBytesIdentical(t *testing.T) {
|
||||
result := ProbeResult{
|
||||
Question: "q",
|
||||
Source: "https://example.com/a",
|
||||
Answer: "a",
|
||||
Implication: "i",
|
||||
Status: "GROUNDED",
|
||||
ProbeID: "probe-1791753600",
|
||||
DecidedAt: "2026-10-12T04:00:00Z",
|
||||
Model: "UNKNOWN",
|
||||
}
|
||||
dir := t.TempDir()
|
||||
path, err := RecordProbe(result, dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if path != filepath.Join(dir, "probe.json") {
|
||||
t.Fatalf("returned path drifted: %q", path)
|
||||
}
|
||||
pj, err := os.ReadFile(filepath.Join(dir, "probe.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pjl, err := os.ReadFile(filepath.Join(dir, "probe.jsonl"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The expected bytes come from the Python-faithful serializers, the
|
||||
// same renderers the frozen truth pins byte-for-byte.
|
||||
wantJSON := PyDumpsIndent2(result.ToDict())
|
||||
wantJSONL := PyDumpsCompact(result.ToDict()) + "\n"
|
||||
if string(pj) != wantJSON {
|
||||
t.Fatalf("probe.json bytes drifted:\n got %q\n want %q", pj, wantJSON)
|
||||
}
|
||||
if string(pjl) != wantJSONL {
|
||||
t.Fatalf("probe.jsonl bytes drifted:\n got %q\n want %q", pjl, wantJSONL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordProbeConcurrentHammer(t *testing.T) {
|
||||
const workers = 32
|
||||
result := ProbeResult{
|
||||
Question: "hammer", Source: "", Answer: "a", Implication: "i",
|
||||
Status: "STILL_UNKNOWN", ProbeID: "probe-1", DecidedAt: "d", Model: "m",
|
||||
}
|
||||
dirs := make([]string, workers)
|
||||
for i := range dirs {
|
||||
dirs[i] = t.TempDir()
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
_, errs[i] = RecordProbe(result, dirs[i])
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("worker %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
want := PyDumpsIndent2(result.ToDict())
|
||||
for i, dir := range dirs {
|
||||
pj, err := os.ReadFile(filepath.Join(dir, "probe.json"))
|
||||
if err != nil || string(pj) != want {
|
||||
t.Fatalf("worker %d bytes diverged: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProbeConsumesSeams(t *testing.T) {
|
||||
// Inject BOTH seams: a runner that writes a valid artifact and a clock
|
||||
// pinned to an off-truth instant — RunProbe must use the injected clock
|
||||
// (not the wall clock) for probe_id/decided_at.
|
||||
dir := t.TempDir()
|
||||
pinned := time.Unix(1234567890, 0)
|
||||
oldClock := ClockNow
|
||||
defer func() { ClockNow = oldClock }()
|
||||
ClockNow = ClockFunc(func() time.Time { return pinned })
|
||||
|
||||
runner := RunnerFunc(func(prompt, artifactPath string) error {
|
||||
if prompt == "" || artifactPath == "" {
|
||||
t.Fatal("seam received empty prompt/path")
|
||||
}
|
||||
return os.WriteFile(artifactPath, []byte(
|
||||
`{"question": "q", "source": "", "answer": "a", "implication": "i", "status": "STILL_UNKNOWN"}`), 0o666)
|
||||
})
|
||||
result, err := RunProbe("q", dir, runner, "seam-model")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.ProbeID != "probe-1234567890" {
|
||||
t.Fatalf("clock seam ignored: probe_id %q", result.ProbeID)
|
||||
}
|
||||
if result.DecidedAt != pinned.UTC().Format("2006-01-02T15:04:05Z") {
|
||||
t.Fatalf("clock seam ignored: decided_at %q", result.DecidedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRunnerConsumesExecSeam(t *testing.T) {
|
||||
oldExec, oldModel := ExecCmd, ModelSelect
|
||||
defer func() { ExecCmd, ModelSelect = oldExec, oldModel }()
|
||||
ExecCmd = CmdRunnerFunc(func(bin string, args []string) ([]byte, error) {
|
||||
if bin != "fake-mimo" || args[0] != "run" {
|
||||
t.Fatalf("exec seam got bin=%q args=%v", bin, args)
|
||||
}
|
||||
return []byte("ok"), nil
|
||||
})
|
||||
ModelSelect = ModelSourceFunc(func() (string, error) { return "seam-m", nil })
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Setenv("MIMO_BIN", "fake-mimo")
|
||||
t.Setenv("MIMO_AGENT", "seam-agent")
|
||||
if err := DefaultRunner("P", filepath.Join(dir, "evidence_probe.json")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
log, err := os.ReadFile(filepath.Join(dir, "evidence_probe.stdout.log"))
|
||||
if err != nil || string(log) != "ok" {
|
||||
t.Fatalf("stdout log drifted: %q %v", log, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExitCodeErrorMapping(t *testing.T) {
|
||||
oldExec, oldModel := ExecCmd, ModelSelect
|
||||
defer func() { ExecCmd, ModelSelect = oldExec, oldModel }()
|
||||
ExecCmd = CmdRunnerFunc(func(bin string, args []string) ([]byte, error) {
|
||||
return nil, exitErrOf(3)
|
||||
})
|
||||
ModelSelect = ModelSourceFunc(func() (string, error) { return "m", nil })
|
||||
|
||||
err := DefaultRunner("P", filepath.Join(t.TempDir(), "evidence_probe.json"))
|
||||
if err == nil || err.Error() != "probe worker exited 3" {
|
||||
t.Fatalf("exit-code mapping drifted: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// pyjson.go — Python-faithful JSON parsing and rendering for the
|
||||
// evidence_probe port. The oracle round-trips artifacts through
|
||||
// json.dumps/loads, so the port must reproduce: object key order
|
||||
// (insertion order, not sorted), float rendering (fmt_num semantics of
|
||||
// the Rust core: integral floats collapse to integers), and
|
||||
// ensure_ascii escaping with lowercase \uXXXX (surrogate pairs for
|
||||
// astral planes).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 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 (o *PyObj) Get(key string) (PyValue, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// PyParse — strict single-value JSON parse (trailing garbage rejected,
|
||||
// like Python json.loads / serde_json).
|
||||
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 := &PyObj{Vals: map[string]PyValue{}}
|
||||
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
|
||||
}
|
||||
if _, dup := obj.Vals[key]; !dup {
|
||||
obj.Keys = append(obj.Keys, key)
|
||||
}
|
||||
obj.Vals[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, "", false)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PyDumpsIndent2 — Python json.dump(value, indent=2): separators ',' / ': '
|
||||
// with 2-space indentation, ensure_ascii=True.
|
||||
func PyDumpsIndent2(v PyValue) string {
|
||||
var b strings.Builder
|
||||
writePy(&b, v, "", true)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writePy(b *strings.Builder, v PyValue, indent string, indented bool) {
|
||||
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(PyFmtNum(t))
|
||||
case string:
|
||||
writePyString(b, t)
|
||||
case []PyValue:
|
||||
if len(t) == 0 {
|
||||
b.WriteString("[]")
|
||||
return
|
||||
}
|
||||
b.WriteByte('[')
|
||||
inner := indent
|
||||
if indented {
|
||||
inner = indent + " "
|
||||
}
|
||||
for i, item := range t {
|
||||
if indented {
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(inner)
|
||||
} else if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePy(b, item, inner, indented)
|
||||
}
|
||||
if indented {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(indent)
|
||||
}
|
||||
b.WriteByte(']')
|
||||
case *PyObj:
|
||||
if len(t.Keys) == 0 {
|
||||
b.WriteString("{}")
|
||||
return
|
||||
}
|
||||
b.WriteByte('{')
|
||||
inner := indent
|
||||
if indented {
|
||||
inner = indent + " "
|
||||
}
|
||||
for i, k := range t.Keys {
|
||||
if indented {
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(inner)
|
||||
} else if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePyString(b, k)
|
||||
b.WriteString(": ")
|
||||
writePy(b, t.Vals[k], inner, indented)
|
||||
}
|
||||
if indented {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(indent)
|
||||
}
|
||||
b.WriteByte('}')
|
||||
default:
|
||||
b.WriteString(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// PyFmtNum — the Rust core's fmt_num: integral floats render as integers,
|
||||
// everything else via shortest fixed-notation round-trip (Rust f64 Display
|
||||
// semantics — never scientific notation).
|
||||
func PyFmtNum(n json.Number) string {
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return n.String()
|
||||
}
|
||||
if !math.IsInf(f, 0) && !math.IsNaN(f) && f == math.Trunc(f) &&
|
||||
math.Abs(f) < 9.007199254740992e15 {
|
||||
return strconv.FormatInt(int64(f), 10)
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -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, \n \r \t control renders (the shapes a status/extra key takes).
|
||||
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()
|
||||
}
|
||||
|
||||
// PyListRepr — Python repr() of a list of strings: ['a', 'b'].
|
||||
func PyListRepr(items []string) string {
|
||||
parts := make([]string, len(items))
|
||||
for i, s := range items {
|
||||
parts[i] = PyRepr(s)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
}
|
||||
|
||||
// PyIsBlank — Python str.strip() emptiness (any non-whitespace = content).
|
||||
func PyIsBlank(s string) bool {
|
||||
return strings.TrimFunc(s, unicode.IsSpace) == ""
|
||||
}
|
||||
|
||||
// pyStrlen — Python len() of a str: Unicode scalar (rune) count.
|
||||
func pyStrlen(s string) int { return utf8.RuneCountInString(s) }
|
||||
@@ -0,0 +1,478 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// scripts/gen_evidence_probe_truth.py against the live Python oracle with
|
||||
// the clock frozen, the schema gate served by the Rust core, the runner and
|
||||
// subprocess seams stubbed) and re-executes every row through this Go port.
|
||||
// Prompt/error/file bytes compare exactly; @ARTDIR@ normalizes the temp
|
||||
// base dir on both sides.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
FrozenTS int64 `json:"frozen_ts"`
|
||||
FrozenDecidedAt string `json:"frozen_decided_at"`
|
||||
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 in its own temp dir 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 _, row := range doc.Rows {
|
||||
if msg := replayRow(&doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
func replayRow(doc *TruthDoc, row *TruthRow) string {
|
||||
base, err := os.MkdirTemp("", "ep_truth_")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(base)
|
||||
|
||||
oldClock := ClockNow
|
||||
oldModel := ModelSelect
|
||||
oldExec := ExecCmd
|
||||
oldEnv := snapshotEnv([]string{"MIMO_BIN", "MIMO_AGENT", "MIMO_MODEL", "EP_CAPTURE", "EP_EXIT"})
|
||||
defer func() {
|
||||
ClockNow, ModelSelect, ExecCmd = oldClock, oldModel, oldExec
|
||||
restoreEnv(oldEnv)
|
||||
}()
|
||||
ClockNow = ClockFunc(func() time.Time { return time.Unix(doc.FrozenTS, 0) })
|
||||
|
||||
var got any
|
||||
switch row.Op {
|
||||
case "prompt":
|
||||
got, err = replayPrompt(row)
|
||||
case "validate":
|
||||
got, err = replayValidate(row)
|
||||
case "count":
|
||||
got, err = replayCount(row, base)
|
||||
case "budget":
|
||||
got, err = replayBudget(row, base)
|
||||
case "record":
|
||||
got, err = replayRecord(row, base)
|
||||
case "run":
|
||||
got, err = replayRun(row, base)
|
||||
case "to_dict":
|
||||
got, err = replayToDict(row)
|
||||
case "default_runner":
|
||||
got, err = replayDefaultRunner(row, base)
|
||||
default:
|
||||
return "unknown op " + row.Op
|
||||
}
|
||||
if err != nil {
|
||||
return "error: " + err.Error()
|
||||
}
|
||||
|
||||
var want any
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
return "parse want: " + err.Error()
|
||||
}
|
||||
if !reflect.DeepEqual(normalizeJSON(got), normalizeJSON(want)) {
|
||||
return fmt.Sprintf("got %#v\n want %#v", got, want)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// normalizeJSON — round-trip through map[string]any/float64 so got and want
|
||||
// numbers compare by value, then stringify json.Number.
|
||||
func normalizeJSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case int:
|
||||
return float64(t)
|
||||
case json.Number:
|
||||
f, err := t.Float64()
|
||||
if err != nil {
|
||||
return t.String()
|
||||
}
|
||||
return f
|
||||
case map[string]any:
|
||||
m := map[string]any{}
|
||||
for k, val := range t {
|
||||
m[k] = normalizeJSON(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
s := make([]any, len(t))
|
||||
for i, val := range t {
|
||||
s[i] = normalizeJSON(val)
|
||||
}
|
||||
return s
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func denorm(s, base string) string { return strings.ReplaceAll(s, "@ARTDIR@", base) }
|
||||
|
||||
// normOut — normalize a Go-side output path/string for comparison with the
|
||||
// frozen truth (the generator replaced its temp base with @ARTDIR@).
|
||||
func normOut(s, base string) string { return strings.ReplaceAll(s, base, "@ARTDIR@") }
|
||||
|
||||
func snapshotEnv(keys []string) map[string]*string {
|
||||
out := map[string]*string{}
|
||||
for _, k := range keys {
|
||||
if v, ok := os.LookupEnv(k); ok {
|
||||
cp := v
|
||||
out[k] = &cp
|
||||
} else {
|
||||
out[k] = nil
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func restoreEnv(snap map[string]*string) {
|
||||
for k, v := range snap {
|
||||
if v == nil {
|
||||
os.Unsetenv(k)
|
||||
} else {
|
||||
os.Setenv(k, *v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ops ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
func replayPrompt(row *TruthRow) (any, error) {
|
||||
var setup struct {
|
||||
Question string `json:"question"`
|
||||
ArtifactPath string `json:"artifact_path"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return BuildProbePrompt(setup.Question, setup.ArtifactPath), nil
|
||||
}
|
||||
|
||||
func replayValidate(row *TruthRow) (any, error) {
|
||||
var setup struct {
|
||||
Artifact json.RawMessage `json:"artifact"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
artifact, err := PyParse(setup.Artifact)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse artifact: %w", err)
|
||||
}
|
||||
errs := ValidateProbe(artifact)
|
||||
out := make([]any, len(errs))
|
||||
for i, e := range errs {
|
||||
out[i] = e
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func replayCount(row *TruthRow, base string) (any, error) {
|
||||
var setup struct {
|
||||
Dir string `json:"dir"`
|
||||
Files map[string]string `json:"files"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := denorm(setup.Dir, base)
|
||||
if err := writeTree(dir, setup.Files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CountProbes(dir), nil
|
||||
}
|
||||
|
||||
func replayBudget(row *TruthRow, base string) (any, error) {
|
||||
var setup struct {
|
||||
Dir string `json:"dir"`
|
||||
Files map[string]string `json:"files"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := denorm(setup.Dir, base)
|
||||
if err := writeTree(dir, setup.Files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := CheckBudget(dir)
|
||||
if err == nil {
|
||||
// exc_row froze {"error": null, "value": null} on success.
|
||||
return map[string]any{"error": nil, "value": nil}, nil
|
||||
}
|
||||
return map[string]any{"error": errorValue(err)}, nil
|
||||
}
|
||||
|
||||
// errorValue — the frozen exception shape; typed nil must stay untyped nil
|
||||
// so DeepEqual matches the JSON null on the want side.
|
||||
func errorValue(err error) any {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
msg := err.Error()
|
||||
typ := fmt.Sprintf("%T", err)
|
||||
switch t := err.(type) {
|
||||
case *ValueError:
|
||||
typ, msg = "ValueError", t.Msg
|
||||
case *ProbeBudgetExceeded:
|
||||
typ, msg = "ProbeBudgetExceeded", t.Msg
|
||||
case *ProviderUnresolvedError:
|
||||
typ, msg = "ProviderUnresolved", t.Msg
|
||||
}
|
||||
return map[string]any{"type": typ, "msg": msg}
|
||||
}
|
||||
|
||||
func replayRecord(row *TruthRow, base string) (any, error) {
|
||||
var setup struct {
|
||||
Result map[string]string `json:"result"`
|
||||
Files map[string]string `json:"files"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := denorm("@ARTDIR@/"+row.Name, base)
|
||||
if err := writeTree(dir, setup.Files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := ProbeResult{
|
||||
Question: setup.Result["question"],
|
||||
Source: setup.Result["source"],
|
||||
Answer: setup.Result["answer"],
|
||||
Implication: setup.Result["implication"],
|
||||
Status: setup.Result["status"],
|
||||
ProbeID: setup.Result["probe_id"],
|
||||
DecidedAt: setup.Result["decided_at"],
|
||||
Model: setup.Result["model"],
|
||||
}
|
||||
path, err := RecordProbe(result, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pj, err := os.ReadFile(filepath.Join(dir, "probe.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pjl, err := os.ReadFile(filepath.Join(dir, "probe.jsonl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"path": normOut(path, base),
|
||||
"probe_json": string(pj),
|
||||
"probe_jsonl": string(pjl),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func writeTree(dir string, files map[string]string) error {
|
||||
for rel, content := range files {
|
||||
p := filepath.Join(dir, filepath.FromSlash(rel))
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(content), 0o666); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replayRun(row *TruthRow, base string) (any, error) {
|
||||
var setup struct {
|
||||
Question string `json:"question"`
|
||||
Model string `json:"model"`
|
||||
Env map[string]any `json:"env"`
|
||||
Files map[string]string `json:"files"`
|
||||
Runner struct {
|
||||
Type string `json:"type"`
|
||||
Artifact json.RawMessage `json:"artifact"`
|
||||
} `json:"runner"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range setup.Env {
|
||||
if v == nil {
|
||||
os.Unsetenv(k)
|
||||
} else if s, ok := v.(string); ok {
|
||||
os.Setenv(k, s)
|
||||
}
|
||||
}
|
||||
dir := denorm("@ARTDIR@/"+row.Name, base)
|
||||
if err := os.MkdirAll(dir, 0o777); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writeTree(dir, setup.Files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
captured := struct {
|
||||
prompt, artifactPath string
|
||||
called bool
|
||||
}{}
|
||||
runner := RunnerFunc(func(prompt, artifactPath string) error {
|
||||
captured.prompt, captured.artifactPath, captured.called = prompt, artifactPath, true
|
||||
switch setup.Runner.Type {
|
||||
case "write":
|
||||
artifact, err := PyParse(setup.Runner.Artifact)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(artifactPath, []byte(PyDumpsCompact(artifact)), 0o666)
|
||||
case "silent":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown runner type %q", setup.Runner.Type)
|
||||
}
|
||||
})
|
||||
|
||||
result, err := RunProbe(setup.Question, dir, runner, setup.Model)
|
||||
out := map[string]any{"error": errorValue(err)}
|
||||
if err == nil {
|
||||
var resultJSON any
|
||||
if uerr := json.Unmarshal([]byte(PyDumpsCompact(result.ToDict())), &resultJSON); uerr != nil {
|
||||
return nil, uerr
|
||||
}
|
||||
out["result"] = resultJSON
|
||||
pj, rerr := os.ReadFile(filepath.Join(dir, "probe.json"))
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
pjl, rerr := os.ReadFile(filepath.Join(dir, "probe.jsonl"))
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
out["probe_json"] = string(pj)
|
||||
out["probe_jsonl"] = string(pjl)
|
||||
}
|
||||
out["runner_called"] = captured.called
|
||||
if captured.called {
|
||||
out["prompt"] = normOut(captured.prompt, base)
|
||||
out["artifact_path"] = normOut(captured.artifactPath, base)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func replayToDict(row *TruthRow) (any, error) {
|
||||
var setup struct {
|
||||
Fields map[string]string `json:"fields"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := ProbeResult{
|
||||
Question: setup.Fields["question"],
|
||||
Source: setup.Fields["source"],
|
||||
Answer: setup.Fields["answer"],
|
||||
Implication: setup.Fields["implication"],
|
||||
Status: setup.Fields["status"],
|
||||
ProbeID: setup.Fields["probe_id"],
|
||||
DecidedAt: setup.Fields["decided_at"],
|
||||
Model: setup.Fields["model"],
|
||||
}
|
||||
if _, ok := setup.Fields["model"]; !ok {
|
||||
result.Model = "UNKNOWN" // the oracle dataclass default
|
||||
}
|
||||
return PyDumpsCompact(result.ToDict()), nil
|
||||
}
|
||||
|
||||
func replayDefaultRunner(row *TruthRow, base string) (any, error) {
|
||||
var setup struct {
|
||||
ExitCode int `json:"exit_code"`
|
||||
AgentEnv *string `json:"agent_env"`
|
||||
ModelMode string `json:"model_mode"`
|
||||
}
|
||||
if err := json.Unmarshal(row.Setup, &setup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stub := filepath.Join(base, "ep_stub.sh")
|
||||
script := "#!/bin/sh\n" +
|
||||
"for a in \"$@\"; do printf '%s\\n' \"$a\" >> \"$EP_CAPTURE\"; done\n" +
|
||||
"printf 'stub-out\\n'\n" +
|
||||
"exit \"$EP_EXIT\"\n"
|
||||
if err := os.WriteFile(stub, []byte(script), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := denorm("@ARTDIR@/"+row.Name, base)
|
||||
if err := os.MkdirAll(dir, 0o777); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
capture := filepath.Join(dir, "argv_capture.txt")
|
||||
os.Remove(capture)
|
||||
artifactPath := filepath.Join(dir, "evidence_probe.json")
|
||||
|
||||
os.Setenv("MIMO_BIN", stub)
|
||||
os.Setenv("EP_CAPTURE", capture)
|
||||
os.Setenv("EP_EXIT", fmt.Sprintf("%d", setup.ExitCode))
|
||||
if setup.AgentEnv == nil {
|
||||
os.Unsetenv("MIMO_AGENT")
|
||||
} else {
|
||||
os.Setenv("MIMO_AGENT", *setup.AgentEnv)
|
||||
}
|
||||
switch setup.ModelMode {
|
||||
case "unresolved":
|
||||
os.Unsetenv("MIMO_MODEL")
|
||||
ModelSelect = EnvModelSource
|
||||
case "blank":
|
||||
os.Setenv("MIMO_MODEL", " ")
|
||||
ModelSelect = EnvModelSource
|
||||
default:
|
||||
ModelSelect = ModelSourceFunc(func() (string, error) { return "test-model-v9", nil })
|
||||
}
|
||||
|
||||
err := DefaultRunner("PROMPT-TEXT", artifactPath)
|
||||
out := map[string]any{"error": errorValue(err)}
|
||||
if lines, rerr := readLines(capture); rerr == nil {
|
||||
norm := make([]any, len(lines))
|
||||
for i, l := range lines {
|
||||
norm[i] = normOut(l, base)
|
||||
}
|
||||
out["argv"] = norm
|
||||
} else {
|
||||
out["argv"] = nil
|
||||
}
|
||||
logPath := filepath.Join(dir, "evidence_probe.stdout.log")
|
||||
if log, rerr := os.ReadFile(logPath); rerr == nil {
|
||||
out["stdout_log"] = string(log)
|
||||
} else {
|
||||
out["stdout_log"] = nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readLines(path string) ([]string, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := strings.TrimSuffix(string(raw), "\n")
|
||||
if content == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return strings.Split(content, "\n"), nil
|
||||
}
|
||||
+1598
-12
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate: the frozen oracle truth (truth.json,
|
||||
// generated against the LIVE Python oracle with the clock frozen and the
|
||||
// Rust core serving validate_probe/count_probes) replays VERBATIM. Any
|
||||
// divergence fails; the truth file rides the repo so the gate holds in CI
|
||||
// without /tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruthDoc(t *testing.T, path string) *TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth.json: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth.json contained no rows")
|
||||
}
|
||||
return &doc
|
||||
}
|
||||
|
||||
func findRow(t *testing.T, doc *TruthDoc, name string) TruthRow {
|
||||
t.Helper()
|
||||
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 mustReplayRow(t *testing.T, doc *TruthDoc, name string) {
|
||||
t.Helper()
|
||||
row := findRow(t, doc, name)
|
||||
if msg := replayRow(doc, &row); msg != "" {
|
||||
t.Errorf("DIVERGED %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullTruthReplayVerbatim(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := replayRow(doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
t.Errorf("DIVERGED %s", f)
|
||||
}
|
||||
t.Fatalf("frozen-truth parity: %d divergence(s)", len(fails))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruthOpCoverage(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
// The frozen battery must keep covering every op family: if a
|
||||
// regeneration drops a family, this fails before parity silently rots.
|
||||
want := map[string]int{
|
||||
"prompt": 5, "validate": 49, "count": 11, "budget": 4,
|
||||
"record": 3, "run": 11, "to_dict": 3, "default_runner": 5,
|
||||
}
|
||||
got := map[string]int{}
|
||||
for _, row := range doc.Rows {
|
||||
got[row.Op]++
|
||||
}
|
||||
if fmt.Sprint(got) != fmt.Sprint(want) {
|
||||
t.Fatalf("truth.json op coverage drifted:\n got %v\n want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptBaselineByteParity(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
row := findRow(t, doc, "prompt_baseline")
|
||||
got, err := replayPrompt(&row)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var want string
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("prompt drifted from frozen oracle:\n got %q\n want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateErrorOrderIsFieldOrder(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
for _, name := range []string{
|
||||
"combined_ordering", "status_obj_order", "grounded_missing_source",
|
||||
"status_repr_unicode", "status_big_float", "status_huge_int",
|
||||
"valid_grounded", "empty_artifact", "extra_sorted", "nonobject_list",
|
||||
} {
|
||||
mustReplayRow(t, doc, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProbeExceptionSemantics(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
for _, name := range []string{
|
||||
"run_success_grounded", "run_success_still_unknown",
|
||||
"run_model_from_env", "run_model_unset_unknown",
|
||||
"run_model_env_blank_is_empty_string",
|
||||
"run_budget_exhausted_before_runner", "run_budget_one_left",
|
||||
"run_silent_runner_missing_artifact", "run_gate_failure_joined",
|
||||
"run_gate_failure_non_dict", "run_gate_failure_status_non_string",
|
||||
} {
|
||||
mustReplayRow(t, doc, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRunnerArgvParity(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
for _, name := range []string{
|
||||
"defrun_exit0_default_agent", "defrun_exit3", "defrun_custom_agent",
|
||||
"defrun_model_unresolved", "defrun_model_blank_env",
|
||||
} {
|
||||
mustReplayRow(t, doc, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountAndBudgetSemantics(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
for _, name := range []string{
|
||||
"count_absent_dir", "count_cr_lines", "count_no_trailing_newline",
|
||||
"count_interleaved_blank", "budget_zero", "budget_one",
|
||||
"budget_two", "budget_three",
|
||||
} {
|
||||
mustReplayRow(t, doc, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordProbeBytesParity(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
for _, name := range []string{
|
||||
"record_fresh_dir_created", "record_appends_existing",
|
||||
"record_unicode_ensure_ascii",
|
||||
} {
|
||||
mustReplayRow(t, doc, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDictFieldOrderParity(t *testing.T) {
|
||||
doc := loadTruthDoc(t, "truth.json")
|
||||
for _, name := range []string{"to_dict_defaults", "to_dict_full", "to_dict_unicode"} {
|
||||
mustReplayRow(t, doc, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeJSONIntFloatEquivalence(t *testing.T) {
|
||||
if !reflect.DeepEqual(normalizeJSON(2), normalizeJSON(2.0)) {
|
||||
t.Fatal("int/float normalization broke count comparisons")
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.209.0"
|
||||
__version__ = "0.229.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust build_basis core (dichotomy wave #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* bb_research_depth({route, mode, justification}) -> depth decision dict
|
||||
* bb_new_basis({question, depth_decision, issue_ref}) -> basis dict
|
||||
* bb_set_dispositions({basis, dispositions}) -> {"basis", "errors"}
|
||||
* bb_validate_basis({basis}) -> [errors]
|
||||
* bb_is_qualified({basis}) -> bool
|
||||
* bb_attach_evidence_ref({basis}) -> str
|
||||
* bb_summary({basis}) -> summary dict
|
||||
* bb_revise_depth({basis, new_depth, reason}) -> {"basis", "revision"}
|
||||
* bb_to_json({basis}) -> str (json.dumps indent=1 sort_keys semantics)
|
||||
* bb_from_json({text}) -> parsed value
|
||||
* bb_generate_research_plan({...}) -> plan dict
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with BB_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "build_basis"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("BB_RUST_LIB", _CRATE_DIR / "target" / "release" / "libbuild_basis.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"bb_research_depth", "bb_new_basis", "bb_set_dispositions",
|
||||
"bb_validate_basis", "bb_is_qualified", "bb_attach_evidence_ref",
|
||||
"bb_summary", "bb_revise_depth", "bb_to_json", "bb_from_json",
|
||||
"bb_generate_research_plan",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class BasisUnavailable(FileNotFoundError):
|
||||
"""The Rust build_basis core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise BasisUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def research_depth(route: str, mode: str = "", justification: str = "") -> dict:
|
||||
return _call("bb_research_depth", {
|
||||
"route": route, "mode": mode, "justification": justification})
|
||||
|
||||
|
||||
def new_basis(question: str, depth_decision: dict, issue_ref: str = "") -> dict:
|
||||
return _call("bb_new_basis", {
|
||||
"question": question, "depth_decision": depth_decision,
|
||||
"issue_ref": issue_ref})
|
||||
|
||||
|
||||
def set_dispositions(basis: dict, dispositions: list) -> dict:
|
||||
"""Returns {"basis": <mutated basis>, "errors": [...]}."""
|
||||
return _call("bb_set_dispositions", {
|
||||
"basis": basis, "dispositions": dispositions})
|
||||
|
||||
|
||||
def validate_basis(basis: dict) -> list:
|
||||
return _call("bb_validate_basis", {"basis": basis})
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
return _call("bb_is_qualified", {"basis": basis})
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
return _call("bb_attach_evidence_ref", {"basis": basis})
|
||||
|
||||
|
||||
def summary(basis: dict) -> dict:
|
||||
return _call("bb_summary", {"basis": basis})
|
||||
|
||||
|
||||
def revise_depth(basis: dict, new_depth: str, reason: str) -> dict:
|
||||
"""Returns {"basis": <mutated basis>, "revision": {...}}; raises
|
||||
RuntimeError carrying the exact oracle ValueError message on refusal."""
|
||||
return _call("bb_revise_depth", {
|
||||
"basis": basis, "new_depth": new_depth, "reason": reason})
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
return _call("bb_to_json", {"basis": basis})
|
||||
|
||||
|
||||
def from_json(text: str):
|
||||
return _call("bb_from_json", {"text": text})
|
||||
|
||||
|
||||
def generate_research_plan(intent: str, target: str = "",
|
||||
current_reality: str = "",
|
||||
unknowns: list | None = None,
|
||||
decisions: list | None = None,
|
||||
risks: list | None = None,
|
||||
depth: str | None = None) -> dict:
|
||||
return _call("bb_generate_research_plan", {
|
||||
"intent": intent, "target": target,
|
||||
"current_reality": current_reality,
|
||||
"unknowns": list(unknowns or []), "decisions": list(decisions or []),
|
||||
"risks": list(risks or []), "depth": depth})
|
||||
+158
-29
@@ -16,16 +16,19 @@ Usage:
|
||||
behavior.py list [--type correction|habit|all]
|
||||
behavior.py stats
|
||||
|
||||
Phase G strangler slice 2 (port 5): the decision math (reinforce
|
||||
counting/confidence/promotion/state formulas) is served IN-PROCESS by the
|
||||
Rust `evidence_provenance` cdylib through ep_loader.py. STORAGE STAYS
|
||||
PYTHON — SQLite I/O and the regex matching layer remain here (the crate is
|
||||
zero-dep by design; hand-rolling SQLite/regex would be hubris, not
|
||||
hardening). Vintage upgrade per the frozen contract §2: this module is the
|
||||
src/rune authority vintage; the deployed ~/.config/opencode/scripts copy
|
||||
is superseded by the same shim shape pointing at the live db path.
|
||||
Timestamps are pinned by the shim (`now`) — the core is deterministic.
|
||||
API, CLI, and PLUGIN shapes unchanged (git history holds the original).
|
||||
Phase G strangler slice 2 (port 5), superseded by the dichotomy wave
|
||||
(#2479 tranche): the FULL decision surface — reinforce counting/
|
||||
confidence/promotion/state AND the match_context per-row predicates —
|
||||
is served IN-PROCESS by the Rust `behavior` cdylib through bh_loader.py.
|
||||
STORAGE STAYS PYTHON — SQLite I/O and row ordering remain here (the
|
||||
crate links no sqlite; hand-rolling SQLite would be hubris, not
|
||||
hardening). The evidence_provenance ep_loader path remains as the
|
||||
Python-pin fallback oracle. Vintage upgrade per the frozen contract §2:
|
||||
this module is the src/rune authority vintage; the deployed
|
||||
~/.config/opencode/scripts copy is superseded by the same shim shape
|
||||
pointing at the live db path. Timestamps are pinned by the shim (`now`)
|
||||
— the core is deterministic. API, CLI, and PLUGIN shapes unchanged (git
|
||||
history holds the original).
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -35,7 +38,7 @@ import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ── Rust core wiring (slice-2 port-5 shim pattern) ──────────────────────────
|
||||
# ── Rust core wiring (dichotomy-wave doc-envelope pattern) ──────────────────
|
||||
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:
|
||||
@@ -45,6 +48,28 @@ import ep_loader # noqa: E402 (loads target/release cdylib)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import memory_env
|
||||
|
||||
try:
|
||||
from . import bh_loader
|
||||
except ImportError: # direct script execution (canary `import behavior`)
|
||||
try:
|
||||
import bh_loader # type: ignore[no-redef]
|
||||
except ImportError: # noqa: S110 — no loader on path → Python oracle below
|
||||
bh_loader = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _bh_core():
|
||||
"""The Rust behavior-core probe (strangler law). RUNE_BEHAVIOR_CORE=
|
||||
python pins the Python fallback — the pin is honored inside
|
||||
bh_loader.core so every consumer gets it. Returns the loader MODULE
|
||||
(never the raw CDLL)."""
|
||||
if bh_loader is None:
|
||||
return None
|
||||
try:
|
||||
return bh_loader if bh_loader.core() is not None else None
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
return None
|
||||
|
||||
|
||||
DB_PATH = os.path.join(memory_env.memory_dir(), "behavior.db")
|
||||
|
||||
|
||||
@@ -157,11 +182,17 @@ def add_habit(trigger, action, name="", context_tags=None, min_activations=3):
|
||||
|
||||
|
||||
def match_context(context, match_type=None):
|
||||
"""Find matching corrections and habits for the current context."""
|
||||
"""Find matching corrections and habits for the current context.
|
||||
|
||||
The per-row predicates (SQL WHERE gates + regex/domain filters)
|
||||
delegate to the Rust core (bh_match_row); the SQL fetch and ordering
|
||||
stay Python. Falls back to the inline predicates on kernel failure
|
||||
or the RUNE_BEHAVIOR_CORE=python pin."""
|
||||
db = connect()
|
||||
try:
|
||||
results = {"corrections": [], "habits": []}
|
||||
context_low = context.lower()
|
||||
core = _bh_core()
|
||||
|
||||
# Match corrections
|
||||
if match_type != "habit":
|
||||
@@ -170,18 +201,17 @@ def match_context(context, match_type=None):
|
||||
ORDER BY confidence DESC
|
||||
""").fetchall()
|
||||
for c in corrections:
|
||||
# Check scope pattern match
|
||||
pattern = c["scope_pattern"]
|
||||
if pattern:
|
||||
if core is not None:
|
||||
try:
|
||||
if not re.search(pattern, context, re.IGNORECASE):
|
||||
if not core.match_row("correction", dict(c), context)["matched"]:
|
||||
continue
|
||||
except re.error:
|
||||
continue # skip invalid regex patterns
|
||||
# Check domain match
|
||||
domain = c["scope_domain"]
|
||||
if domain and domain.lower() not in context_low:
|
||||
continue
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
core = None
|
||||
if not _correction_matches_python(dict(c), context, context_low):
|
||||
continue
|
||||
else:
|
||||
if not _correction_matches_python(dict(c), context, context_low):
|
||||
continue
|
||||
row = dict(c)
|
||||
# Parse scope_tags from JSON string to dict
|
||||
try:
|
||||
@@ -198,12 +228,17 @@ def match_context(context, match_type=None):
|
||||
ORDER BY confidence * activation_count DESC
|
||||
""").fetchall()
|
||||
for h in habits:
|
||||
trigger = h["trigger_pattern"]
|
||||
try:
|
||||
if not re.search(trigger, context, re.IGNORECASE):
|
||||
if core is not None:
|
||||
try:
|
||||
if not core.match_row("habit", dict(h), context)["matched"]:
|
||||
continue
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
core = None
|
||||
if not _habit_matches_python(dict(h), context):
|
||||
continue
|
||||
else:
|
||||
if not _habit_matches_python(dict(h), context):
|
||||
continue
|
||||
except re.error:
|
||||
continue # skip invalid regex patterns
|
||||
results["habits"].append(dict(h))
|
||||
|
||||
return results
|
||||
@@ -211,11 +246,105 @@ def match_context(context, match_type=None):
|
||||
db.close()
|
||||
|
||||
|
||||
def _correction_matches_python(row, context, context_low):
|
||||
"""The inline per-row correction predicates (Python oracle)."""
|
||||
# Check scope pattern match
|
||||
pattern = row["scope_pattern"]
|
||||
if pattern:
|
||||
try:
|
||||
if not re.search(pattern, context, re.IGNORECASE):
|
||||
return False
|
||||
except re.error:
|
||||
return False # skip invalid regex patterns
|
||||
# Check domain match
|
||||
domain = row["scope_domain"]
|
||||
if domain and domain.lower() not in context_low:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _habit_matches_python(row, context):
|
||||
"""The inline per-row habit predicate (Python oracle)."""
|
||||
trigger = row["trigger_pattern"]
|
||||
try:
|
||||
if not re.search(trigger, context, re.IGNORECASE):
|
||||
return False
|
||||
except re.error:
|
||||
return False # skip invalid regex patterns
|
||||
return True
|
||||
|
||||
|
||||
def reinforce(item_id, outcome, item_type="habit"):
|
||||
"""Reinforce a correction or habit based on outcome.
|
||||
|
||||
Decision math (counting, confidence formula, promotion, state) served
|
||||
by the Rust core; SQL stays here."""
|
||||
Decision math (counting, confidence formula, promotion, state)
|
||||
delegates to the Rust behavior core; SQL stays here. Falls back to
|
||||
the evidence_provenance shim path on kernel failure or the
|
||||
RUNE_BEHAVIOR_CORE=python pin."""
|
||||
core = _bh_core()
|
||||
if core is not None:
|
||||
try:
|
||||
return _reinforce_rust(core, item_id, outcome, item_type)
|
||||
except Exception: # noqa: BLE001 — kernel failure → shim path below
|
||||
pass
|
||||
return _reinforce_python(item_id, outcome, item_type)
|
||||
|
||||
|
||||
def _reinforce_rust(core, item_id, outcome, item_type):
|
||||
"""The bh-core delegation half of reinforce (fetch-row -> Rust math ->
|
||||
write-back). Raises through to the shim path on any failure."""
|
||||
db = connect()
|
||||
try:
|
||||
ts = _now()
|
||||
|
||||
if item_type == "habit":
|
||||
habit = db.execute("SELECT * FROM habits WHERE id=?", (item_id,)).fetchone()
|
||||
if not habit:
|
||||
return {"error": f"Habit {item_id} not found"}
|
||||
|
||||
upd = core.habit_reinforce(dict(habit), outcome)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE habits SET activation_count=?, success_count=?, failure_count=?,
|
||||
confidence=?, state=?, last_activated=? WHERE id=?
|
||||
""",
|
||||
(upd["activation_count"], upd["success_count"], upd["failure_count"],
|
||||
upd["confidence"], upd["state"], ts, item_id),
|
||||
)
|
||||
|
||||
# Log activation
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO habit_activations (habit_id, context, outcome, activated_at)
|
||||
VALUES (?, '', ?, ?)
|
||||
""",
|
||||
(item_id, outcome, ts),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return {"reinforced": item_id, "confidence": upd["confidence"], "state": upd["state"]}
|
||||
|
||||
else: # correction
|
||||
correction = db.execute("SELECT * FROM corrections WHERE id=?", (item_id,)).fetchone()
|
||||
if not correction:
|
||||
return {"error": f"Correction {item_id} not found"}
|
||||
|
||||
upd = core.correction_reinforce(dict(correction), outcome)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE corrections SET confidence=?, evidence_count=?, state=?, last_reinforced=?
|
||||
WHERE id=?
|
||||
""",
|
||||
(upd["confidence"], upd["evidence_count"], upd["state"], ts, item_id),
|
||||
)
|
||||
db.commit()
|
||||
return {"reinforced": item_id, "confidence": upd["confidence"], "state": upd["state"]}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _reinforce_python(item_id, outcome, item_type="habit"):
|
||||
"""Reinforce via the evidence_provenance shim (the prior oracle path)."""
|
||||
db = connect()
|
||||
try:
|
||||
ts = _now()
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust behavior core (dichotomy wave, #2479
|
||||
tranche). Mirrors contracts_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* bh_habit_reinforce({row, outcome}) -> update dict
|
||||
* bh_correction_reinforce({row, outcome}) -> update dict
|
||||
* bh_match_row({kind, row, context}) -> {"visible", "matched"}
|
||||
* bh_plugin() -> the PLUGIN descriptor
|
||||
|
||||
SQLite I/O stays in behavior.py; this core owns the reinforce decision
|
||||
math and the match predicates. Free: capability_scoring's cs_free_string
|
||||
(shared process) — NEVER re-export a free symbol from this crate.
|
||||
Override the .so with BEHAVIOR_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" / "behavior"
|
||||
_LIB_PATH = Path(os.environ.get("BEHAVIOR_RUST_LIB", _CRATE_DIR / "target" / "release" / "libbehavior.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
_SYMBOLS = (
|
||||
"bh_habit_reinforce",
|
||||
"bh_correction_reinforce",
|
||||
"bh_match_row",
|
||||
"bh_plugin",
|
||||
)
|
||||
|
||||
|
||||
class BehaviorCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust behavior 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_BEHAVIOR_CORE=python pins the fallback — THIS is
|
||||
where the pin is honored, so every consumer gets it."""
|
||||
if os.environ.get("RUNE_BEHAVIOR_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 BehaviorCoreUnavailable(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 habit_reinforce(row: dict, outcome: str) -> dict:
|
||||
return _call("bh_habit_reinforce", {"row": row, "outcome": outcome})
|
||||
|
||||
|
||||
def correction_reinforce(row: dict, outcome: str) -> dict:
|
||||
return _call("bh_correction_reinforce", {"row": row, "outcome": outcome})
|
||||
|
||||
|
||||
def match_row(kind: str, row: dict, context: str) -> dict:
|
||||
return _call("bh_match_row", {"kind": kind, "row": row, "context": context})
|
||||
|
||||
|
||||
def plugin() -> dict:
|
||||
return _call("bh_plugin", {})
|
||||
@@ -19,11 +19,34 @@ structures the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def _rust(fn: str, *args, **kwargs):
|
||||
"""Delegate to the Rust kernel (dichotomy wave #2479); returns
|
||||
_MISSING when the core is unavailable so the Python oracle body runs.
|
||||
RUNE_BB_CORE=python pins the oracle unconditionally."""
|
||||
if os.environ.get("RUNE_BB_CORE", "auto") == "python":
|
||||
return _MISSING
|
||||
try:
|
||||
from rune import bb_loader
|
||||
if not bb_loader.available():
|
||||
return _MISSING
|
||||
return getattr(bb_loader, fn)(*args, **kwargs)
|
||||
except Exception:
|
||||
return _MISSING
|
||||
|
||||
|
||||
class _Missing:
|
||||
pass
|
||||
|
||||
|
||||
_MISSING = _Missing()
|
||||
|
||||
# Research depth tiers — derived from adaptive_router ROUTES, never a
|
||||
# second router (rule C/A). Each tier records WHY it was chosen.
|
||||
DEPTH_NONE = "NONE" # tiny obvious local change; local evidence only
|
||||
@@ -48,6 +71,9 @@ def research_depth(route: str, *, mode: str = "",
|
||||
the canonical route authority (adaptive_router); `justification` is
|
||||
REQUIRED when deviating from the route default — recorded, never
|
||||
silent (rule D: record why research depth was chosen)."""
|
||||
out = _rust("research_depth", route, mode, justification)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
default = _ROUTE_DEFAULT_DEPTH.get(route, DEPTH_INWARD)
|
||||
depth = default
|
||||
why = f"route {route} default"
|
||||
@@ -89,6 +115,9 @@ def new_basis(*, question: str, depth_decision: dict,
|
||||
"""Create an empty-but-valid Build Basis. References only — full
|
||||
research dumps live in the Research Core's artifact dirs (rule 18C:
|
||||
do not store unnecessary dumps)."""
|
||||
out = _rust("new_basis", question, depth_decision, issue_ref)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
@@ -112,6 +141,11 @@ def new_basis(*, question: str, depth_decision: dict,
|
||||
def set_dispositions(basis: dict, dispositions: list[dict]) -> list[str]:
|
||||
"""Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
Returns validation errors; the basis stays unqualified until valid."""
|
||||
out = _rust("set_dispositions", basis, dispositions)
|
||||
if out is not _MISSING:
|
||||
basis.clear()
|
||||
basis.update(out["basis"])
|
||||
return out["errors"]
|
||||
basis["dispositions"] = [
|
||||
{"item": d.get("item", ""), "disposition": d.get("disposition", ""),
|
||||
"why": d.get("why", ""), "ref": d.get("ref", "")}
|
||||
@@ -124,6 +158,9 @@ def validate_basis(basis: dict) -> list[str]:
|
||||
"""Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
decision with why, at least one outward prior-art answer when depth
|
||||
is OUTWARD/FULL, and every disposition uses a legal disposition."""
|
||||
out = _rust("validate_basis", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
errors: list[str] = []
|
||||
if basis.get("schema_version") != SCHEMA_VERSION:
|
||||
errors.append(f"schema_version {basis.get('schema_version')!r}")
|
||||
@@ -152,12 +189,18 @@ def validate_basis(basis: dict) -> list[str]:
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
out = _rust("is_qualified", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return not validate_basis(basis)
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
"""The evidence_ref string a contract carries to point at this basis
|
||||
(durable pointer, not a copy — stores stay independent)."""
|
||||
out = _rust("attach_evidence_ref", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return (f"build_basis:{basis.get('issue_ref') or 'adhoc'}:"
|
||||
f"{basis.get('created_at')}")
|
||||
|
||||
@@ -165,6 +208,9 @@ def attach_evidence_ref(basis: dict) -> str:
|
||||
def summary(basis: dict) -> dict:
|
||||
"""Compact projection for prompts/previews (no private reasoning —
|
||||
structured conclusions only, rule 18G)."""
|
||||
out = _rust("summary", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return {
|
||||
"schema_version": basis.get("schema_version"),
|
||||
"question": basis.get("question"),
|
||||
@@ -193,6 +239,11 @@ def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
|
||||
explicit resolution evidence ('discovery resolved', 'prior-art
|
||||
resolved', 'unnecessary') — never a bare skip."""
|
||||
out = _rust("revise_depth", basis, new_depth=new_depth, reason=reason)
|
||||
if out is not _MISSING:
|
||||
basis.clear()
|
||||
basis.update(out["basis"])
|
||||
return out["revision"]
|
||||
if new_depth not in DEPTH_TIERS:
|
||||
raise ValueError(f"unknown depth {new_depth!r}")
|
||||
original = basis["depth"]["depth"]
|
||||
@@ -216,11 +267,17 @@ def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
out = _rust("to_json", basis)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
import json
|
||||
return json.dumps(basis, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
def from_json(text: str) -> dict:
|
||||
out = _rust("from_json", text)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
import json
|
||||
return json.loads(text)
|
||||
|
||||
@@ -248,6 +305,10 @@ def generate_research_plan(*, intent: str, target: str = "",
|
||||
risks of THIS intent — the function has no topic vocabulary and no
|
||||
permanent category list. Depth, when not supplied, is chosen by
|
||||
consequence heuristics recorded in the plan (why is always present)."""
|
||||
out = _rust("generate_research_plan", intent, target, current_reality,
|
||||
unknowns, decisions, risks, depth)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
unknowns = [u for u in (unknowns or []) if u.strip()]
|
||||
decisions = [d for d in (decisions or []) if d.strip()]
|
||||
risks = [r for r in (risks or []) if r.strip()]
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.209.0"' in rune_version,
|
||||
'__version__ = "0.229.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -15,6 +15,7 @@ the history list.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -28,6 +29,22 @@ def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _kernel():
|
||||
"""The Rust ledger core (cl_loader) when RUNE_LEDGER_CORE allows it,
|
||||
else None — every public function delegates there first and falls
|
||||
back to the Python body below (same process, same semantics; the
|
||||
kernel returns the oracle output verbatim plus the updated doc)."""
|
||||
if os.environ.get("RUNE_LEDGER_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cl_loader
|
||||
if cl_loader.available():
|
||||
return cl_loader
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class CapabilityLedger:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "capability_ledger.json"
|
||||
@@ -37,13 +54,26 @@ class CapabilityLedger:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.new(_now())["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return {"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capabilities": {}, "history": [],
|
||||
"updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
self.doc = k.save(self.doc, _now())["doc"]
|
||||
except Exception:
|
||||
self.doc["updated_at"] = _now()
|
||||
else:
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
|
||||
@@ -61,6 +91,23 @@ class CapabilityLedger:
|
||||
generation: str = "") -> dict:
|
||||
"""Create a capability record (auto-invoked by Scaffolding when a
|
||||
meaningful capability is created — rule 19Q)."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.define(self.doc, _now(), capability_id=capability_id,
|
||||
name=name, purpose=purpose,
|
||||
owning_project=owning_project,
|
||||
component=component,
|
||||
source_locations=source_locations,
|
||||
contract=contract, state_touched=state_touched,
|
||||
dependencies=dependencies, consumers=consumers,
|
||||
invariants=invariants,
|
||||
behaviour_specs=behaviour_specs,
|
||||
constraints=constraints, generation=generation)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass # kernel err (or absent) — Python body re-raises properly
|
||||
cid = f"capability:{capability_id}"
|
||||
if cid in self.doc["capabilities"]:
|
||||
raise ValueError(f"capability exists: {cid}")
|
||||
@@ -88,6 +135,12 @@ class CapabilityLedger:
|
||||
return rec
|
||||
|
||||
def get(self, capability_id: str) -> dict | None:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.get(self.doc, capability_id)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return self.doc["capabilities"].get(
|
||||
capability_id if ":" in capability_id
|
||||
else f"capability:{capability_id}")
|
||||
@@ -98,6 +151,16 @@ class CapabilityLedger:
|
||||
"""Record a material change (auto-invoked after construction —
|
||||
rule 19Q.2). History entry keeps traceability; dependents' harden
|
||||
state is the behaviour ledger's job (via structural closure)."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.material_change(self.doc, _now(), capability_id,
|
||||
note=note, generation=generation,
|
||||
source_locations=source_locations)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -124,6 +187,14 @@ class CapabilityLedger:
|
||||
def move(self, capability_id: str, new_locations: list[str]) -> dict:
|
||||
"""File move: STABLE IDENTITY SURVIVES (rule 19S-6); the move is
|
||||
recorded, history preserved."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.move(self.doc, _now(), capability_id, new_locations)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -137,6 +208,15 @@ class CapabilityLedger:
|
||||
|
||||
def supersede(self, capability_id: str, by: str, *,
|
||||
note: str = "") -> dict:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.supersede(self.doc, _now(), capability_id, by,
|
||||
note=note)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -150,6 +230,14 @@ class CapabilityLedger:
|
||||
return rec
|
||||
|
||||
def retire(self, capability_id: str, *, note: str = "") -> dict:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.retire(self.doc, _now(), capability_id, note=note)
|
||||
self.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -163,15 +251,33 @@ class CapabilityLedger:
|
||||
# -- queries (surgical context, 19E) -----------------------------------
|
||||
|
||||
def active(self) -> list[dict]:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.active(self.doc)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return [r for r in self.doc["capabilities"].values()
|
||||
if r["maturity"] not in ("retired", "superseded")]
|
||||
|
||||
def by_component(self, component: str) -> list[dict]:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.by_component(self.doc, component)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
return [r for r in self.active() if r["component"] == component]
|
||||
|
||||
def context_packet(self, capability_id: str) -> dict:
|
||||
"""The LOCAL context packet (19E): everything a fresh builder
|
||||
needs for one capability — bounded, structured, no reasoning."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.context_packet(self.doc, capability_id)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
return {"error": f"unknown capability {capability_id!r}"}
|
||||
@@ -227,6 +333,16 @@ def classify_version_bump(*, contract_changed: bool,
|
||||
implementation_only: bool) -> str:
|
||||
"""§21D-3: derive PATCH/MINOR/MAJOR from the change shape. The model
|
||||
may propose; the machinery verifies."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.classify_version_bump(
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
if consumer_break:
|
||||
return "MAJOR"
|
||||
if contract_changed:
|
||||
@@ -241,6 +357,12 @@ def classify_version_bump(*, contract_changed: bool,
|
||||
|
||||
|
||||
def bump_version(version: str, kind: str) -> str:
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.bump_version(version, kind)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
m = SEMVER_RE.match(version)
|
||||
if not m:
|
||||
raise SpecError(f"invalid semver {version!r}")
|
||||
@@ -267,6 +389,21 @@ def attach_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
the starting version is explicit — 1.0.0 only for stable contracts
|
||||
with sufficient evidence; otherwise 0.x with a reason."""
|
||||
from rune.capability_ledger import LEDGER_SCHEMA_VERSION # noqa: F401
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.attach_spec(ledger.doc, _now(), capability_id,
|
||||
requirements=requirements,
|
||||
expected_behaviours=expected_behaviours,
|
||||
non_goals=non_goals,
|
||||
acceptance_criteria=acceptance_criteria,
|
||||
baseline_version=baseline_version,
|
||||
baseline_reason=baseline_reason,
|
||||
governance_version_started_at=governance_version_started_at)
|
||||
ledger.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -307,6 +444,23 @@ def revise_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
The model proposes; the machinery classifies and records. Material
|
||||
change without a bump is impossible (every revise computes one);
|
||||
unjustified bumps are impossible (no-change revise raises)."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
out = k.revise_spec(ledger.doc, _now(), capability_id,
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only,
|
||||
reason=reason,
|
||||
changed_sections=changed_sections,
|
||||
affected_consumers=affected_consumers,
|
||||
validation_consequences=validation_consequences,
|
||||
proposed_kind=proposed_kind)
|
||||
ledger.doc = out["doc"]
|
||||
return out["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
@@ -357,6 +511,12 @@ def spec_drift_status(ledger: "CapabilityLedger",
|
||||
→ SPEC_VERSION_DRIFT + durable obligation
|
||||
|
||||
Returns {"drift": bool, "obligation": str|None, "detail": ...}."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.spec_drift_status(ledger.doc, capability_id)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
return {"drift": False, "obligation": None,
|
||||
@@ -389,6 +549,12 @@ def spec_drift_status(ledger: "CapabilityLedger",
|
||||
def spec_context(rec: dict) -> dict:
|
||||
"""The spec half of a surgical context packet (§21M): WHY + WHAT
|
||||
SHOULD EXIST, compact."""
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.spec_context(rec)["value"]
|
||||
except Exception:
|
||||
pass
|
||||
s = rec.get("spec") or {}
|
||||
return {
|
||||
"capability_id": rec["capability_id"],
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust capability_ledger core (dichotomy factory
|
||||
batch, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cl_new({now, file_text?}) -> doc
|
||||
* cl_define({doc, now, ...define kwargs}) -> {"record", "doc"}
|
||||
* cl_get({doc, capability_id}) -> record|null
|
||||
* cl_material_change({doc, now, ...}) -> {"record", "doc"}
|
||||
* cl_move({doc, now, capability_id, new_locations}) -> {"record", "doc"}
|
||||
* cl_supersede({doc, now, capability_id, by, note?}) -> {"record", "doc"}
|
||||
* cl_retire({doc, now, capability_id, note?}) -> {"record", "doc"}
|
||||
* cl_active({doc}) / cl_by_component({doc, component}) -> [records]
|
||||
* cl_context_packet({doc, capability_id}) -> packet dict
|
||||
* cl_save({doc, now}) -> doc (stamped)
|
||||
* cl_classify_version_bump({4 bools}) -> "PATCH"|"MINOR"|"MAJOR"
|
||||
* cl_bump_version({version, kind}) -> new version
|
||||
* cl_attach_spec({doc, now, ...}) -> {"spec", "doc"}
|
||||
* cl_revise_spec({doc, now, ...}) -> {"result", "doc"}
|
||||
* cl_spec_drift_status({doc, capability_id}) -> status dict
|
||||
* cl_spec_context({rec}) -> packet dict
|
||||
|
||||
The doc is the FULL ledger document ({"schema_version", "capabilities",
|
||||
"history", "updated_at"}); mutators return the updated doc so the facade
|
||||
keeps authoritative state. Free: capability_scoring's cs_free_string
|
||||
(shared process) — NEVER re-export. Override the .so with
|
||||
LEDGER_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "capability_ledger"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("LEDGER_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcapability_ledger.so")
|
||||
)
|
||||
|
||||
SYMBOLS = (
|
||||
"cl_new", "cl_define", "cl_get", "cl_material_change", "cl_move",
|
||||
"cl_supersede", "cl_retire", "cl_active", "cl_by_component",
|
||||
"cl_context_packet", "cl_save", "cl_classify_version_bump",
|
||||
"cl_bump_version", "cl_attach_spec", "cl_revise_spec",
|
||||
"cl_spec_drift_status", "cl_spec_context",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class LedgerCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust ledger 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 LedgerCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict) -> dict:
|
||||
"""Returns the FULL success envelope: {"value": ..., "doc": ...} for
|
||||
mutators (the updated ledger doc travels beside the oracle output so
|
||||
the facade can thread self.doc), {"value": ...} for queries. Errors
|
||||
raise RuntimeError(err) — the facade's except-path falls back to the
|
||||
Python body, which re-raises the proper exception type."""
|
||||
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
|
||||
|
||||
|
||||
def new(now: str, file_text: str | None = None) -> dict:
|
||||
payload: dict = {"now": now}
|
||||
if file_text is not None:
|
||||
payload["file_text"] = file_text
|
||||
return _call("cl_new", payload)
|
||||
|
||||
|
||||
def define(doc: dict, now: str, **kwargs) -> dict:
|
||||
return _call("cl_define", {"doc": doc, "now": now, **kwargs})
|
||||
|
||||
|
||||
def get(doc: dict, capability_id: str) -> dict | None:
|
||||
return _call("cl_get", {"doc": doc, "capability_id": capability_id})
|
||||
|
||||
|
||||
def material_change(doc: dict, now: str, capability_id: str, note: str,
|
||||
generation: str = "", source_locations=None) -> dict:
|
||||
payload = {"doc": doc, "now": now, "capability_id": capability_id,
|
||||
"note": note, "generation": generation}
|
||||
if source_locations is not None:
|
||||
payload["source_locations"] = source_locations
|
||||
return _call("cl_material_change", payload)
|
||||
|
||||
|
||||
def move(doc: dict, now: str, capability_id: str, new_locations: list) -> dict:
|
||||
return _call("cl_move", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id,
|
||||
"new_locations": new_locations})
|
||||
|
||||
|
||||
def supersede(doc: dict, now: str, capability_id: str, by: str,
|
||||
note: str = "") -> dict:
|
||||
return _call("cl_supersede", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id,
|
||||
"by": by, "note": note})
|
||||
|
||||
|
||||
def retire(doc: dict, now: str, capability_id: str, note: str = "") -> dict:
|
||||
return _call("cl_retire", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id, "note": note})
|
||||
|
||||
|
||||
def active(doc: dict) -> list:
|
||||
return _call("cl_active", {"doc": doc})
|
||||
|
||||
|
||||
def by_component(doc: dict, component: str) -> list:
|
||||
return _call("cl_by_component", {"doc": doc, "component": component})
|
||||
|
||||
|
||||
def context_packet(doc: dict, capability_id: str) -> dict:
|
||||
return _call("cl_context_packet", {"doc": doc, "capability_id": capability_id})
|
||||
|
||||
|
||||
def save(doc: dict, now: str) -> dict:
|
||||
return _call("cl_save", {"doc": doc, "now": now})
|
||||
|
||||
|
||||
def classify_version_bump(*, contract_changed: bool, behaviour_added: bool,
|
||||
consumer_break: bool,
|
||||
implementation_only: bool) -> str:
|
||||
return _call("cl_classify_version_bump", {
|
||||
"contract_changed": contract_changed,
|
||||
"behaviour_added": behaviour_added,
|
||||
"consumer_break": consumer_break,
|
||||
"implementation_only": implementation_only,
|
||||
})
|
||||
|
||||
|
||||
def bump_version(version: str, kind: str) -> str:
|
||||
return _call("cl_bump_version", {"version": version, "kind": kind})
|
||||
|
||||
|
||||
def attach_spec(doc: dict, now: str, capability_id: str, **kwargs) -> dict:
|
||||
return _call("cl_attach_spec", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id, **kwargs})
|
||||
|
||||
|
||||
def revise_spec(doc: dict, now: str, capability_id: str, **kwargs) -> dict:
|
||||
return _call("cl_revise_spec", {"doc": doc, "now": now,
|
||||
"capability_id": capability_id, **kwargs})
|
||||
|
||||
|
||||
def spec_drift_status(doc: dict, capability_id: str) -> dict:
|
||||
return _call("cl_spec_drift_status", {"doc": doc,
|
||||
"capability_id": capability_id})
|
||||
|
||||
|
||||
def spec_context(rec: dict) -> dict:
|
||||
return _call("cl_spec_context", {"rec": rec})
|
||||
@@ -9,6 +9,13 @@ it belongs to — or proposes a new topic when nothing fits.
|
||||
|
||||
No preset categories anywhere: the seed set is empty, and every
|
||||
category in the topology was earned by co-firing.
|
||||
|
||||
Dichotomy wave #2479 tranche (contract
|
||||
BUILDER-2026-10-11T23:55:54Z-982aa3-factory-wave): the deterministic
|
||||
topology kernel is ported to FULL Rust (src/rust/concept_topology,
|
||||
parity oracle frozen_ct_truth.json). This facade delegates to the Rust
|
||||
core via rune/ct_loader when built; the Python implementation below is
|
||||
the strangler fallback, pinned with RUNE_CT_CORE=python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,9 +24,28 @@ DECAY_PER_TICK = 0.95
|
||||
CLUSTER_THRESHOLD = 2.0
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust concept_topology core when available (strangler: Python
|
||||
fallback is load-bearing). RUNE_CT_CORE=python pins the fallback."""
|
||||
import os
|
||||
if os.environ.get("RUNE_CT_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ct_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return ct_loader if ct_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def cofire(topology: dict, topics: list) -> dict:
|
||||
"""Record one co-firing event: every pair in `topics` gains an edge
|
||||
(weight +1). Topics are created on first sight (no preset list)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.cofire(topology, topics)
|
||||
topology = dict(topology)
|
||||
edges = dict(topology.get("edges", {}))
|
||||
seen = dict(topology.get("seen", {}))
|
||||
@@ -36,6 +62,9 @@ def cofire(topology: dict, topics: list) -> dict:
|
||||
|
||||
def decay(topology: dict) -> dict:
|
||||
"""Decay all edges one tick (stale associations weaken)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.decay(topology)
|
||||
topology = dict(topology)
|
||||
edges = {k: v * DECAY_PER_TICK for k, v in topology.get("edges", {}).items()
|
||||
if v * DECAY_PER_TICK >= 0.5}
|
||||
@@ -45,6 +74,9 @@ def decay(topology: dict) -> dict:
|
||||
|
||||
def clusters(topology: dict, threshold: float = CLUSTER_THRESHOLD) -> list:
|
||||
"""Emergent clusters: connected components over edges >= threshold."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.clusters(topology, threshold)
|
||||
edges = {k: v for k, v in topology.get("edges", {}).items()
|
||||
if v >= threshold}
|
||||
parent: dict = {}
|
||||
@@ -73,6 +105,9 @@ def clusters(topology: dict, threshold: float = CLUSTER_THRESHOLD) -> list:
|
||||
|
||||
def suggest(topology: dict, fact_topics: list) -> dict:
|
||||
"""Suggest the cluster a new fact belongs to — or a new topic."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.suggest(topology, fact_topics)
|
||||
cls = clusters(topology)
|
||||
best, best_overlap = None, 0
|
||||
for c in cls:
|
||||
@@ -87,4 +122,7 @@ def suggest(topology: dict, fact_topics: list) -> dict:
|
||||
|
||||
def new_topology() -> dict:
|
||||
"""The empty seed: no preset categories."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.new_topology()
|
||||
return {"edges": {}, "seen": {}}
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ct_loader.py — ctypes loader for the Rust concept_topology core
|
||||
(dichotomy wave #2479 tranche, contract
|
||||
BUILDER-2026-10-11T23:55:54Z-982aa3-factory-wave). Mirrors broker_loader.py:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ct_cofire({"topology", "topics"}) -> topology dict
|
||||
* ct_decay({"topology"}) -> topology dict
|
||||
* ct_clusters({"topology", "threshold"?}) -> cluster list
|
||||
* ct_suggest({"topology", "fact_topics"}) -> suggestion dict
|
||||
* ct_new_topology({}) -> empty topology
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). NEVER re-export
|
||||
a free symbol here. Override the .so with CT_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" / "concept_topology"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("CT_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconcept_topology.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ConceptTopologyUnavailable(FileNotFoundError):
|
||||
"""The Rust topology core is not built. Callers fall back to Python
|
||||
(strangler law; RUNE_CT_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 ConceptTopologyUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("ct_cofire", "ct_decay", "ct_clusters", "ct_suggest", "ct_new_topology"):
|
||||
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 cofire(topology: dict, topics: list) -> dict:
|
||||
return _call("ct_cofire", {"topology": topology, "topics": topics})
|
||||
|
||||
|
||||
def decay(topology: dict) -> dict:
|
||||
return _call("ct_decay", {"topology": topology})
|
||||
|
||||
|
||||
def clusters(topology: dict, threshold: float | None = None) -> list:
|
||||
payload = {"topology": topology}
|
||||
if threshold is not None:
|
||||
payload["threshold"] = threshold
|
||||
return _call("ct_clusters", payload)
|
||||
|
||||
|
||||
def suggest(topology: dict, fact_topics: list) -> dict:
|
||||
return _call("ct_suggest", {"topology": topology, "fact_topics": fact_topics})
|
||||
|
||||
|
||||
def new_topology() -> dict:
|
||||
return _call("ct_new_topology", {})
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust drift_cache core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* dcc_compute_priority({entry}) -> {priority}
|
||||
* dcc_qualifies_for_cache({entry}) -> {qualifies}
|
||||
* dcc_add({entries, entry_id, entry, now}) -> {entries, admitted, evicted}
|
||||
* dcc_access({entries, entry_id, now}) -> {entries, changed}
|
||||
* dcc_contains({entries, entry_id}) -> {contains}
|
||||
* dcc_get_priority({entries, entry_id}) -> {priority}
|
||||
* dcc_update_entries({entries, rows}) -> {entries}
|
||||
* dcc_get_stats({entries}) -> {stats}
|
||||
|
||||
No-clock law: `now` (aware UTC ISO-8601) is always an argument to the
|
||||
stamping calls — the kernel never reads the wall clock.
|
||||
|
||||
Boundary: the sqlite read (update_from_store's per-id SELECT — passed in
|
||||
as `rows`) and the cache FILE IO stay in the Python facade
|
||||
(drift_cache.py); the kernel is a pure function of their results.
|
||||
|
||||
The entries map is keyed by entry id (int in the facade). JSON object
|
||||
keys are strings, so this loader converts int->str on the way in and
|
||||
str->int on the way out (mirroring the oracle's `int(k)` _load coercion).
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char* — the default c_int truncates 64-bit pointers).
|
||||
Strings are freed via capability_scoring's cs_free_string — the kernel
|
||||
exports NO free symbol of its own (duplicate exports break the cdylib
|
||||
link when sibling crates share one process).
|
||||
|
||||
Override the .so with DC_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "drift_cache"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"DC_RUST_LIB", _CRATE_DIR / "target" / "release" / "libdrift_cache.so"
|
||||
)
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"dcc_compute_priority",
|
||||
"dcc_qualifies_for_cache",
|
||||
"dcc_add",
|
||||
"dcc_access",
|
||||
"dcc_contains",
|
||||
"dcc_get_priority",
|
||||
"dcc_update_entries",
|
||||
"dcc_get_stats",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class DriftCacheCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust drift_cache 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 DriftCacheCoreUnavailable(
|
||||
f"Rust drift_cache core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _entries_in(entries: dict | None) -> dict:
|
||||
return {str(k): v for k, v in (entries or {}).items()}
|
||||
|
||||
|
||||
def _entries_out(entries) -> dict:
|
||||
return {int(k): v for k, v in entries.items()}
|
||||
|
||||
|
||||
def compute_priority(entry: dict) -> float:
|
||||
return _call("dcc_compute_priority", {"entry": entry})["priority"]
|
||||
|
||||
|
||||
def qualifies_for_cache(entry: dict) -> bool:
|
||||
return _call("dcc_qualifies_for_cache", {"entry": entry})["qualifies"]
|
||||
|
||||
|
||||
def add(entries: dict, entry_id: int, entry: dict, now: str) -> dict:
|
||||
out = _call("dcc_add", {"entries": _entries_in(entries), "entry_id": entry_id,
|
||||
"entry": entry, "now": now})
|
||||
out["entries"] = _entries_out(out["entries"])
|
||||
return out
|
||||
|
||||
|
||||
def access(entries: dict, entry_id: int, now: str) -> dict:
|
||||
out = _call("dcc_access", {"entries": _entries_in(entries),
|
||||
"entry_id": entry_id, "now": now})
|
||||
out["entries"] = _entries_out(out["entries"])
|
||||
return out
|
||||
|
||||
|
||||
def contains(entries: dict, entry_id: int) -> bool:
|
||||
return _call("dcc_contains", {"entries": _entries_in(entries),
|
||||
"entry_id": entry_id})["contains"]
|
||||
|
||||
|
||||
def get_priority(entries: dict, entry_id: int) -> float:
|
||||
return _call("dcc_get_priority", {"entries": _entries_in(entries),
|
||||
"entry_id": entry_id})["priority"]
|
||||
|
||||
|
||||
def update_entries(entries: dict, rows: dict) -> dict:
|
||||
return _entries_out(_call("dcc_update_entries", {
|
||||
"entries": _entries_in(entries), "rows": rows})["entries"])
|
||||
|
||||
|
||||
def get_stats(entries: dict) -> dict:
|
||||
return _call("dcc_get_stats", {"entries": _entries_in(entries)})["stats"]
|
||||
+67
-2
@@ -46,6 +46,23 @@ CACHE_BOOST_ESTABLISHED = 1.5 # multiplier for established entries
|
||||
from rune.drift_core import _ms_core # noqa: E402 (no cycle)
|
||||
|
||||
|
||||
def _dc_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_DC_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the priority /
|
||||
qualification / admission / eviction / stats state machine; the
|
||||
sqlite read and the cache FILE IO stay here."""
|
||||
if os.environ.get("RUNE_DC_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import dc_loader
|
||||
|
||||
if dc_loader.available():
|
||||
return dc_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _py_compute_priority(entry: dict) -> float:
|
||||
"""Compute cache priority: wash_count × importance × status boost."""
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
@@ -73,7 +90,11 @@ def _py_qualifies_for_cache(entry: dict) -> bool:
|
||||
|
||||
|
||||
def compute_priority(entry: dict) -> float:
|
||||
"""Module-level priority (Rust core when built)."""
|
||||
"""Module-level priority (drift_cache Rust core, then the legacy
|
||||
drift-core chain, then the Python fallback)."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
return core.compute_priority(entry)
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.compute_priority(entry)
|
||||
@@ -81,7 +102,11 @@ def compute_priority(entry: dict) -> float:
|
||||
|
||||
|
||||
def qualifies_for_cache(entry: dict) -> bool:
|
||||
"""Module-level qualification (Rust core when built)."""
|
||||
"""Module-level qualification (drift_cache Rust core, then the legacy
|
||||
drift-core chain, then the Python fallback)."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
return core.qualifies_for_cache(entry)
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.qualifies_for_cache(entry)
|
||||
@@ -122,6 +147,16 @@ class DriftCache:
|
||||
- wash_count >= MIN_WASH_FOR_CACHE (proven through use), OR
|
||||
- importance >= 0.8 (high importance, worth caching even if untested)
|
||||
"""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
out = core.add(
|
||||
self.entries, entry_id, entry, datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
if out["admitted"]:
|
||||
self.entries = out["entries"]
|
||||
self._save()
|
||||
return
|
||||
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
if not qualifies_for_cache(entry):
|
||||
return # Not proven enough and not important enough
|
||||
@@ -144,6 +179,15 @@ class DriftCache:
|
||||
|
||||
def access(self, entry_id: int):
|
||||
"""Record a cache access (reinforces the entry)."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
out = core.access(
|
||||
self.entries, entry_id, datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
if out["changed"]:
|
||||
self.entries = out["entries"]
|
||||
self._save()
|
||||
return
|
||||
if entry_id in self.entries:
|
||||
self.entries[entry_id]["last_accessed"] = datetime.now(timezone.utc).isoformat()
|
||||
self.entries[entry_id]["access_count"] = self.entries[entry_id].get("access_count", 0) + 1
|
||||
@@ -151,10 +195,16 @@ class DriftCache:
|
||||
|
||||
def contains(self, entry_id: int) -> bool:
|
||||
"""Check if an entry is in the cache."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
return core.contains(self.entries, entry_id)
|
||||
return entry_id in self.entries
|
||||
|
||||
def get_priority(self, entry_id: int) -> float:
|
||||
"""Get the cache priority of an entry."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
return core.get_priority(self.entries, entry_id)
|
||||
if entry_id in self.entries:
|
||||
return self.entries[entry_id].get("priority", 0.0)
|
||||
return 0.0
|
||||
@@ -169,6 +219,18 @@ class DriftCache:
|
||||
|
||||
def update_from_store(self, db):
|
||||
"""Sync cache with memory store — update priorities from current wash counts."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
rows = {}
|
||||
for entry_id in list(self.entries.keys()):
|
||||
row = db.execute(
|
||||
"SELECT wash_count, importance, rock_status FROM entries WHERE id = ?",
|
||||
(entry_id,),
|
||||
).fetchone()
|
||||
rows[str(entry_id)] = list(row) if row else None
|
||||
self.entries = core.update_entries(self.entries, rows)
|
||||
self._save()
|
||||
return
|
||||
for entry_id in list(self.entries.keys()):
|
||||
row = db.execute(
|
||||
"SELECT wash_count, importance, rock_status FROM entries WHERE id = ?", (entry_id,)
|
||||
@@ -189,6 +251,9 @@ class DriftCache:
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Get cache statistics."""
|
||||
core = _dc_core()
|
||||
if core is not None:
|
||||
return core.get_stats(self.entries)
|
||||
if not self.entries:
|
||||
return {
|
||||
"size": 0,
|
||||
|
||||
@@ -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,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust lexicon_evolution core (dichotomy wave
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* le_content_words({text}) -> words list
|
||||
* le_distinctive_terms({fact, corpus_texts}) -> sorted terms list
|
||||
* le_shared_distinctive({fact, existing_values, corpus_texts}) -> sorted shared list
|
||||
* le_coherence({fact, existing_values, corpus_texts}) -> verdict str
|
||||
* le_grounded({fact, source_texts, min_distinct}) -> bool
|
||||
* le_stem({word}) -> stem str
|
||||
* le_promote_eligible({proposal, existing, evidence_strength,
|
||||
min_strength, corpus_texts}) -> {eligible, reason}
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with LE_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" / "lexicon_evolution"
|
||||
LIB_PATH = Path(
|
||||
os.environ.get("LE_RUST_LIB", _CRATE_DIR / "target" / "release" / "liblexicon_evolution.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class LexiconUnavailable(FileNotFoundError):
|
||||
"""The Rust lexicon 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 LexiconUnavailable(str(LIB_PATH))
|
||||
lib = ctypes.CDLL(str(LIB_PATH))
|
||||
for sym in ("le_content_words", "le_distinctive_terms", "le_shared_distinctive",
|
||||
"le_coherence", "le_grounded", "le_stem", "le_promote_eligible"):
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.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 content_words(text) -> list:
|
||||
return _call("le_content_words", {"text": text})["words"]
|
||||
|
||||
|
||||
def distinctive_terms(fact, corpus_texts) -> list:
|
||||
return _call("le_distinctive_terms", {
|
||||
"fact": fact, "corpus_texts": list(corpus_texts),
|
||||
})["terms"]
|
||||
|
||||
|
||||
def shared_distinctive(fact, existing_values, corpus_texts) -> list:
|
||||
return _call("le_shared_distinctive", {
|
||||
"fact": fact, "existing_values": list(existing_values),
|
||||
"corpus_texts": list(corpus_texts),
|
||||
})["shared"]
|
||||
|
||||
|
||||
def coherence(fact, existing_values, corpus_texts=()) -> str:
|
||||
return _call("le_coherence", {
|
||||
"fact": fact, "existing_values": list(existing_values),
|
||||
"corpus_texts": list(corpus_texts),
|
||||
})["verdict"]
|
||||
|
||||
|
||||
def grounded(fact, source_texts, min_distinct=1) -> bool:
|
||||
return _call("le_grounded", {
|
||||
"fact": fact, "source_texts": list(source_texts),
|
||||
"min_distinct": min_distinct,
|
||||
})["grounded"]
|
||||
|
||||
|
||||
def stem(word) -> str:
|
||||
return _call("le_stem", {"word": word})["stem"]
|
||||
|
||||
|
||||
def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
|
||||
corpus_texts=()) -> dict:
|
||||
return _call("le_promote_eligible", {
|
||||
"proposal": proposal, "existing": list(existing),
|
||||
"evidence_strength": evidence_strength, "min_strength": min_strength,
|
||||
"corpus_texts": list(corpus_texts),
|
||||
})
|
||||
@@ -36,6 +36,11 @@ Decisions:
|
||||
|
||||
Usage:
|
||||
from lexicon_evolution import coherence, grounded, promote_eligible
|
||||
|
||||
Dichotomy wave #2479: the pure lexical decisions run through the FULL Rust
|
||||
core (src/rust/lexicon_evolution) when built; RUNE_LE_CORE=python pins the
|
||||
Python fallback — parity harnesses. The _py_ variants are the load-bearing
|
||||
fallback + parity oracle; the ctypes seam is rune.le_loader.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -67,7 +72,34 @@ _NEGATION = {"not", "never", "don't", "doesn't", "cannot", "can't", "no",
|
||||
_DISTINCTIVE_DF = 0.05 # same rare-word threshold as recall/associations
|
||||
|
||||
|
||||
def _le_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_LE_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the entire pure
|
||||
lexical decision surface (tokenize, stem, distinctiveness, verdicts)."""
|
||||
if os.environ.get("RUNE_LE_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import le_loader
|
||||
except ImportError:
|
||||
try:
|
||||
import le_loader
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
return le_loader if le_loader.available() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def content_words(text):
|
||||
"""Significant content words (length > 3, not stop/framing)."""
|
||||
core = _le_core()
|
||||
if core is not None:
|
||||
return core.content_words(text)
|
||||
return _py_content_words(text)
|
||||
|
||||
|
||||
def _py_content_words(text):
|
||||
"""Significant content words (length > 3, not stop/framing)."""
|
||||
return [w for w in re.findall(r"[a-z]{4,}", (text or "").lower())
|
||||
if w not in _STOP and w not in _FRAMING]
|
||||
@@ -78,8 +110,16 @@ def distinctive_terms(fact, corpus_texts):
|
||||
|
||||
Same signal as the recall gate: a term common in the store ("black",
|
||||
"physics") is NOT a relation; a rare term ("celesta", "oatmilk") is."""
|
||||
core = _le_core()
|
||||
if core is not None:
|
||||
return set(core.distinctive_terms(fact, corpus_texts))
|
||||
return _py_distinctive_terms(fact, corpus_texts)
|
||||
|
||||
|
||||
def _py_distinctive_terms(fact, corpus_texts):
|
||||
"""The terms in `fact` that are RARE across the corpus (< 5% df)."""
|
||||
total = max(len(corpus_texts), 1)
|
||||
fact_terms = set(content_words(fact))
|
||||
fact_terms = set(_py_content_words(fact))
|
||||
if not fact_terms:
|
||||
return set()
|
||||
# stem-aware document frequency (migrate/migration count as one term)
|
||||
@@ -94,7 +134,15 @@ def distinctive_terms(fact, corpus_texts):
|
||||
|
||||
def shared_distinctive(fact, existing_values, corpus_texts):
|
||||
"""Distinctive terms shared between `fact` and existing values."""
|
||||
distinct = distinctive_terms(fact, corpus_texts)
|
||||
core = _le_core()
|
||||
if core is not None:
|
||||
return set(core.shared_distinctive(fact, existing_values, corpus_texts))
|
||||
return _py_shared_distinctive(fact, existing_values, corpus_texts)
|
||||
|
||||
|
||||
def _py_shared_distinctive(fact, existing_values, corpus_texts):
|
||||
"""Distinctive terms shared between `fact` and existing values."""
|
||||
distinct = _py_distinctive_terms(fact, corpus_texts)
|
||||
if not distinct:
|
||||
return set()
|
||||
shared = set()
|
||||
@@ -114,11 +162,19 @@ def coherence(fact, existing_values, corpus_texts=()):
|
||||
|
||||
Returns "conflict" | "consistent" | "novel".
|
||||
"""
|
||||
core = _le_core()
|
||||
if core is not None:
|
||||
return core.coherence(fact, existing_values, corpus_texts)
|
||||
return _py_coherence(fact, existing_values, corpus_texts)
|
||||
|
||||
|
||||
def _py_coherence(fact, existing_values, corpus_texts=()):
|
||||
"""Classify a new value against existing ones, lexicon-aware."""
|
||||
values = [v for v in existing_values if v and v.strip()]
|
||||
if not values:
|
||||
return "novel"
|
||||
corpus = corpus_texts or values
|
||||
shared = shared_distinctive(fact, values, corpus)
|
||||
shared = _py_shared_distinctive(fact, values, corpus)
|
||||
if not shared:
|
||||
# No distinctive shared term: the value is a NEW axis. This is NOT a
|
||||
# contradiction (a shared common word like "work" is meaningless), and
|
||||
@@ -143,10 +199,18 @@ def grounded(fact, source_texts, min_distinct=1):
|
||||
using a light suffix fold — exact word identity is too strict for natural
|
||||
sources.
|
||||
"""
|
||||
core = _le_core()
|
||||
if core is not None:
|
||||
return core.grounded(fact, source_texts, min_distinct)
|
||||
return _py_grounded(fact, source_texts, min_distinct)
|
||||
|
||||
|
||||
def _py_grounded(fact, source_texts, min_distinct=1):
|
||||
"""Is `fact` grounded in real material via a distinctive shared term?"""
|
||||
srcs = [s for s in source_texts if s and s.strip()]
|
||||
if not srcs:
|
||||
return False
|
||||
fact_terms = set(content_words(fact))
|
||||
fact_terms = set(_py_content_words(fact))
|
||||
if not fact_terms:
|
||||
return False
|
||||
joined = " ".join(s.lower() for s in srcs)
|
||||
@@ -205,6 +269,17 @@ def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
|
||||
real absorbed material (the corpus), so arbitrary trivia ("pizza on
|
||||
fridays") cannot be promoted as a principle no matter the sighting count.
|
||||
"""
|
||||
core = _le_core()
|
||||
if core is not None:
|
||||
return core.promote_eligible(proposal, existing, evidence_strength,
|
||||
min_strength, corpus_texts)
|
||||
return _py_promote_eligible(proposal, existing, evidence_strength,
|
||||
min_strength, corpus_texts)
|
||||
|
||||
|
||||
def _py_promote_eligible(proposal, existing, evidence_strength, min_strength=3,
|
||||
corpus_texts=()):
|
||||
"""Is a persona/constitution promotion eligible, lexicon-aware?"""
|
||||
if evidence_strength < min_strength:
|
||||
return {"eligible": False,
|
||||
"reason": f"evidence strength {evidence_strength} < {min_strength}"}
|
||||
@@ -212,17 +287,17 @@ def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
|
||||
if not existing:
|
||||
return {"eligible": True, "reason": "empty block — first principle"}
|
||||
corpus = corpus_texts or existing
|
||||
shared = shared_distinctive(proposal, existing, corpus)
|
||||
shared = _py_shared_distinctive(proposal, existing, corpus)
|
||||
if shared:
|
||||
return {"eligible": True,
|
||||
"reason": f"shares distinctive term(s): {sorted(shared)[:3]}"}
|
||||
# No distinctive shared term: a NOVEL axis is eligible only if it is
|
||||
# GROUNDED in the absorbed corpus via a distinctive term (a real principle
|
||||
# names something real), and the proposal is not trivial framing.
|
||||
terms = content_words(proposal)
|
||||
terms = _py_content_words(proposal)
|
||||
if not terms:
|
||||
return {"eligible": False, "reason": "no lexical signal"}
|
||||
if grounded(proposal, corpus, min_distinct=1):
|
||||
if _py_grounded(proposal, corpus, min_distinct=1):
|
||||
return {"eligible": True,
|
||||
"reason": f"novel axis grounded in corpus ({len(terms)} terms)"}
|
||||
return {"eligible": False,
|
||||
|
||||
+113
-5
@@ -32,6 +32,24 @@ KINDS = {"message", "reply", "tool_call", "tool_result", "revision", "branch_hea
|
||||
EDGE_TYPES = {"parent", "reply_to", "calls", "results_in", "revises", "supersedes", "branches_from"}
|
||||
|
||||
|
||||
def _mg_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_MG_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the graph state
|
||||
machine (load parsing + schema gate, node construction, revise/branch
|
||||
wiring, lineage/history walks, flat migration, cues); the uuid/clock
|
||||
seams, the save FILE IO and the json.dumps renderings stay here."""
|
||||
if os.environ.get("RUNE_MG_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import mg_loader
|
||||
|
||||
if mg_loader.available():
|
||||
return mg_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
@@ -48,6 +66,18 @@ class MessageGraph:
|
||||
|
||||
# -- persistence ----------------------------------------------------
|
||||
def _load(self) -> None:
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
text = None
|
||||
if self.path.exists():
|
||||
try:
|
||||
text = self.path.read_text()
|
||||
except OSError:
|
||||
text = None
|
||||
state = core.load(text)
|
||||
self.nodes = state["nodes"]
|
||||
self.order = state["order"]
|
||||
return
|
||||
if not self.path.exists():
|
||||
return
|
||||
try:
|
||||
@@ -61,8 +91,13 @@ class MessageGraph:
|
||||
self.order.append(n["id"])
|
||||
|
||||
def save(self) -> None:
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
doc = core.save({"nodes": self.nodes, "order": self.order})
|
||||
else:
|
||||
doc = {"schema": SCHEMA, "nodes": [self.nodes[i] for i in self.order]}
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps({"schema": SCHEMA, "nodes": [self.nodes[i] for i in self.order]}, indent=2))
|
||||
self.path.write_text(json.dumps(doc, indent=2))
|
||||
|
||||
# -- append ---------------------------------------------------------
|
||||
def add(
|
||||
@@ -73,6 +108,15 @@ class MessageGraph:
|
||||
raise ValueError(f"unknown kind {kind!r}")
|
||||
if edge not in EDGE_TYPES:
|
||||
raise ValueError(f"unknown edge {edge!r}")
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
out = core.add(
|
||||
{"nodes": self.nodes, "order": self.order},
|
||||
uuid.uuid4().hex[:12], _now(), role, content, kind, parent, edge,
|
||||
)
|
||||
self.nodes = out["graph"]["nodes"]
|
||||
self.order = out["graph"]["order"]
|
||||
return out["node"]
|
||||
node = {
|
||||
"id": uuid.uuid4().hex[:12],
|
||||
"kind": kind,
|
||||
@@ -88,21 +132,49 @@ class MessageGraph:
|
||||
return node
|
||||
|
||||
def tool_call(self, name: str, args: dict, parent: str | None = None) -> dict:
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
out = core.tool_call(
|
||||
{"nodes": self.nodes, "order": self.order},
|
||||
uuid.uuid4().hex[:12], _now(),
|
||||
json.dumps({"call": name, "args": args}), parent,
|
||||
)
|
||||
self.nodes = out["graph"]["nodes"]
|
||||
self.order = out["graph"]["order"]
|
||||
return out["node"]
|
||||
return self.add("tool", json.dumps({"call": name, "args": args}), kind="tool_call", parent=parent, edge="calls")
|
||||
|
||||
def tool_result(self, call_id: str, result: str) -> dict:
|
||||
call = self.nodes.get(call_id)
|
||||
if call is None:
|
||||
if call_id not in self.nodes:
|
||||
raise KeyError(f"no such call {call_id!r}")
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
out = core.tool_result(
|
||||
{"nodes": self.nodes, "order": self.order},
|
||||
uuid.uuid4().hex[:12], _now(), call_id, result,
|
||||
)
|
||||
self.nodes = out["graph"]["nodes"]
|
||||
self.order = out["graph"]["order"]
|
||||
return out["node"]
|
||||
call = self.nodes[call_id]
|
||||
return self.add("tool", result, kind="tool_result", parent=call_id, edge="results_in")
|
||||
|
||||
# -- revision / branch ----------------------------------------------
|
||||
def revise(self, node_id: str, new_content: str) -> dict:
|
||||
"""Regenerate an answer without losing provenance: the old node is
|
||||
marked superseded; the revision carries the supersedes edge."""
|
||||
old = self.nodes.get(node_id)
|
||||
if old is None:
|
||||
if node_id not in self.nodes:
|
||||
raise KeyError(f"no such message {node_id!r}")
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
out = core.revise(
|
||||
{"nodes": self.nodes, "order": self.order},
|
||||
node_id, new_content, uuid.uuid4().hex[:12], _now(),
|
||||
)
|
||||
self.nodes = out["graph"]["nodes"]
|
||||
self.order = out["graph"]["order"]
|
||||
return out["node"]
|
||||
old = self.nodes[node_id]
|
||||
rev = self.add(old["role"], new_content, kind="revision", parent=old.get("parent"), edge="revises")
|
||||
rev["edges"].append({"type": "supersedes", "to": node_id})
|
||||
old["superseded_by"] = rev["id"]
|
||||
@@ -112,6 +184,15 @@ class MessageGraph:
|
||||
"""Start a new conversation branch off any message."""
|
||||
if node_id not in self.nodes:
|
||||
raise KeyError(f"no such message {node_id!r}")
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
out = core.branch(
|
||||
{"nodes": self.nodes, "order": self.order},
|
||||
node_id, uuid.uuid4().hex[:12], _now(),
|
||||
)
|
||||
self.nodes = out["graph"]["nodes"]
|
||||
self.order = out["graph"]["order"]
|
||||
return out["node"]
|
||||
return self.add("system", f"branched from {node_id}", kind="branch_head", parent=node_id, edge="branches_from")
|
||||
|
||||
# -- lineage context -------------------------------------------------
|
||||
@@ -123,6 +204,9 @@ class MessageGraph:
|
||||
node_id = self.order[-1] if self.order else None
|
||||
if node_id is None:
|
||||
return []
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
return core.lineage({"nodes": self.nodes, "order": self.order}, node_id)
|
||||
chain: list[dict] = []
|
||||
seen = set()
|
||||
cur = node_id
|
||||
@@ -142,6 +226,9 @@ class MessageGraph:
|
||||
|
||||
def history(self, node_id: str) -> list[dict]:
|
||||
"""Every revision of a message, oldest first (the provenance trail)."""
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
return core.history({"nodes": self.nodes, "order": self.order}, node_id)
|
||||
trail = []
|
||||
# find the root: walk supersedes edges backwards
|
||||
cur = node_id
|
||||
@@ -168,6 +255,24 @@ class MessageGraph:
|
||||
"""Schema migration: the flat {role, content} transcript becomes a
|
||||
chained graph (arrival order, parent links). Compatibility preserved —
|
||||
no conversation is lost in the upgrade."""
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
g = cls.__new__(cls)
|
||||
g.path = Path(path) if path else GRAPH_PATH
|
||||
text = None
|
||||
if g.path.exists():
|
||||
try:
|
||||
text = g.path.read_text()
|
||||
except OSError:
|
||||
text = None
|
||||
state = core.migrate_flat(
|
||||
text, list(transcript),
|
||||
[uuid.uuid4().hex[:12] for _ in transcript],
|
||||
[_now() for _ in transcript],
|
||||
)
|
||||
g.nodes = state["nodes"]
|
||||
g.order = state["order"]
|
||||
return g
|
||||
g = cls(path=path)
|
||||
for m in transcript:
|
||||
g.add(m.get("role", "system"), m.get("content", ""), kind="message")
|
||||
@@ -177,6 +282,9 @@ class MessageGraph:
|
||||
@staticmethod
|
||||
def cue(node: dict) -> str:
|
||||
"""One quiet relationship line per node (no visual clutter)."""
|
||||
core = _mg_core()
|
||||
if core is not None:
|
||||
return core.cue(node)
|
||||
for e in node.get("edges", []):
|
||||
if e["type"] == "supersedes":
|
||||
return f"replaces {e['to'][:8]}"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust message_graph core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* mg_load({"text": str|null}) -> graph wire {"nodes", "order"}
|
||||
* mg_save({"graph"}) -> save doc {"schema", "nodes"}
|
||||
* mg_add({graph, id, ts, role, content, kind, parent, edge}) -> {graph, node}
|
||||
* mg_tool_call({graph, id, ts, content, parent}) -> {graph, node}
|
||||
* mg_tool_result({graph, id, ts, call_id, result}) -> {graph, node}
|
||||
* mg_revise({graph, node_id, new_content, id, ts}) -> {graph, node}
|
||||
* mg_branch({graph, node_id, id, ts}) -> {graph, node}
|
||||
* mg_lineage({graph, node_id}) -> [nodes]
|
||||
* mg_history({graph, node_id}) -> [nodes]
|
||||
* mg_migrate_flat({text, transcript, ids, tss}) -> graph wire
|
||||
* mg_cue({"node"}) -> cue string
|
||||
|
||||
Graph wire: {"nodes": {id: node} (insertion-ordered), "order": [ids]}.
|
||||
Errors ride the envelope typed — {"ok": false, "err": msg, "type":
|
||||
"ValueError"|"KeyError"|"AttributeError"|"TypeError"} — and the loader
|
||||
raises the MATCHING Python exception (the oracle raises those; the parity
|
||||
harness compares exception types and messages, so the kernel's guard
|
||||
clauses must round-trip as the same Python error the oracle raises).
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with MG_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" / "message_graph"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("MG_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmessage_graph.so")
|
||||
)
|
||||
|
||||
_ERR_TYPES = {
|
||||
"ValueError": ValueError,
|
||||
"KeyError": KeyError,
|
||||
"AttributeError": AttributeError,
|
||||
"TypeError": TypeError,
|
||||
}
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class MessageGraphCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust message-graph 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 MessageGraphCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("mg_load", "mg_save", "mg_add", "mg_tool_call", "mg_tool_result",
|
||||
"mg_revise", "mg_branch", "mg_lineage", "mg_history",
|
||||
"mg_migrate_flat", "mg_cue"):
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.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"):
|
||||
etype = doc.get("type", "RuntimeError")
|
||||
raise _ERR_TYPES.get(etype, RuntimeError)(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def load(text: str | None) -> dict:
|
||||
return _call("mg_load", {"text": text})
|
||||
|
||||
|
||||
def save(graph: dict) -> dict:
|
||||
return _call("mg_save", {"graph": graph})
|
||||
|
||||
|
||||
def add(graph: dict, node_id: str, ts: str, role, content, kind: str,
|
||||
parent: str | None, edge: str) -> dict:
|
||||
return _call("mg_add", {"graph": graph, "id": node_id, "ts": ts, "role": role,
|
||||
"content": content, "kind": kind, "parent": parent,
|
||||
"edge": edge})
|
||||
|
||||
|
||||
def tool_call(graph: dict, node_id: str, ts: str, content: str,
|
||||
parent: str | None) -> dict:
|
||||
return _call("mg_tool_call", {"graph": graph, "id": node_id, "ts": ts,
|
||||
"content": content, "parent": parent})
|
||||
|
||||
|
||||
def tool_result(graph: dict, node_id: str, ts: str, call_id: str, result) -> dict:
|
||||
return _call("mg_tool_result", {"graph": graph, "id": node_id, "ts": ts,
|
||||
"call_id": call_id, "result": result})
|
||||
|
||||
|
||||
def revise(graph: dict, node_id: str, new_content, rev_id: str, ts: str) -> dict:
|
||||
return _call("mg_revise", {"graph": graph, "node_id": node_id,
|
||||
"new_content": new_content, "id": rev_id, "ts": ts})
|
||||
|
||||
|
||||
def branch(graph: dict, node_id: str, head_id: str, ts: str) -> dict:
|
||||
return _call("mg_branch", {"graph": graph, "node_id": node_id,
|
||||
"id": head_id, "ts": ts})
|
||||
|
||||
|
||||
def lineage(graph: dict, node_id: str | None) -> list:
|
||||
return _call("mg_lineage", {"graph": graph, "node_id": node_id})
|
||||
|
||||
|
||||
def history(graph: dict, node_id: str) -> list:
|
||||
return _call("mg_history", {"graph": graph, "node_id": node_id})
|
||||
|
||||
|
||||
def migrate_flat(text: str | None, transcript: list, ids: list, tss: list) -> dict:
|
||||
return _call("mg_migrate_flat", {"text": text, "transcript": transcript,
|
||||
"ids": ids, "tss": tss})
|
||||
|
||||
|
||||
def cue(node: dict) -> str:
|
||||
return _call("mg_cue", {"node": node})
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust outcome_ledger core (dichotomy wave, #2479
|
||||
tranche). Mirrors to_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* ol_run({op, db_path, record?, task_family?, model?}) ->
|
||||
{"ok": true, "value": ...} | {"ok": false, "err": "..."}
|
||||
|
||||
The kernel is RUST-side per the outcome_ledger intent ledger — it owns the
|
||||
ledger SQL (DDL, INSERT, the six-aggregate stats query) against the system
|
||||
libsqlite3. The facade keeps makedirs, the CLI and test_ledger.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process) — this crate
|
||||
deliberately exports NO free symbol of its own. Override the .so with
|
||||
OL_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" / "outcome_ledger"
|
||||
_LIB_PATH = Path(os.environ.get("OL_RUST_LIB", _CRATE_DIR / "target" / "release" / "liboutcome_ledger.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class OutcomeLedgerCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust outcome_ledger 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 OutcomeLedgerCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.ol_run.argtypes = [ctypes.c_void_p]
|
||||
lib.ol_run.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.ol_run(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("ol_run returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "ol_run failed"))
|
||||
return doc
|
||||
|
||||
|
||||
def run_op(op: str, db_path: str, *, record: dict | None = None,
|
||||
task_family: str | None = None, model: str | None = None) -> dict:
|
||||
"""Run one kernel op. Returns the envelope value (stats doc, null, ...)."""
|
||||
payload: dict = {"op": op, "db_path": db_path}
|
||||
if record is not None:
|
||||
payload["record"] = record
|
||||
if task_family is not None:
|
||||
payload["task_family"] = task_family
|
||||
if model is not None:
|
||||
payload["model"] = model
|
||||
return _call(payload)["value"]
|
||||
@@ -71,7 +71,13 @@ class OutcomeLedger:
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
"""Initialize the database."""
|
||||
"""Initialize the database (Rust core when built; makedirs stay
|
||||
Python — the adapter owns directory prep, the kernel owns the DDL)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
core.run_op("init_db", self.db_path)
|
||||
return
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
@@ -99,7 +105,12 @@ class OutcomeLedger:
|
||||
conn.close()
|
||||
|
||||
def record(self, outcome: OutcomeRecord):
|
||||
"""Record an outcome."""
|
||||
"""Record an outcome (Rust core when built — the INSERT runs in the
|
||||
kernel against the system libsqlite3)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
core.run_op("record", self.db_path, record=outcome.to_dict())
|
||||
return
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
@@ -179,16 +190,37 @@ class OutcomeLedger:
|
||||
}
|
||||
|
||||
def get_stats(self, task_family: str = None) -> dict:
|
||||
"""Get statistics for a task family."""
|
||||
"""Get statistics for a task family (Rust core when built — the
|
||||
six-aggregate query runs in the kernel)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
return core.run_op("get_stats", self.db_path, task_family=task_family)
|
||||
if task_family:
|
||||
return self._query_stats("WHERE task_family = ?", (task_family,))
|
||||
return self._query_stats("", ())
|
||||
|
||||
def get_model_stats(self, model: str) -> dict:
|
||||
"""Get statistics for a model."""
|
||||
"""Get statistics for a model (Rust core when built)."""
|
||||
core = _ol_core()
|
||||
if core is not None:
|
||||
return core.run_op("get_model_stats", self.db_path, model=model)
|
||||
return self._query_stats("WHERE model = ?", (model,))
|
||||
|
||||
|
||||
# ── Rust core delegation (dichotomy wave, #2479 tranche) ────────────────
|
||||
|
||||
def _ol_core():
|
||||
"""The Rust outcome_ledger core when available (strangler law).
|
||||
RUNE_OL_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_OL_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ol_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
return ol_loader if ol_loader.available() else None
|
||||
|
||||
|
||||
def test_ledger():
|
||||
"""Test the outcome ledger."""
|
||||
print("Testing Outcome Ledger...")
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pc_loader.py — ctypes loader for the Rust programme_continuation core
|
||||
(dichotomy wave, #2479 tranche). Mirrors ci_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* pc_canary({prog}) -> {"verdict", "next_action", "reason"}
|
||||
* pc_eligible({prog}) -> ["stage_id", ...] priority-first
|
||||
* pc_stage_to_dict({stage}) -> durable stage dict
|
||||
* pc_save_doc({prog, saved_at}) -> durable programme record doc
|
||||
* pc_load({doc | null}) -> programme dict or null
|
||||
* pc_run_programme({prog, max_transitions}, op_host)
|
||||
-> {"log", "states"}
|
||||
* pc_stage_run({cfg, stage}, op_host) -> {"state", "result"}
|
||||
* pc_run_construction({prog, cfg, max_transitions}, op_host)
|
||||
-> {"log", "states", "results"}
|
||||
|
||||
The drivers run FULL Rust; the seams the oracle leaves to the caller —
|
||||
the Scaffolding interface, the event bridge, the work callable, the
|
||||
stage runner, and the clock — cross back as JSON ops served by
|
||||
OpsProxy against the live Python objects. No free-string export here —
|
||||
callers free through capability_scoring's cs_free_string.
|
||||
|
||||
Override the .so with PC_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "programme_continuation"
|
||||
_LIB_PATH = Path(os.environ.get("PC_RUST_LIB",
|
||||
_CRATE_DIR / "target" / "release" /
|
||||
"libprogramme_continuation.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)
|
||||
|
||||
|
||||
class ProgrammeContinuationCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust programme-continuation core is not built. Callers fall
|
||||
back to the Python oracle."""
|
||||
|
||||
|
||||
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 ProgrammeContinuationCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for name in ("pc_run_programme", "pc_stage_run", "pc_run_construction"):
|
||||
fn = getattr(lib, name)
|
||||
fn.argtypes = [ctypes.c_void_p, OP_CB, ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
for name in ("pc_canary", "pc_eligible", "pc_stage_to_dict",
|
||||
"pc_save_doc", "pc_load"):
|
||||
fn = getattr(lib, name)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
# 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
|
||||
|
||||
|
||||
def _unwrap(ptr) -> dict:
|
||||
if not ptr:
|
||||
raise RuntimeError("pc core returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
_free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "pc core failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _call_simple(name: str, payload: dict):
|
||||
lib = _load()
|
||||
buf = ctypes.create_string_buffer(json.dumps(payload).encode())
|
||||
ptr = getattr(lib, name)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
return _unwrap(ptr)
|
||||
|
||||
|
||||
def _call_host(name: str, payload: dict, host):
|
||||
"""Drive a host-op FFI export: the kernel issues JSON ops, `host`
|
||||
serves each and the result returns through the envelope."""
|
||||
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 = host(op)
|
||||
except Exception as exc: # noqa: BLE001 — surfaced by the kernel
|
||||
result = {"__host_error__": str(exc)[:500]}
|
||||
data = json.dumps(_jsonable(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, name)(ctypes.cast(buf, ctypes.c_void_p), cb, None)
|
||||
return _unwrap(ptr)
|
||||
|
||||
|
||||
def _jsonable(v):
|
||||
return json.loads(json.dumps(v, default=str))
|
||||
|
||||
|
||||
def _entity_by_id(doc: dict, entity_id: str) -> dict | None:
|
||||
for e in (doc or {}).get("entities", []):
|
||||
if e.get("id") == entity_id:
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
class OpsProxy:
|
||||
"""Serves the kernel's JSON ops against the live Python objects —
|
||||
the mirror image of the truth generator's recording proxy."""
|
||||
|
||||
def __init__(self, iface=None, stages: dict | None = None, runner=None):
|
||||
self.iface = iface
|
||||
self.stages = stages or {}
|
||||
self.runner = runner
|
||||
self._doc = None
|
||||
|
||||
def __call__(self, op: dict):
|
||||
name = op.get("op")
|
||||
a = op.get("args") or {}
|
||||
if name == "scaffold_load":
|
||||
self._doc = self.iface.scaffold.load()
|
||||
return copy.deepcopy(self._doc)
|
||||
if name == "contract":
|
||||
c = _entity_by_id(self._doc, f"contract:{a['contract_id']}") \
|
||||
if self._doc else None
|
||||
return {"contract": copy.deepcopy(c) if c else None,
|
||||
"entity_id": (c or {}).get("id")}
|
||||
if name == "scaffold_save":
|
||||
return _jsonable(self.iface.scaffold.save(a["doc"]))
|
||||
if name == "transition":
|
||||
return _jsonable(self.iface.transition(
|
||||
a["contract_id"], a["phase"],
|
||||
evidence_ref=a.get("evidence_ref", "")))
|
||||
if name == "close":
|
||||
return _jsonable(self.iface.close(
|
||||
a["contract_id"], observed_evidence=a["observed_evidence"],
|
||||
scope_files=a.get("scope_files")))
|
||||
if name == "work":
|
||||
stage = self.stages[a["stage_id"]]
|
||||
w = stage.result.get("work")
|
||||
outcome = dict(w(self.iface, stage)) if callable(w) else dict(w)
|
||||
return {"outcome": _jsonable(outcome)}
|
||||
if name == "now":
|
||||
from rune import programme_continuation as pc_mod
|
||||
return {"now": pc_mod.time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
pc_mod.time.gmtime()),
|
||||
"epoch": pc_mod.time.time()}
|
||||
if name == "register_process_wait":
|
||||
from rune import event_wait as ew
|
||||
ew.register_process_wait(
|
||||
a["op"], contract_id=a["contract_id"], project=a["project"],
|
||||
objective=a["objective"], watched_for=a["watched_for"],
|
||||
watchdog_secs=a["watchdog_secs"])
|
||||
return None
|
||||
if name == "run_builder_under_events":
|
||||
from rune import event_wait as ew
|
||||
notifier = ew.run_builder_under_events(
|
||||
a["async_cmd"], a["op"], daemon_url=a["daemon_url"])
|
||||
return {"pid": notifier.pid}
|
||||
if name == "await_resume_event":
|
||||
from rune import event_wait as ew
|
||||
return _jsonable(ew.await_resume_event(
|
||||
a["op"], watchdog_secs=a["watchdog_secs"], pgid=a["pgid"]))
|
||||
if name == "run_stage":
|
||||
return {"state": self.runner(self.stages[a["stage_id"]])}
|
||||
if name == "save_programme":
|
||||
base = Path(a["base"])
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
# oracle byte format: json.dumps(..., indent=1) — reordered to
|
||||
# the oracle's key order first (serde maps are alphabetical)
|
||||
(base / f"{a['doc']['programme_id']}.json").write_text(
|
||||
json.dumps(_normalize_programme_doc(a["doc"]), indent=1))
|
||||
return None
|
||||
raise RuntimeError(f"pc kernel issued unknown op: {name}")
|
||||
|
||||
|
||||
def _normalize_programme_doc(doc: dict) -> dict:
|
||||
"""Reorder a kernel-built doc into the oracle's key order so the
|
||||
written bytes match json.dumps of the oracle's dict (indent=1)."""
|
||||
out = {
|
||||
"programme_id": doc.get("programme_id", ""),
|
||||
"objective": doc.get("objective", ""),
|
||||
"stages": [
|
||||
{"stage_id": s.get("stage_id", ""),
|
||||
"objective": s.get("objective", ""),
|
||||
"state": s.get("state", "PENDING"),
|
||||
"depends_on": list(s.get("depends_on") or []),
|
||||
"priority": s.get("priority", 0)}
|
||||
for s in doc.get("stages", [])
|
||||
],
|
||||
"fallbacks": list(doc.get("fallbacks") or []),
|
||||
"saved_at": doc.get("saved_at", ""),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def _normalize_contract_doc(doc: dict) -> dict:
|
||||
"""Checkpoint dicts the kernel appends are alphabetical; reorder to
|
||||
the oracle's {at, phase, evidence, note} before a real store save."""
|
||||
for e in (doc or {}).get("entities", []):
|
||||
cps = e.get("checkpoints")
|
||||
if isinstance(cps, list):
|
||||
e["checkpoints"] = [
|
||||
{"at": cp.get("at", ""), "phase": cp.get("phase", ""),
|
||||
"evidence": cp.get("evidence", ""), "note": cp.get("note", "")}
|
||||
if isinstance(cp, dict) else cp
|
||||
for cp in cps
|
||||
]
|
||||
return doc
|
||||
|
||||
|
||||
# ── payload builders ────────────────────────────────────────────────────
|
||||
|
||||
def prog_payload(prog, *, work_marker: bool = False) -> dict:
|
||||
return {
|
||||
"programme_id": prog.programme_id,
|
||||
"objective": prog.objective,
|
||||
"stages": [stage_payload(s, work_marker=work_marker) for s in prog.stages],
|
||||
"fallbacks": list(prog.fallbacks),
|
||||
}
|
||||
|
||||
|
||||
def stage_payload(s, *, work_marker: bool = False) -> dict:
|
||||
d = {
|
||||
"stage_id": s.stage_id,
|
||||
"objective": s.objective,
|
||||
"state": s.state,
|
||||
"depends_on": list(s.depends_on),
|
||||
"priority": s.priority,
|
||||
"result": {k: v for k, v in s.result.items() if k != "work"},
|
||||
}
|
||||
if work_marker and "work" in s.result:
|
||||
w = s.result["work"]
|
||||
# the kernel never reads work contents — presence is the signal;
|
||||
# the host resolves callables vs literal specs behind the work op
|
||||
d["result"]["work"] = ({"__callable__": True} if callable(w)
|
||||
else _jsonable(w))
|
||||
return d
|
||||
|
||||
|
||||
# ── pure exports ────────────────────────────────────────────────────────
|
||||
|
||||
def canary(prog) -> dict:
|
||||
return _call_simple("pc_canary", {"prog": prog_payload(prog)})
|
||||
|
||||
|
||||
def eligible(prog) -> list:
|
||||
return _call_simple("pc_eligible", {"prog": prog_payload(prog)})
|
||||
|
||||
|
||||
def stage_to_dict(s) -> dict:
|
||||
return _call_simple("pc_stage_to_dict", {"stage": stage_payload(s)})
|
||||
|
||||
|
||||
def save_doc(prog) -> dict:
|
||||
from rune import programme_continuation as pc_mod
|
||||
saved_at = pc_mod.time.strftime("%Y-%m-%dT%H:%M:%SZ", pc_mod.time.gmtime())
|
||||
doc = _call_simple("pc_save_doc",
|
||||
{"prog": prog_payload(prog), "saved_at": saved_at})
|
||||
return _normalize_programme_doc(doc)
|
||||
|
||||
|
||||
def load_doc(doc: dict | None):
|
||||
out = _call_simple("pc_load", {"doc": doc})
|
||||
return None if out is None else out
|
||||
|
||||
|
||||
# ── host-driven exports ─────────────────────────────────────────────────
|
||||
|
||||
def run_programme(prog, stage_runner, *, max_transitions: int = 100) -> dict:
|
||||
proxy = OpsProxy(stages={s.stage_id: s for s in prog.stages},
|
||||
runner=stage_runner)
|
||||
out = _call_host("pc_run_programme",
|
||||
{"prog": prog_payload(prog),
|
||||
"max_transitions": max_transitions}, proxy)
|
||||
out["log"] = [_normalize_log_entry(e) for e in out["log"]]
|
||||
return out
|
||||
|
||||
|
||||
def stage_run(stage, cfg: dict, iface) -> dict:
|
||||
proxy = OpsProxy(iface=iface, stages={stage.stage_id: stage})
|
||||
out = _call_host("pc_stage_run",
|
||||
{"cfg": cfg, "stage": stage_payload(stage,
|
||||
work_marker=True)},
|
||||
proxy)
|
||||
return out
|
||||
|
||||
|
||||
def run_construction(prog, cfg: dict, iface, *,
|
||||
max_transitions: int = 100) -> dict:
|
||||
proxy = OpsProxy(iface=iface,
|
||||
stages={s.stage_id: s for s in prog.stages})
|
||||
out = _call_host("pc_run_construction",
|
||||
{"prog": prog_payload(prog, work_marker=True), "cfg": cfg,
|
||||
"max_transitions": max_transitions}, proxy)
|
||||
out["log"] = [_normalize_log_entry(e) for e in out["log"]]
|
||||
return out
|
||||
|
||||
|
||||
def _normalize_log_entry(e: dict) -> dict:
|
||||
"""The oracle's next_action is a tuple; the kernel's is an array."""
|
||||
if "canary" in e:
|
||||
na = e["canary"].get("next_action")
|
||||
if isinstance(na, list):
|
||||
e["canary"]["next_action"] = tuple(na)
|
||||
return e
|
||||
@@ -19,6 +19,7 @@ list.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -47,6 +48,13 @@ class ProgrammeStage:
|
||||
result: dict = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
return pc_loader.stage_to_dict(self)
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
return {"stage_id": self.stage_id, "objective": self.objective,
|
||||
"state": self.state, "depends_on": list(self.depends_on),
|
||||
"priority": self.priority}
|
||||
@@ -66,6 +74,17 @@ class Programme:
|
||||
def continuation_canary(self) -> dict:
|
||||
"""'task done' ≠ 'programme done'. Returns the verdict AND the
|
||||
next action — the runner never has to guess whether to stop."""
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
out = pc_loader.canary(self)
|
||||
na = out.get("next_action")
|
||||
return {"verdict": out["verdict"],
|
||||
"next_action": tuple(na) if na is not None else None,
|
||||
"reason": out["reason"]}
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
complete = [s for s in self.stages if s.state == COMPLETE]
|
||||
if len(complete) == len(self.stages):
|
||||
return {"verdict": "PROGRAMME_COMPLETE",
|
||||
@@ -118,6 +137,15 @@ class Programme:
|
||||
"""PENDING stages whose dependencies are COMPLETE, priority-first.
|
||||
ELIGIBLE counts as runnable too — an eligible stage nobody started
|
||||
is exactly the case the continuation canary exists to catch."""
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
ids = pc_loader.eligible(self)
|
||||
by_id = {s.stage_id: s for s in self.stages}
|
||||
return [by_id[i] for i in ids if i in by_id]
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
done = {s.stage_id for s in self.stages if s.state == COMPLETE}
|
||||
out = [s for s in self.stages
|
||||
if s.state in (PENDING, ELIGIBLE)
|
||||
@@ -127,6 +155,18 @@ class Programme:
|
||||
|
||||
# ── durable persistence (canonical store, not transcript) ────────
|
||||
def save(self, base: Path) -> Path:
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
doc = pc_loader.save_doc(self) # clock read via pc.time
|
||||
base = Path(base)
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
p = base / f"{self.programme_id}.json"
|
||||
p.write_text(json.dumps(doc, indent=1))
|
||||
return p
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
p = base / f"{self.programme_id}.json"
|
||||
p.write_text(json.dumps({
|
||||
@@ -145,6 +185,21 @@ class Programme:
|
||||
if not p.exists():
|
||||
return None
|
||||
d = json.loads(p.read_text())
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
out = pc_loader.load_doc(d)
|
||||
if out is None:
|
||||
return None
|
||||
prog = cls(programme_id=out["programme_id"],
|
||||
objective=out["objective"],
|
||||
fallbacks=out.get("fallbacks", []))
|
||||
for s in out["stages"]:
|
||||
prog.stages.append(ProgrammeStage(**s))
|
||||
return prog
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
prog = cls(programme_id=d["programme_id"],
|
||||
objective=d["objective"], fallbacks=d.get("fallbacks", []))
|
||||
for s in d["stages"]:
|
||||
@@ -158,6 +213,19 @@ def run_programme(prog: Programme, stage_runner, *,
|
||||
provided by the caller — must return a state for the stage. Between
|
||||
EVERY stage: persist → continuation canary → next. Returns the
|
||||
transition log (the 15M proof artifact)."""
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
out = pc_loader.run_programme(
|
||||
prog, stage_runner, max_transitions=max_transitions)
|
||||
by_id = {s.stage_id: s for s in prog.stages}
|
||||
for sid, state in out["states"].items():
|
||||
if sid in by_id:
|
||||
by_id[sid].state = state
|
||||
return out["log"]
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
log: list[dict] = []
|
||||
for _ in range(max_transitions):
|
||||
prog_canary = prog.continuation_canary()
|
||||
@@ -202,6 +270,26 @@ def scaffolding_stage_runner(interface, *, scaffold_store: Path,
|
||||
runs under the event bridge and the driver resumes on the EVENT;
|
||||
canonical verification still decides completion truth. Stages without
|
||||
work complete as governance-only transitions."""
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
from rune import event_wait as ew # optional dependency
|
||||
|
||||
cfg = {"scaffold_store": str(scaffold_store),
|
||||
"daemon_url": daemon_url or "",
|
||||
"watchdog_secs": watchdog_secs,
|
||||
"project_id": getattr(interface.scaffold,
|
||||
"project_id", "")}
|
||||
|
||||
def run_kernel(stage: ProgrammeStage) -> str:
|
||||
out = pc_loader.stage_run(stage, cfg, interface)
|
||||
stage.result.update(out["result"])
|
||||
return out["state"]
|
||||
|
||||
return run_kernel
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
from rune import event_wait as ew # local import: optional dependency
|
||||
|
||||
def _phase(contract_id: str) -> str:
|
||||
@@ -321,6 +409,27 @@ def run_construction_programme(prog: Programme, interface, *,
|
||||
run_programme with the Scaffolding-bound runner; persists the
|
||||
programme around every transition so a restart resumes from the
|
||||
durable record, never a transcript. Returns the transition log."""
|
||||
if os.environ.get("RUNE_PC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import pc_loader
|
||||
if pc_loader.available():
|
||||
cfg = {"scaffold_store": str(scaffold_store),
|
||||
"daemon_url": daemon_url or "",
|
||||
"watchdog_secs": watchdog_secs,
|
||||
"project_id": getattr(interface.scaffold,
|
||||
"project_id", "")}
|
||||
out = pc_loader.run_construction(
|
||||
prog, cfg, interface, max_transitions=max_transitions)
|
||||
by_id = {s.stage_id: s for s in prog.stages}
|
||||
for sid, state in out["states"].items():
|
||||
if sid in by_id:
|
||||
by_id[sid].state = state
|
||||
for sid, result in out["results"].items():
|
||||
if sid in by_id:
|
||||
by_id[sid].result.update(result)
|
||||
return out["log"]
|
||||
except Exception:
|
||||
pass # Python oracle below; never crash the caller
|
||||
runner = scaffolding_stage_runner(
|
||||
interface, scaffold_store=scaffold_store,
|
||||
daemon_url=daemon_url, watchdog_secs=watchdog_secs)
|
||||
|
||||
@@ -73,6 +73,30 @@ def _now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
# ── Rust core delegation (dichotomy wave, #2479 tranche) ────────────────
|
||||
# The derivation kernel is FULL Rust (src/rust/prompt_sync, ps_loader.py);
|
||||
# the Python code below each delegation point is the fallback oracle.
|
||||
# RUNE_PS_CORE=python pins the oracle (poisoned-truth law).
|
||||
|
||||
_PS_LOADER = None
|
||||
_PS_LOADER_TRIED = False
|
||||
|
||||
|
||||
def _rust_available() -> bool:
|
||||
global _PS_LOADER, _PS_LOADER_TRIED
|
||||
if os.environ.get("RUNE_PS_CORE", "auto") == "python":
|
||||
return False
|
||||
if not _PS_LOADER_TRIED:
|
||||
_PS_LOADER_TRIED = True
|
||||
try:
|
||||
import ps_loader as _pl
|
||||
if _pl.available():
|
||||
_PS_LOADER = _pl
|
||||
except Exception:
|
||||
logger.debug("ps_loader unavailable", exc_info=True)
|
||||
return _PS_LOADER is not None
|
||||
|
||||
|
||||
def get_store():
|
||||
"""Connect to the memory store. Use as context manager: `with get_store() as db:`."""
|
||||
db = sqlite3.connect(STORE_DB)
|
||||
@@ -86,6 +110,11 @@ def get_store():
|
||||
|
||||
def get_entries_by_topic(db, topic):
|
||||
"""Get all active always-on entries for a topic."""
|
||||
if _rust_available():
|
||||
try:
|
||||
return _PS_LOADER.get_entries_by_topic(STORE_DB, topic)
|
||||
except Exception:
|
||||
logger.debug("ps_get_entries_by_topic fell back", exc_info=True)
|
||||
rows = db.execute(
|
||||
"SELECT text, importance, confidence FROM entries "
|
||||
"WHERE status='active' AND always_on=1 AND topic=? "
|
||||
@@ -98,6 +127,11 @@ def get_entries_by_topic(db, topic):
|
||||
def get_constitution(db):
|
||||
"""Get constitution entries from store. Curated: top entries by importance."""
|
||||
entries = get_entries_by_topic(db, "constitution")
|
||||
if _rust_available():
|
||||
try:
|
||||
return _PS_LOADER.get_constitution(entries)
|
||||
except Exception:
|
||||
logger.debug("ps_get_constitution fell back", exc_info=True)
|
||||
if not entries:
|
||||
return None
|
||||
# Sort by importance, take top 25 (most important constitution entries)
|
||||
@@ -116,6 +150,11 @@ def get_constitution(db):
|
||||
def get_identity(db):
|
||||
"""Get identity entries from store."""
|
||||
entries = get_entries_by_topic(db, "identity")
|
||||
if _rust_available():
|
||||
try:
|
||||
return _PS_LOADER.get_identity(entries)
|
||||
except Exception:
|
||||
logger.debug("ps_get_identity fell back", exc_info=True)
|
||||
if not entries:
|
||||
return None
|
||||
lines = []
|
||||
@@ -129,6 +168,11 @@ def get_identity(db):
|
||||
def get_operating(db):
|
||||
"""Get operating rules from store. Curated: top entries by importance."""
|
||||
entries = get_entries_by_topic(db, "operating")
|
||||
if _rust_available():
|
||||
try:
|
||||
return _PS_LOADER.get_operating(entries)
|
||||
except Exception:
|
||||
logger.debug("ps_get_operating fell back", exc_info=True)
|
||||
if not entries:
|
||||
return None
|
||||
# Sort by importance, take top 15
|
||||
@@ -147,6 +191,15 @@ def get_operating(db):
|
||||
def get_defining_principles():
|
||||
"""Get defining principles from the defining document."""
|
||||
defining_path = os.path.join(memory_env.memory_dir(), "rune_defining_document.md")
|
||||
if _rust_available():
|
||||
try:
|
||||
content = None
|
||||
if os.path.exists(defining_path):
|
||||
with open(defining_path) as f:
|
||||
content = f.read()
|
||||
return _PS_LOADER.get_defining_principles(content)
|
||||
except Exception:
|
||||
logger.debug("ps_get_defining_principles fell back", exc_info=True)
|
||||
if not os.path.exists(defining_path):
|
||||
return None
|
||||
try:
|
||||
@@ -179,6 +232,11 @@ def get_defining_principles():
|
||||
def get_human(db):
|
||||
"""Get human model from store."""
|
||||
entries = get_entries_by_topic(db, "human")
|
||||
if _rust_available():
|
||||
try:
|
||||
return _PS_LOADER.get_human(entries)
|
||||
except Exception:
|
||||
logger.debug("ps_get_human fell back", exc_info=True)
|
||||
if not entries:
|
||||
return None
|
||||
lines = []
|
||||
@@ -191,6 +249,27 @@ def get_human(db):
|
||||
|
||||
def build_prompt(db):
|
||||
"""Build the full system prompt from store entries."""
|
||||
if _rust_available():
|
||||
try:
|
||||
voice_register = voice_sync.get_delivery_register_from_db(db)
|
||||
voice_body = None
|
||||
if voice_register:
|
||||
voice_body = voice_sync.generate_voice_section(
|
||||
voice_sync.extract_voice_section(voice_register))
|
||||
defining_content = None
|
||||
defining_path = os.path.join(memory_env.memory_dir(), "rune_defining_document.md")
|
||||
if os.path.exists(defining_path):
|
||||
with open(defining_path) as f:
|
||||
defining_content = f.read()
|
||||
return _PS_LOADER.build_prompt(
|
||||
STATIC_HEADER, STATIC_FOOTER, build_pipeline_policy.PROMPT_SECTION,
|
||||
voice_body, defining_content,
|
||||
get_entries_by_topic(db, "constitution"),
|
||||
get_entries_by_topic(db, "operating"),
|
||||
get_entries_by_topic(db, "human"),
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("ps_build_prompt fell back", exc_info=True)
|
||||
sections = []
|
||||
sections.append(STATIC_HEADER)
|
||||
|
||||
@@ -270,6 +349,15 @@ def check_sync():
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if _rust_available():
|
||||
try:
|
||||
current = None
|
||||
if os.path.exists(RUNE_MD):
|
||||
with open(RUNE_MD) as f:
|
||||
current = f.read()
|
||||
return _PS_LOADER.check_sync(new_prompt, current)
|
||||
except Exception:
|
||||
logger.debug("ps_check_sync fell back", exc_info=True)
|
||||
if not os.path.exists(RUNE_MD):
|
||||
return {"synced": False, "reason": "rune.md does not exist"}
|
||||
|
||||
@@ -290,6 +378,15 @@ def diff_prompt():
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if _rust_available():
|
||||
try:
|
||||
current = None
|
||||
if os.path.exists(RUNE_MD):
|
||||
with open(RUNE_MD) as f:
|
||||
current = f.read()
|
||||
return _PS_LOADER.diff_prompt(new_prompt, current)
|
||||
except Exception:
|
||||
logger.debug("ps_diff_prompt fell back", exc_info=True)
|
||||
if not os.path.exists(RUNE_MD):
|
||||
return {"diff": "rune.md does not exist — would create new"}
|
||||
|
||||
@@ -310,6 +407,17 @@ def diff_prompt():
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
"""On-session-end hook: check if prompt needs syncing."""
|
||||
if _rust_available():
|
||||
try:
|
||||
check_synced = None
|
||||
sync_result = None
|
||||
if event == "on_session_end":
|
||||
check_synced = bool(check_sync().get("synced"))
|
||||
if not check_synced:
|
||||
sync_result = sync_prompt()
|
||||
return _PS_LOADER.handle_hook(event, check_synced, sync_result)
|
||||
except Exception:
|
||||
logger.debug("ps_handle_hook fell back", exc_info=True)
|
||||
if event == "on_session_end":
|
||||
result = check_sync()
|
||||
if not result.get("synced"):
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust prompt_sync core (dichotomy wave, #2479
|
||||
tranche). Mirrors ci_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ps_get_entries_by_topic({db_path, topic}) -> [row dicts]
|
||||
* ps_get_constitution({entries}) -> str | None
|
||||
* ps_get_identity({entries}) -> str | None
|
||||
* ps_get_operating({entries}) -> str | None
|
||||
* ps_get_defining_principles({content}) -> str | None
|
||||
* ps_get_human({entries}) -> str | None
|
||||
* ps_build_prompt({static_header, static_footer, pipeline_section,
|
||||
voice_body?, defining_content?, constitution_rows, operating_rows,
|
||||
human_rows}) -> prompt string
|
||||
* ps_check_sync({new_prompt, current}) -> {synced, reason}
|
||||
* ps_diff_prompt({new_prompt, current}) -> {diff, added, removed}
|
||||
* ps_handle_hook({event, check_synced, sync_result}) -> response dict
|
||||
|
||||
The kernel is PURE: it receives rows/contents/voice body as JSON; rune.md
|
||||
I/O, the voice_sync chain, and timestamps stay in the Python facade.
|
||||
The read-only sqlite path inside the kernel uses the SYSTEM libsqlite3.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with PS_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" / "prompt_sync"
|
||||
_LIB_PATH = Path(os.environ.get("PS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libprompt_sync.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class PromptSyncCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust prompt_sync 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 PromptSyncCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("ps_get_entries_by_topic", "ps_get_constitution", "ps_get_identity",
|
||||
"ps_get_operating", "ps_get_defining_principles", "ps_get_human",
|
||||
"ps_build_prompt", "ps_check_sync", "ps_diff_prompt", "ps_handle_hook"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def get_entries_by_topic(db_path: str, topic: str) -> list:
|
||||
return _call("ps_get_entries_by_topic", {"db_path": db_path, "topic": topic})
|
||||
|
||||
|
||||
def get_constitution(entries: list) -> str | None:
|
||||
return _call("ps_get_constitution", {"entries": entries})
|
||||
|
||||
|
||||
def get_identity(entries: list) -> str | None:
|
||||
return _call("ps_get_identity", {"entries": entries})
|
||||
|
||||
|
||||
def get_operating(entries: list) -> str | None:
|
||||
return _call("ps_get_operating", {"entries": entries})
|
||||
|
||||
|
||||
def get_defining_principles(content: str | None) -> str | None:
|
||||
return _call("ps_get_defining_principles", {"content": content})
|
||||
|
||||
|
||||
def get_human(entries: list) -> str | None:
|
||||
return _call("ps_get_human", {"entries": entries})
|
||||
|
||||
|
||||
def build_prompt(static_header: str, static_footer: str, pipeline_section: str,
|
||||
voice_body: str | None, defining_content: str | None,
|
||||
constitution_rows: list, operating_rows: list,
|
||||
human_rows: list) -> str:
|
||||
return _call("ps_build_prompt", {
|
||||
"static_header": static_header, "static_footer": static_footer,
|
||||
"pipeline_section": pipeline_section, "voice_body": voice_body,
|
||||
"defining_content": defining_content,
|
||||
"constitution_rows": constitution_rows, "operating_rows": operating_rows,
|
||||
"human_rows": human_rows,
|
||||
})
|
||||
|
||||
|
||||
def check_sync(new_prompt: str, current: str | None) -> dict:
|
||||
return _call("ps_check_sync", {"new_prompt": new_prompt, "current": current})
|
||||
|
||||
|
||||
def diff_prompt(new_prompt: str, current: str | None) -> dict:
|
||||
return _call("ps_diff_prompt", {"new_prompt": new_prompt, "current": current})
|
||||
|
||||
|
||||
def handle_hook(event: str, check_synced: bool | None, sync_result: dict | None) -> dict:
|
||||
return _call("ps_handle_hook", {
|
||||
"event": event, "check_synced": check_synced, "sync_result": sync_result,
|
||||
})
|
||||
@@ -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.209.0
|
||||
Version: v0.229.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.209.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.229.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -13,6 +13,14 @@ the gate suite green + no regression vs the baseline run.
|
||||
Worktree isolation: proposals build on `git worktree` branches off the
|
||||
live repo, so the candidate never touches the working tree until
|
||||
promoted. Discard = remove worktree + delete branch (full cleanup).
|
||||
|
||||
Dichotomy wave #2479: the FULL Rust core (src/rust/self_modify via
|
||||
smod_loader) owns the subprocess seam (_run: every git invocation and
|
||||
gate command) and every proposal decision; this module remains the
|
||||
parity oracle and the strangler fallback (RUNE_SMOD_CORE=python pins it
|
||||
— the RUNE_SM_CORE pin belongs to shard_manager). Raise parity holds at
|
||||
the type level: the loader re-raises FileNotFoundError /
|
||||
TimeoutExpired / KeyError / TypeError exactly as the oracle would.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +32,25 @@ import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def _smod_core():
|
||||
"""Dichotomy wave #2479: Rust core first (src/rust/self_modify via
|
||||
smod_loader). RUNE_SMOD_CORE=python pins the Python fallback below —
|
||||
it remains the parity oracle (strangler law). The RUNE_SM_CORE pin
|
||||
belongs to shard_manager. The kernel owns the subprocess seam (_run:
|
||||
every git invocation and gate command) and every proposal decision;
|
||||
the wall clock enters as an argument (no-clock law)."""
|
||||
if os.environ.get("RUNE_SMOD_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import smod_loader
|
||||
|
||||
if smod_loader.available():
|
||||
return smod_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _run(cmd, cwd, timeout=300):
|
||||
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
|
||||
timeout=timeout)
|
||||
@@ -33,6 +60,11 @@ def _run(cmd, cwd, timeout=300):
|
||||
def propose_change(repo: str, branch: str, base: str = "master") -> dict:
|
||||
"""Create the isolated worktree + branch. Returns the proposal record
|
||||
(with rollback_ref = the base HEAD — the recovery path)."""
|
||||
core = _smod_core()
|
||||
if core is not None:
|
||||
return core.propose_change(
|
||||
repo, branch, base,
|
||||
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
|
||||
rc, out, err = _run(["git", "rev-parse", base], cwd=repo)
|
||||
if rc != 0:
|
||||
return {"proposed": False, "error": f"unknown base: {base}"}
|
||||
@@ -49,6 +81,9 @@ def propose_change(repo: str, branch: str, base: str = "master") -> dict:
|
||||
|
||||
def run_gates(worktree: str, test_cmd: list | None = None) -> dict:
|
||||
"""Run the gate suite inside the worktree. Returns {passed, output}."""
|
||||
core = _smod_core()
|
||||
if core is not None:
|
||||
return core.run_gates(worktree, test_cmd)
|
||||
cmd = test_cmd or ["python3", "-m", "pytest", "tests/", "-q", "-x"]
|
||||
rc, out, err = _run(cmd, cwd=worktree, timeout=600)
|
||||
return {"passed": rc == 0,
|
||||
@@ -57,6 +92,9 @@ def run_gates(worktree: str, test_cmd: list | None = None) -> dict:
|
||||
|
||||
def compare_baseline(candidate_result: dict, baseline_result: dict) -> dict:
|
||||
"""Compare candidate vs baseline gate runs. Regression blocks."""
|
||||
core = _smod_core()
|
||||
if core is not None:
|
||||
return core.compare_baseline(candidate_result, baseline_result)
|
||||
if not candidate_result.get("passed"):
|
||||
return {"promote": False,
|
||||
"reason": "candidate gates red — regression blocks promotion"}
|
||||
@@ -69,6 +107,9 @@ def compare_baseline(candidate_result: dict, baseline_result: dict) -> dict:
|
||||
|
||||
def promote_or_discard(repo: str, proposal: dict, verdict: dict) -> dict:
|
||||
"""Merge (promote) or delete (discard) the proposal branch."""
|
||||
core = _smod_core()
|
||||
if core is not None:
|
||||
return core.promote_or_discard(repo, proposal, verdict)
|
||||
branch = proposal["branch"]
|
||||
wt = proposal["worktree"]
|
||||
if verdict.get("promote"):
|
||||
@@ -88,6 +129,9 @@ def promote_or_discard(repo: str, proposal: dict, verdict: dict) -> dict:
|
||||
|
||||
def rollback(repo: str, rollback_ref: str) -> dict:
|
||||
"""Restore the pre-change ref. The recovery path every proposal carries."""
|
||||
core = _smod_core()
|
||||
if core is not None:
|
||||
return core.rollback(repo, rollback_ref)
|
||||
rc, _out, err = _run(["git", "reset", "--hard", rollback_ref], cwd=repo)
|
||||
if rc != 0:
|
||||
return {"rolled_back": False, "error": err[:200]}
|
||||
@@ -96,6 +140,9 @@ def rollback(repo: str, rollback_ref: str) -> dict:
|
||||
|
||||
def monitor(proposal: dict) -> dict:
|
||||
"""The post-promotion watch record: what to observe after promotion."""
|
||||
core = _smod_core()
|
||||
if core is not None:
|
||||
return core.monitor(proposal)
|
||||
return {"monitoring": True, "branch": proposal.get("branch", ""),
|
||||
"rollback_ref": proposal.get("rollback_ref", ""),
|
||||
"watch": ["gate suite green on next CI run",
|
||||
|
||||
@@ -23,14 +23,41 @@ SHARD_INDEX = os.path.expanduser("~/.config/opencode/memory/shard_index.json")
|
||||
|
||||
|
||||
class ShardManager:
|
||||
"""Manages topic-based SQLite shards for unbounded memory growth."""
|
||||
"""Manages topic-based SQLite shards for unbounded memory growth.
|
||||
|
||||
FULL Rust core (src/rust/shard_manager) when built — strangler law:
|
||||
RUNE_SM_CORE=python pins the Python fallback, which remains the parity
|
||||
oracle. The sqlite/sqlite-vec resource boundary is served as ops by
|
||||
sm_loader; connection construction stays Python-side."""
|
||||
|
||||
def __init__(self):
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
self.index = core.init(SHARD_DIR, SHARD_INDEX)
|
||||
return
|
||||
os.makedirs(SHARD_DIR, exist_ok=True)
|
||||
self.index = self._load_index()
|
||||
|
||||
@staticmethod
|
||||
def _core():
|
||||
"""The Rust shard_manager core when available (strangler law).
|
||||
RUNE_SM_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_SM_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import sm_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return sm_loader if sm_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
def _load_index(self) -> Dict[str, str]:
|
||||
"""Load shard index from disk."""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
return core.load_index(SHARD_INDEX)
|
||||
if os.path.exists(SHARD_INDEX):
|
||||
try:
|
||||
with open(SHARD_INDEX) as f:
|
||||
@@ -41,11 +68,20 @@ class ShardManager:
|
||||
|
||||
def _save_index(self):
|
||||
"""Save shard index to disk."""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
core.save_index(SHARD_INDEX, self.index)
|
||||
return
|
||||
with open(SHARD_INDEX, "w") as f:
|
||||
json.dump(self.index, f, indent=2)
|
||||
|
||||
def create_shard(self, topic: str) -> str:
|
||||
"""Create a new shard for a topic. Returns shard path."""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
res = core.create_shard(SHARD_DIR, SHARD_INDEX, topic, self.index)
|
||||
self.index = res["index"]
|
||||
return res["shard_path"]
|
||||
shard_name = f"shard_{topic.replace(' ', '_').replace('/', '_')}.db"
|
||||
shard_path = os.path.join(SHARD_DIR, shard_name)
|
||||
|
||||
@@ -94,6 +130,9 @@ class ShardManager:
|
||||
|
||||
Returns the topic name. Falls back to 'general' if no match.
|
||||
"""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
return core.route_query(query, self.index)
|
||||
query_lower = query.lower()
|
||||
|
||||
# Check existing shards for keyword matches
|
||||
@@ -125,10 +164,15 @@ class ShardManager:
|
||||
extension loaded PER CONNECTION — a fresh connection without it cannot
|
||||
see or query entries_vec, even though the schema created it ("no such
|
||||
table" in clean environments). Mirror memory_store.connect()."""
|
||||
if topic not in self.index:
|
||||
self.create_shard(topic)
|
||||
|
||||
shard_path = self.index[topic]
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
res = core.get_shard(SHARD_DIR, SHARD_INDEX, topic, self.index)
|
||||
self.index = res["index"]
|
||||
shard_path = res["shard_path"]
|
||||
else:
|
||||
if topic not in self.index:
|
||||
self.create_shard(topic)
|
||||
shard_path = self.index[topic]
|
||||
db = sqlite3.connect(shard_path)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.enable_load_extension(True)
|
||||
@@ -142,6 +186,9 @@ class ShardManager:
|
||||
|
||||
def merge_results(self, results_list: List[List[dict]]) -> List[dict]:
|
||||
"""Merge results from multiple shards, deduplicate, sort by relevance."""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
return core.merge_results(results_list)
|
||||
seen = set()
|
||||
merged = []
|
||||
|
||||
@@ -158,6 +205,9 @@ class ShardManager:
|
||||
|
||||
def get_shard_stats(self) -> Dict[str, int]:
|
||||
"""Get entry counts per shard."""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
return core.get_shard_stats(self.index)
|
||||
stats = {}
|
||||
for topic, path in self.index.items():
|
||||
try:
|
||||
@@ -171,6 +221,9 @@ class ShardManager:
|
||||
|
||||
def total_entries(self) -> int:
|
||||
"""Get total entries across all shards."""
|
||||
core = self._core()
|
||||
if core is not None:
|
||||
return core.total_entries(self.index)
|
||||
stats = self.get_shard_stats()
|
||||
return sum(stats.values())
|
||||
|
||||
|
||||
+205
-29
@@ -1,9 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust structural_model core (facade burn-down).
|
||||
"""ctypes loader for the FULL Rust shard_manager core (dichotomy wave,
|
||||
issue #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* sm_query({relationships, eid, op}) -> list/str/None
|
||||
* sm_free_string(ptr)
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* sm_init({shard_dir, index_path}) -> index dict
|
||||
* sm_load_index({index_path}) -> index dict
|
||||
* sm_save_index({index_path, index}) -> null
|
||||
* sm_create_shard({shard_dir, index_path, topic, index}, ops_cb)
|
||||
-> {shard_path, index}
|
||||
* sm_route_query({query, index}) -> topic string
|
||||
* sm_merge_results({results_list}) -> merged list
|
||||
* sm_get_shard({shard_dir, index_path, topic, index}, ops_cb)
|
||||
-> {shard_path, index}
|
||||
* sm_get_shard_stats({index}, ops_cb) -> stats dict
|
||||
* sm_total_entries({index}, ops_cb) -> int
|
||||
|
||||
Ops seam (resource boundary the kernel cannot own): the Rust kernel issues
|
||||
`ensure_schema` / `count_entries` ops through a C callback; THIS loader
|
||||
serves them against real sqlite3 + sqlite_vec. Connection construction
|
||||
stays Python-side (see shard_manager.ShardManager.get_shard).
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with SM_RUST_LIB (a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,19 +29,20 @@ from __future__ import annotations
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "structural_model"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"SM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libstructural_model.so"
|
||||
)
|
||||
)
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "shard_manager"
|
||||
_LIB_PATH = Path(os.environ.get("SM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libshard_manager.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ShardCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust shard_manager core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
@@ -32,32 +51,189 @@ def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise FileNotFoundError(f"structural_model not built: {_LIB_PATH}")
|
||||
if not _LIB_PATH.is_file():
|
||||
raise ShardCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.sm_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.sm_free_string.restype = None
|
||||
fn = lib.sm_query
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
for sym in (
|
||||
"sm_init",
|
||||
"sm_load_index",
|
||||
"sm_save_index",
|
||||
"sm_route_query",
|
||||
"sm_merge_results",
|
||||
):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
for sym in (
|
||||
"sm_create_shard",
|
||||
"sm_get_shard",
|
||||
"sm_get_shard_stats",
|
||||
"sm_total_entries",
|
||||
):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p, _OPS_CB]
|
||||
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):
|
||||
def _call(sym: str, payload: dict, with_ops: bool = False):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
args = [ctypes.cast(buf, ctypes.c_void_p)]
|
||||
if with_ops:
|
||||
args.append(_ops_cb)
|
||||
ptr = getattr(lib, sym)(*args)
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.sm_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
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 query(relationships: list, eid: str, op: str):
|
||||
return _call("sm_query", {"relationships": relationships, "eid": eid, "op": op})
|
||||
# ── ops seam: the loader serves the kernel's sqlite ops ──────────────────────
|
||||
|
||||
_OPS_CB = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
|
||||
_last_ops_buf = None
|
||||
|
||||
SCHEMA = """
|
||||
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 '',
|
||||
source TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'active',
|
||||
wash_count INTEGER DEFAULT 0,
|
||||
last_washed TEXT,
|
||||
rock_status TEXT DEFAULT 'washable',
|
||||
tier TEXT DEFAULT 'warm'
|
||||
);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
|
||||
text, content='', tokenize='porter unicode61'
|
||||
);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS entries_vec USING vec0(
|
||||
embedding float[256]
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_entries_status ON entries(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic);
|
||||
CREATE INDEX IF NOT EXISTS idx_entries_wash_count ON entries(wash_count);
|
||||
CREATE INDEX IF NOT EXISTS idx_entries_tier ON entries(tier);
|
||||
"""
|
||||
|
||||
|
||||
def _execute_op(op: dict) -> dict:
|
||||
kind = op.get("op")
|
||||
args = op.get("args") or {}
|
||||
if kind == "ensure_schema":
|
||||
# mirror oracle create_shard lines 55-86 (sqlite + sqlite_vec + schema)
|
||||
path = args["path"]
|
||||
db = sqlite3.connect(path)
|
||||
try:
|
||||
db.enable_load_extension(True)
|
||||
import sqlite_vec
|
||||
|
||||
sqlite_vec.load(db) # #26: entries_vec virtual table needs the extension per connection
|
||||
db.executescript(SCHEMA)
|
||||
finally:
|
||||
db.close()
|
||||
return {"ok": True}
|
||||
if kind == "count_entries":
|
||||
# mirror oracle get_shard_stats lines 163-169 (plain connect; any
|
||||
# failure degrades to the except branch → kernel writes 0)
|
||||
try:
|
||||
path = args["path"]
|
||||
db = sqlite3.connect(path)
|
||||
try:
|
||||
count = db.execute("SELECT COUNT(*) FROM entries WHERE status='active'").fetchone()[0]
|
||||
finally:
|
||||
db.close()
|
||||
return {"ok": True, "value": count}
|
||||
except Exception: # noqa: BLE001 — deliberate: best-effort degrade, crash would be worse
|
||||
return {"ok": False}
|
||||
return {"ok": False, "err": f"unknown op {kind!r}"}
|
||||
|
||||
|
||||
def _ops_handler(op_ptr) -> int:
|
||||
global _last_ops_buf
|
||||
op = json.loads(ctypes.string_at(op_ptr).decode())
|
||||
result = _execute_op(op)
|
||||
_last_ops_buf = ctypes.create_string_buffer(json.dumps(result).encode())
|
||||
return ctypes.cast(_last_ops_buf, ctypes.c_void_p).value
|
||||
|
||||
|
||||
_ops_cb = _OPS_CB(_ops_handler)
|
||||
|
||||
|
||||
# ── typed public API ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def init(shard_dir: str, index_path: str) -> dict:
|
||||
return _call("sm_init", {"shard_dir": shard_dir, "index_path": index_path})
|
||||
|
||||
|
||||
def load_index(index_path: str) -> dict:
|
||||
return _call("sm_load_index", {"index_path": index_path})
|
||||
|
||||
|
||||
def save_index(index_path: str, index: dict) -> None:
|
||||
_call("sm_save_index", {"index_path": index_path, "index": index})
|
||||
|
||||
|
||||
def create_shard(shard_dir: str, index_path: str, topic: str, index: dict) -> dict:
|
||||
return _call(
|
||||
"sm_create_shard",
|
||||
{
|
||||
"shard_dir": shard_dir,
|
||||
"index_path": index_path,
|
||||
"topic": topic,
|
||||
"index": index,
|
||||
},
|
||||
with_ops=True,
|
||||
)
|
||||
|
||||
|
||||
def route_query(query: str, index: dict) -> str:
|
||||
return _call("sm_route_query", {"query": query, "index": index})
|
||||
|
||||
|
||||
def merge_results(results_list: list) -> list:
|
||||
return _call("sm_merge_results", {"results_list": results_list})
|
||||
|
||||
|
||||
def get_shard(shard_dir: str, index_path: str, topic: str, index: dict) -> dict:
|
||||
return _call(
|
||||
"sm_get_shard",
|
||||
{
|
||||
"shard_dir": shard_dir,
|
||||
"index_path": index_path,
|
||||
"topic": topic,
|
||||
"index": index,
|
||||
},
|
||||
with_ops=True,
|
||||
)
|
||||
|
||||
|
||||
def get_shard_stats(index: dict) -> dict:
|
||||
return _call("sm_get_shard_stats", {"index": index}, with_ops=True)
|
||||
|
||||
|
||||
def total_entries(index: dict) -> int:
|
||||
return _call("sm_total_entries", {"index": index}, with_ops=True)
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust self_modify core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* smod_propose_change({repo, branch, base, now}) -> proposal dict
|
||||
* smod_run_gates({worktree, test_cmd|null}) -> {passed, output}
|
||||
* smod_compare_baseline({candidate_result, baseline_result}) -> verdict
|
||||
* smod_promote_or_discard({repo, proposal, verdict}) -> result dict
|
||||
* smod_rollback({repo, rollback_ref}) -> result dict
|
||||
* smod_monitor({proposal}) -> watch record
|
||||
|
||||
No-clock law: `now` (the oracle's
|
||||
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())) is always an
|
||||
argument — the kernel never reads the wall clock.
|
||||
|
||||
Boundary: the subprocess seam (_run — every git invocation and gate
|
||||
command) is owned by the kernel, exactly like the oracle's _run. The
|
||||
facade keeps only the wall clock and the RUNE_SMOD_CORE pin.
|
||||
|
||||
Raise parity: the oracle RAISES (FileNotFoundError from a missing cwd /
|
||||
binary, TimeoutExpired on gate timeouts, KeyError from a missing proposal
|
||||
key, TypeError at the argv seam). The kernel answers with the envelope's
|
||||
err half and this loader re-raises the ORACLE'S exception TYPE via
|
||||
_raise_like_oracle — raise parity holds at the type level, not just
|
||||
raise-vs-return.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process) — this crate
|
||||
deliberately exports NO free symbol of its own (duplicate exports break
|
||||
the cdylib link when sibling crates share one process). Override the .so
|
||||
with SMOD_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "self_modify"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("SMOD_RUST_LIB", _CRATE_DIR / "target" / "release" / "libself_modify.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"smod_propose_change",
|
||||
"smod_run_gates",
|
||||
"smod_compare_baseline",
|
||||
"smod_promote_or_discard",
|
||||
"smod_rollback",
|
||||
"smod_monitor",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class SelfModifyCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust self-modify 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 SelfModifyCoreUnavailable(
|
||||
f"Rust self_modify core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _raise_like_oracle(err: str, sym: str):
|
||||
"""Re-raise the ORACLE's exception type, not a blanket RuntimeError.
|
||||
|
||||
The kernel's envelope err encodes enough to reconstruct the raise
|
||||
parity the Python oracle has (subprocess FileNotFoundError /
|
||||
PermissionError / TimeoutExpired, dict KeyError, argv TypeError):
|
||||
* "[Errno N] …" → OSError subclass by errno (spawn seam)
|
||||
* "Command '…' timed out after S seconds" → subprocess.TimeoutExpired
|
||||
* "KeyError: 'k'" → KeyError(k)
|
||||
* "TypeError: …" → TypeError
|
||||
Anything else stays a RuntimeError.
|
||||
"""
|
||||
if err.startswith("[Errno "):
|
||||
try:
|
||||
errno = int(err[7:].split("]", 1)[0])
|
||||
except ValueError:
|
||||
raise RuntimeError(err) from None
|
||||
if errno == 2:
|
||||
raise FileNotFoundError(err)
|
||||
if errno == 13:
|
||||
raise PermissionError(err)
|
||||
if errno == 20:
|
||||
raise NotADirectoryError(err)
|
||||
raise OSError(errno, err)
|
||||
m = re.match(r"Command '(.*)' timed out after (\d+) seconds$", err)
|
||||
if m:
|
||||
raise subprocess.TimeoutExpired(m.group(1), float(m.group(2)))
|
||||
if err.startswith("KeyError: "):
|
||||
raise KeyError(err[len("KeyError: "):].strip("'"))
|
||||
if err.startswith("TypeError: "):
|
||||
raise TypeError(err[len("TypeError: "):])
|
||||
raise RuntimeError(err)
|
||||
|
||||
|
||||
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 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
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
_raise_like_oracle(doc.get("err", f"{sym} failed"), sym)
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def propose_change(repo: str, branch: str, base: str, now: str) -> dict:
|
||||
return _call("smod_propose_change", {
|
||||
"repo": repo, "branch": branch, "base": base, "now": now,
|
||||
})
|
||||
|
||||
|
||||
def run_gates(worktree: str, test_cmd: list | None) -> dict:
|
||||
return _call("smod_run_gates", {
|
||||
"worktree": worktree, "test_cmd": test_cmd,
|
||||
})
|
||||
|
||||
|
||||
def compare_baseline(candidate_result: dict, baseline_result: dict) -> dict:
|
||||
return _call("smod_compare_baseline", {
|
||||
"candidate_result": candidate_result,
|
||||
"baseline_result": baseline_result,
|
||||
})
|
||||
|
||||
|
||||
def promote_or_discard(repo: str, proposal: dict, verdict: dict) -> dict:
|
||||
return _call("smod_promote_or_discard", {
|
||||
"repo": repo, "proposal": proposal, "verdict": verdict,
|
||||
})
|
||||
|
||||
|
||||
def rollback(repo: str, rollback_ref: str) -> dict:
|
||||
return _call("smod_rollback", {
|
||||
"repo": repo, "rollback_ref": rollback_ref,
|
||||
})
|
||||
|
||||
|
||||
def monitor(proposal: dict) -> dict:
|
||||
return _call("smod_monitor", {"proposal": proposal})
|
||||
+15
-5
@@ -602,12 +602,22 @@ def build(manifest_path: Path | None = None) -> dict:
|
||||
strangler law: the old path stays load-bearing until deletion is proven)."""
|
||||
inputs = _gather_build_inputs(manifest_path)
|
||||
core = _rust_core()
|
||||
# The unseeded path is the one-time Phase A migration: it stays on the
|
||||
# Python core so the public ingest_umbrellas seam (monkeypatched by
|
||||
# isolation tests) is honored byte-for-byte. Steady-state (seeded)
|
||||
# builds delegate to the Rust core; the parity battery covers BOTH
|
||||
# paths, including an explicit unseeded-ingest parity section.
|
||||
# The unseeded path is the one-time Phase A migration: the public
|
||||
# ingest_umbrellas seam stays authoritative for it (isolation tests
|
||||
# monkeypatch it; the file-driven refusal contract lives there).
|
||||
# Route the manifest ingest through the seam FIRST, then delegate the
|
||||
# rest of the projection with the ingested doc carried as `existing` —
|
||||
# the kernel then takes the carry-forward branch (its own manifest
|
||||
# ingest would bypass the seam and raise the refusal the caller is
|
||||
# entitled to intercept; regression caught by
|
||||
# test_t214_material_closure.test_spine_seeds_entry_point).
|
||||
seeded = bool(inputs["existing"]) and any(r["type"] == "belongs_to" for r in inputs["existing"]["relationships"])
|
||||
if not seeded and core is not None:
|
||||
seed_doc = new_doc()
|
||||
mp = inputs.get("manifest_path")
|
||||
ingest_umbrellas(seed_doc, Path(mp) if mp else None)
|
||||
inputs = dict(inputs, existing=seed_doc, manifest=None)
|
||||
seeded = bool(seed_doc["entities"]) or bool(seed_doc["relationships"])
|
||||
doc = core.project(inputs) if core is not None and seeded else _py_build_from_inputs(inputs)
|
||||
# both cores funnel through the same post-projection passes (the
|
||||
# contract-discovery write-back + language placement) — the doc-level
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust time_awareness core (dichotomy wave,
|
||||
#2479 tranche). Mirrors to_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* ta_run({op, state, args, now_us, now_mono}) ->
|
||||
{"ok": true, "value": ..., "state": {...mutated...},
|
||||
"save": bool, "snapshot": {...}}
|
||||
|
||||
The kernel is PURE — no clock, no filesystem. The caller supplies the
|
||||
state (last_interaction as isoformat text-or-null / dirty /
|
||||
last_save_time) and the clock (now_us = UTC epoch microseconds,
|
||||
now_mono = monotonic reading). `save: true` means the oracle would have
|
||||
written the file mid-call; `snapshot` is the trigger-time file content —
|
||||
the facade performs the write. All I/O, the singleton and the CLI stay
|
||||
in Python.
|
||||
|
||||
Override the .so with TA_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" / "time_awareness"
|
||||
_LIB_PATH = Path(os.environ.get("TA_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtime_awareness.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class TimeAwarenessCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust time_awareness 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 TimeAwarenessCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.ta_run.argtypes = [ctypes.c_void_p]
|
||||
lib.ta_run.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.ta_run(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("ta_run returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "ta_run failed"))
|
||||
return doc
|
||||
|
||||
|
||||
def run_op(op: str, state: dict, args: dict | None = None, now_us: int = 0, now_mono: float = 0.0) -> dict:
|
||||
"""Run one kernel op. Returns the full envelope:
|
||||
{"value": ..., "state": {...mutated...}, "save": bool, "snapshot": ...}.
|
||||
"""
|
||||
payload = {"op": op, "state": state, "args": args or {},
|
||||
"now_us": now_us, "now_mono": now_mono}
|
||||
return _call(payload)
|
||||
@@ -31,6 +31,24 @@ import memory_env
|
||||
DB_PATH = os.path.join(memory_env.memory_dir(), "task_gate.db")
|
||||
|
||||
|
||||
def _tg_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_TG_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the verdict logic
|
||||
(pattern extraction, gate matching, count/threshold/status transitions,
|
||||
promotion, active-gate projection, hook echo); the sqlite IO and the
|
||||
CLI stay here."""
|
||||
if os.environ.get("RUNE_TG_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import tg_loader
|
||||
|
||||
if tg_loader.available():
|
||||
return tg_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
@@ -67,6 +85,36 @@ def record_instruction(text):
|
||||
"""Record an instruction. If it matches an existing pattern, increment count."""
|
||||
db = connect()
|
||||
try:
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
now = _now()
|
||||
rows = [dict(r) for r in
|
||||
db.execute("SELECT * FROM instructions").fetchall()]
|
||||
out = core.record_instruction(rows, text, now)
|
||||
if out.get("matched"):
|
||||
db.execute(
|
||||
"UPDATE instructions SET count=?, status=?, last_seen=? WHERE id=?",
|
||||
(out["count"], out["status"], out["last_seen"], out["id"]),
|
||||
)
|
||||
db.commit()
|
||||
return {
|
||||
"matched": True,
|
||||
"id": out["id"],
|
||||
"count": out["count"],
|
||||
"status": out["status"],
|
||||
"promoted": out["promoted"],
|
||||
}
|
||||
row = out["new_row"]
|
||||
cur = db.execute(
|
||||
"INSERT INTO instructions (instruction, pattern, count, threshold, status, created_at, last_seen) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(row["instruction"], row["pattern"], row["count"],
|
||||
row["threshold"], row["status"], row["created_at"],
|
||||
row["last_seen"]),
|
||||
)
|
||||
db.commit()
|
||||
return {"matched": False, "id": cur.lastrowid, "count": 1, "status": "observing"}
|
||||
|
||||
text_lower = text.lower().strip()
|
||||
|
||||
# Check if this matches an existing instruction
|
||||
@@ -112,6 +160,12 @@ def check_context(context):
|
||||
"""Check if context matches any active gates."""
|
||||
db = connect()
|
||||
try:
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
rows = [dict(r) for r in
|
||||
db.execute("SELECT * FROM instructions").fetchall()]
|
||||
return core.check_context(rows, context)["matches"]
|
||||
|
||||
context_lower = context.lower()
|
||||
|
||||
active = db.execute("SELECT * FROM instructions WHERE status='active'").fetchall()
|
||||
@@ -146,6 +200,15 @@ def promote_gate(gate_id):
|
||||
"""Manually promote a gate to active."""
|
||||
db = connect()
|
||||
try:
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
out = core.promote_gate(gate_id, _now())
|
||||
db.execute(
|
||||
"UPDATE instructions SET status='active', promoted_at=? WHERE id=?",
|
||||
(out["promoted_at"], gate_id),
|
||||
)
|
||||
db.commit()
|
||||
return {"promoted": gate_id}
|
||||
db.execute("UPDATE instructions SET status='active', promoted_at=? WHERE id=?", (_now(), gate_id))
|
||||
db.commit()
|
||||
return {"promoted": gate_id}
|
||||
@@ -155,6 +218,9 @@ def promote_gate(gate_id):
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
"""Hook handler — check for repeated instructions."""
|
||||
core = _tg_core()
|
||||
if core is not None:
|
||||
return core.handle_hook(event)
|
||||
return {"status": "ok", "event": event, "handler": "task_gate"}
|
||||
|
||||
|
||||
|
||||
+228
-62
@@ -10,9 +10,19 @@ hardened behaviours stay trusted.
|
||||
Forge projection (19P): failures in active iteration create/update a
|
||||
human-visible issue through the T18 forge boundary. Forge failure never
|
||||
rewrites canonical truth.
|
||||
|
||||
Dichotomy wave #2479: the session state machine (next-item selection,
|
||||
record bookkeeping, pause/resume, blockers), the observation
|
||||
record/close shaping, the forge projection title/body and the aggregate
|
||||
row shaping live in the Rust kernel (src/rust/testing_mode) behind
|
||||
doc-envelope FFI; the session/observations FILE IO, the ledger read,
|
||||
the registry IO and the forge adapter call stay here — the kernel is a
|
||||
pure function of their results. RUNE_TM_CORE=python pins the Python
|
||||
fallback (parity harnesses).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -27,6 +37,129 @@ def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _tm_core():
|
||||
"""Dichotomy wave #2479: Rust core first (RUNE_TM_CORE=python pins the
|
||||
Python fallback — parity harnesses). The kernel owns the session
|
||||
state machine + observation/forge/aggregate shaping; the FILE IO,
|
||||
the ledger read and the registry IO stay in this facade."""
|
||||
if os.environ.get("RUNE_TM_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import tm_loader
|
||||
|
||||
if tm_loader.available():
|
||||
return tm_loader
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ── pure shaping helpers (the Python fallback + parity oracle) ───────────
|
||||
|
||||
def _py_default_state(project, scope_component, now) -> dict:
|
||||
return {"schema_version": SESSION_SCHEMA_VERSION,
|
||||
"project": project, "scope_component": scope_component,
|
||||
"current_item": None, "completed_this_session": [],
|
||||
"newly_invalidated": [], "blockers": [],
|
||||
"paused": False, "updated_at": now}
|
||||
|
||||
|
||||
def _py_next_item(state: dict, outstanding: list) -> tuple[dict, dict | None]:
|
||||
"""19L next eligible item — paused sessions dequeue nothing."""
|
||||
if state["paused"]:
|
||||
return state, None
|
||||
item = outstanding[0] if outstanding else None
|
||||
state["current_item"] = (item["behaviour_id"] if item else None)
|
||||
return state, item
|
||||
|
||||
|
||||
def _py_record(state: dict, rec: dict, passed: bool, now: str) -> dict:
|
||||
state["completed_this_session"].append(
|
||||
{"behaviour_id": rec["behaviour_id"], "passed": passed,
|
||||
"at": now})
|
||||
if passed and rec["state"] == USER_CONFIRMED:
|
||||
pass # hardened; queue re-derives
|
||||
return state
|
||||
|
||||
|
||||
def _py_note_blocker(state: dict, behaviour_id: str, note: str, now: str) -> dict:
|
||||
state["blockers"].append({"behaviour_id": behaviour_id,
|
||||
"note": note[:200], "at": now})
|
||||
return state
|
||||
|
||||
|
||||
def _py_record_observation(*, now: str, now_ts: int, observed: str,
|
||||
expected: str, behaviour_id: str = "",
|
||||
environment: str = "", reproduction: str = "",
|
||||
candidate_areas: list[str] | None = None,
|
||||
confidence: str = "low") -> dict:
|
||||
"""Durable real-world failure report. Candidate areas are
|
||||
HYPOTHESES, not declared root cause (rule 19O)."""
|
||||
obs = {
|
||||
"observation_id": f"obs-{now_ts}",
|
||||
"observed": observed, "expected": expected,
|
||||
"behaviour_id": behaviour_id,
|
||||
"environment": environment, "reproduction": reproduction,
|
||||
"candidate_areas": list(candidate_areas or []), # hypotheses
|
||||
"confidence": confidence,
|
||||
"created_at": now,
|
||||
"closure": None, # set by close_observation
|
||||
}
|
||||
return obs
|
||||
|
||||
|
||||
def _py_close_observation(records: list, observation_id: str, *,
|
||||
behaviour_retest_passed: bool,
|
||||
user_confirmed: bool,
|
||||
contract_ref: str = "",
|
||||
now: str = "") -> tuple[list, dict | None]:
|
||||
"""Closure returns to the ORIGINAL observed scenario (rule 19O):
|
||||
code changed ≠ closure; automated green ≠ closure. The observation
|
||||
closes only when the behaviour was retested AND user-confirmed."""
|
||||
out = []
|
||||
result = None
|
||||
for rec in records:
|
||||
if rec.get("observation_id") == observation_id:
|
||||
rec["closure"] = {
|
||||
"behaviour_retest_passed": behaviour_retest_passed,
|
||||
"user_confirmed": user_confirmed,
|
||||
"contract_ref": contract_ref,
|
||||
"closed": behaviour_retest_passed and user_confirmed,
|
||||
"at": now}
|
||||
result = rec
|
||||
out.append(rec)
|
||||
return out, result
|
||||
|
||||
|
||||
def _py_project(behaviour_id: str, observation: dict) -> tuple[str, str]:
|
||||
"""19P forge projection title/body (the projection itself is a
|
||||
PROJECTION — canonical truth stays in the Behaviour Ledger)."""
|
||||
title = f"[testing] {behaviour_id}: {observation.get('expected', '')[:60]}"
|
||||
body = (f"Observed: {observation.get('observed', '')}\n"
|
||||
f"Expected: {observation.get('expected', '')}\n"
|
||||
f"Behaviour: {behaviour_id}\n"
|
||||
f"Observation: {observation.get('observation_id')}\n"
|
||||
f"Reproduction: {observation.get('reproduction', '')}")
|
||||
return title, body
|
||||
|
||||
|
||||
def _py_aggregate_rows(entries: list) -> list[dict]:
|
||||
"""19L aggregate row shaping over facade-fetched per-project
|
||||
summaries (summary None → no behaviour ledger yet)."""
|
||||
out = []
|
||||
for entry in entries:
|
||||
if entry["summary"] is None:
|
||||
out.append({"project": entry["project_id"],
|
||||
"outstanding": None,
|
||||
"note": "no behaviour ledger yet"})
|
||||
continue
|
||||
s = entry["summary"]
|
||||
out.append({"project": entry["project_id"],
|
||||
"outstanding": s["outstanding"],
|
||||
"states": s["states"]})
|
||||
return out
|
||||
|
||||
|
||||
# ── 19N: session = coordination only ─────────────────────────────────────
|
||||
|
||||
class TestingSession:
|
||||
@@ -40,11 +173,12 @@ class TestingSession:
|
||||
import json
|
||||
if self.path.is_file():
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": SESSION_SCHEMA_VERSION,
|
||||
"project": project, "scope_component": scope_component,
|
||||
"current_item": None, "completed_this_session": [],
|
||||
"newly_invalidated": [], "blockers": [],
|
||||
"paused": False, "updated_at": _now()}
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
return core.default_state(project=project,
|
||||
scope_component=scope_component,
|
||||
now=_now())
|
||||
return _py_default_state(project, scope_component, _now())
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
@@ -55,14 +189,17 @@ class TestingSession:
|
||||
# ── 19L: next eligible item ──────────────────────────────────────
|
||||
|
||||
def next_item(self, behaviours: BehaviourLedger) -> dict | None:
|
||||
if self.state["paused"]:
|
||||
return None
|
||||
outstanding = behaviours.outstanding(
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
outstanding = behaviours.outstanding(
|
||||
component=self.state.get("scope_component"),
|
||||
project=self.state.get("project") or None)
|
||||
out = core.next_item(state=self.state, outstanding=outstanding)
|
||||
self.state = out["state"]
|
||||
return out["item"]
|
||||
_, item = _py_next_item(self.state, behaviours.outstanding(
|
||||
component=self.state.get("scope_component"),
|
||||
project=self.state.get("project") or None)
|
||||
item = outstanding[0] if outstanding else None
|
||||
self.state["current_item"] = (item["behaviour_id"]
|
||||
if item else None)
|
||||
project=self.state.get("project") or None))
|
||||
return item
|
||||
|
||||
# ── 19M: record result; dynamic queue re-derives from states ─────
|
||||
@@ -73,25 +210,40 @@ class TestingSession:
|
||||
rec = behaviours.record_real_world(
|
||||
behaviour_id, passed=passed, detail=detail, confirmed_by=by,
|
||||
generation=generation)
|
||||
self.state["completed_this_session"].append(
|
||||
{"behaviour_id": rec["behaviour_id"], "passed": passed,
|
||||
"at": _now()})
|
||||
if passed and rec["state"] == USER_CONFIRMED:
|
||||
pass # hardened; queue re-derives
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
self.state = core.record(state=self.state, rec=rec,
|
||||
passed=passed, now=_now())
|
||||
else:
|
||||
self.state = _py_record(self.state, rec, passed, _now())
|
||||
self.save()
|
||||
return rec
|
||||
|
||||
def pause(self) -> None:
|
||||
self.state["paused"] = True
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
self.state = core.pause(state=self.state)
|
||||
else:
|
||||
self.state["paused"] = True
|
||||
self.save()
|
||||
|
||||
def resume(self) -> None:
|
||||
self.state["paused"] = False
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
self.state = core.resume(state=self.state)
|
||||
else:
|
||||
self.state["paused"] = False
|
||||
self.save()
|
||||
|
||||
def note_blocker(self, behaviour_id: str, note: str) -> None:
|
||||
self.state["blockers"].append({"behaviour_id": behaviour_id,
|
||||
"note": note[:200], "at": _now()})
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
self.state = core.note_blocker(state=self.state,
|
||||
behaviour_id=behaviour_id,
|
||||
note=note, now=_now())
|
||||
else:
|
||||
self.state = _py_note_blocker(self.state, behaviour_id, note,
|
||||
_now())
|
||||
self.save()
|
||||
|
||||
|
||||
@@ -105,16 +257,23 @@ def record_observation(store_dir: Path | str, *, observed: str,
|
||||
"""Durable real-world failure report. Candidate areas are
|
||||
HYPOTHESES, not declared root cause (rule 19O)."""
|
||||
import json
|
||||
obs = {
|
||||
"observation_id": f"obs-{int(time.time())}",
|
||||
"observed": observed, "expected": expected,
|
||||
"behaviour_id": behaviour_id,
|
||||
"environment": environment, "reproduction": reproduction,
|
||||
"candidate_areas": list(candidate_areas or []), # hypotheses
|
||||
"confidence": confidence,
|
||||
"created_at": _now(),
|
||||
"closure": None, # set by close_observation
|
||||
}
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
obs = core.record_observation(now=_now(), now_ts=int(time.time()),
|
||||
observed=observed, expected=expected,
|
||||
behaviour_id=behaviour_id,
|
||||
environment=environment,
|
||||
reproduction=reproduction,
|
||||
candidate_areas=list(candidate_areas or []),
|
||||
confidence=confidence)
|
||||
else:
|
||||
obs = _py_record_observation(now=_now(), now_ts=int(time.time()),
|
||||
observed=observed, expected=expected,
|
||||
behaviour_id=behaviour_id,
|
||||
environment=environment,
|
||||
reproduction=reproduction,
|
||||
candidate_areas=candidate_areas,
|
||||
confidence=confidence)
|
||||
p = Path(store_dir) / "observations.jsonl"
|
||||
with p.open("a") as f:
|
||||
f.write(json.dumps(obs, sort_keys=True) + "\n")
|
||||
@@ -131,20 +290,24 @@ def close_observation(store_dir: Path | str, observation_id: str, *,
|
||||
import json
|
||||
p = Path(store_dir) / "observations.jsonl"
|
||||
lines = p.read_text().strip().splitlines()
|
||||
out = []
|
||||
result = None
|
||||
for line in lines:
|
||||
rec = json.loads(line)
|
||||
if rec.get("observation_id") == observation_id:
|
||||
rec["closure"] = {
|
||||
"behaviour_retest_passed": behaviour_retest_passed,
|
||||
"user_confirmed": user_confirmed,
|
||||
"contract_ref": contract_ref,
|
||||
"closed": behaviour_retest_passed and user_confirmed,
|
||||
"at": _now()}
|
||||
result = rec
|
||||
out.append(json.dumps(rec, sort_keys=True))
|
||||
p.write_text("\n".join(out) + "\n")
|
||||
records = [json.loads(line) for line in lines]
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
out = core.close_observation(records=records,
|
||||
observation_id=observation_id,
|
||||
behaviour_retest_passed=behaviour_retest_passed,
|
||||
user_confirmed=user_confirmed,
|
||||
contract_ref=contract_ref,
|
||||
now=_now())
|
||||
out_records, result = out["records"], out["result"]
|
||||
else:
|
||||
out_records, result = _py_close_observation(
|
||||
records, observation_id,
|
||||
behaviour_retest_passed=behaviour_retest_passed,
|
||||
user_confirmed=user_confirmed, contract_ref=contract_ref,
|
||||
now=_now())
|
||||
p.write_text("\n".join(json.dumps(rec, sort_keys=True)
|
||||
for rec in out_records) + "\n")
|
||||
if result is None:
|
||||
raise ValueError(f"unknown observation {observation_id!r}")
|
||||
return result
|
||||
@@ -160,12 +323,13 @@ def project_failure_to_forge(forge_adapter, repo: str, *,
|
||||
in the Behaviour Ledger. Forge failure never rewrites truth
|
||||
(rule 19P/19S-14): exceptions propagate to the caller as
|
||||
ForgeUnavailable; the ledger state is untouched."""
|
||||
title = f"[testing] {behaviour_id}: {observation.get('expected', '')[:60]}"
|
||||
body = (f"Observed: {observation.get('observed', '')}\n"
|
||||
f"Expected: {observation.get('expected', '')}\n"
|
||||
f"Behaviour: {behaviour_id}\n"
|
||||
f"Observation: {observation.get('observation_id')}\n"
|
||||
f"Reproduction: {observation.get('reproduction', '')}")
|
||||
core = _tm_core()
|
||||
if core is not None:
|
||||
proj = core.project(behaviour_id=behaviour_id,
|
||||
observation=observation)
|
||||
title, body = proj["title"], proj["body"]
|
||||
else:
|
||||
title, body = _py_project(behaviour_id, observation)
|
||||
if dry_run:
|
||||
return {"projected": False, "dry_run": True, "title": title}
|
||||
return forge_adapter.create_or_patch_release(
|
||||
@@ -177,20 +341,22 @@ def project_failure_to_forge(forge_adapter, repo: str, *,
|
||||
def aggregate(registry: ProjectRegistry, store_root: Path) -> list[dict]:
|
||||
"""Outstanding validation across ALL registered scaffolded projects
|
||||
(the /testing aggregate)."""
|
||||
out = []
|
||||
core = _tm_core()
|
||||
entries = []
|
||||
for entry in registry.all_projects():
|
||||
loc = Path(entry["scaffold_location"])
|
||||
if not (loc / "behaviour_ledger.json").is_file():
|
||||
out.append({"project": entry["project_id"],
|
||||
"outstanding": None,
|
||||
"note": "no behaviour ledger yet"})
|
||||
entries.append({"project_id": entry["project_id"],
|
||||
"summary": None})
|
||||
continue
|
||||
beh = BehaviourLedger(loc)
|
||||
s = beh.summary()
|
||||
registry.set_outstanding_validations(entry["project_id"],
|
||||
s["outstanding"])
|
||||
out.append({"project": entry["project_id"],
|
||||
"outstanding": s["outstanding"],
|
||||
"states": s["states"]})
|
||||
entries.append({"project_id": entry["project_id"],
|
||||
"summary": beh.summary()})
|
||||
rows = (core.aggregate(entries=entries) if core is not None
|
||||
else _py_aggregate_rows(entries))
|
||||
for row in rows:
|
||||
if row["outstanding"] is not None:
|
||||
registry.set_outstanding_validations(row["project"],
|
||||
row["outstanding"])
|
||||
registry.save()
|
||||
return out
|
||||
return rows
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust task_gate core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* tg_record_instruction({instructions, text, now})
|
||||
→ {matched, id, count, status, promoted, last_seen | new_row}
|
||||
* tg_check_context({instructions, context}) → {matches}
|
||||
* tg_promote_gate({id, now}) → {status, promoted_at}
|
||||
* tg_handle_hook({event}) → {status, event, handler}
|
||||
|
||||
No-clock law: `now` (aware UTC ISO-8601) is always an argument — the
|
||||
kernel never reads the wall clock.
|
||||
|
||||
Boundary: the sqlite IO (connect, the row reads, the UPDATE/INSERT) and
|
||||
the CLI main() stay in the Python facade (task_gate.py); the kernel owns
|
||||
the verdict logic (pattern extraction, gate matching, count/threshold/
|
||||
status transitions, promotion, active-gate projection, hook echo).
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char* — the default c_int truncates 64-bit pointers).
|
||||
Strings are freed via capability_scoring's cs_free_string — the kernel
|
||||
exports NO free symbol of its own (duplicate exports break the cdylib
|
||||
link when sibling crates share one process).
|
||||
|
||||
Override the .so with TG_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" / "task_gate"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("TG_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtask_gate.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"tg_record_instruction",
|
||||
"tg_check_context",
|
||||
"tg_promote_gate",
|
||||
"tg_handle_hook",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class TaskGateCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust task_gate 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 TaskGateCoreUnavailable(
|
||||
f"Rust task_gate core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def record_instruction(instructions, text, now) -> dict:
|
||||
return _call("tg_record_instruction", {
|
||||
"instructions": instructions, "text": text, "now": now,
|
||||
})
|
||||
|
||||
|
||||
def check_context(instructions, context) -> dict:
|
||||
return _call("tg_check_context", {
|
||||
"instructions": instructions, "context": context,
|
||||
})
|
||||
|
||||
|
||||
def promote_gate(gate_id, now) -> dict:
|
||||
return _call("tg_promote_gate", {"id": gate_id, "now": now})
|
||||
|
||||
|
||||
def handle_hook(event) -> dict:
|
||||
return _call("tg_handle_hook", {"event": event})
|
||||
+120
-4
@@ -30,6 +30,8 @@ if _SD not in sys.path:
|
||||
sys.path.insert(0, _SD)
|
||||
import memory_env
|
||||
|
||||
_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemporalContext:
|
||||
@@ -106,13 +108,80 @@ class TimeAwareness:
|
||||
if now - self._last_save_time >= self._SAVE_DEBOUNCE_SECS:
|
||||
self._save()
|
||||
|
||||
# ── Rust kernel plumbing (dichotomy wave) ───────────────────────────
|
||||
# The kernel owns every temporal DECISION (time-of-day/circadian/
|
||||
# urgency branches, hours-since math, temporal-reference parsing,
|
||||
# dirty/debounce save policy); this facade keeps the I/O (_load/
|
||||
# _save), the clock and the CLI. `save: true` in the envelope means
|
||||
# the oracle wrote the file mid-call with the trigger-time snapshot —
|
||||
# we perform that write, then adopt the kernel's final state.
|
||||
|
||||
def _to_state(self) -> dict:
|
||||
"""Kernel transfer form; last_interaction as isoformat-or-null."""
|
||||
return {
|
||||
"last_interaction": self.last_interaction.isoformat() if self.last_interaction else None,
|
||||
"dirty": self._dirty,
|
||||
"last_save_time": self._last_save_time,
|
||||
}
|
||||
|
||||
def _apply_state(self, st: dict):
|
||||
self.last_interaction = (
|
||||
datetime.fromisoformat(st["last_interaction"])
|
||||
if st["last_interaction"] is not None
|
||||
else None
|
||||
)
|
||||
self._dirty = st["dirty"]
|
||||
self._last_save_time = st["last_save_time"]
|
||||
|
||||
def _write_snapshot(self, snap: dict):
|
||||
os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
|
||||
data = {"last_interaction": snap["last_interaction"]}
|
||||
tmp = self.storage_path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
os.replace(tmp, self.storage_path)
|
||||
|
||||
def _run_kernel(self, op: str, args: dict, now_us: int, now_mono: float):
|
||||
"""Run one op through the Rust kernel; None → fall back to the
|
||||
Python body (kernel absent, loader failure — never a broken call)."""
|
||||
core = _ta_core()
|
||||
if core is None:
|
||||
return None
|
||||
try:
|
||||
doc = core.run_op(op, self._to_state(), args, now_us, now_mono)
|
||||
except Exception:
|
||||
return None
|
||||
if doc.get("save"):
|
||||
self._write_snapshot(doc["snapshot"])
|
||||
self._apply_state(doc["state"])
|
||||
return doc["value"]
|
||||
|
||||
def get_context(self, update_interaction: bool = True) -> TemporalContext:
|
||||
"""Get current temporal context.
|
||||
|
||||
Args:
|
||||
update_interaction: If True, update last_interaction timestamp.
|
||||
Set to False for read-only status checks.
|
||||
"""
|
||||
Delegates the whole temporal decision to the Rust core when built."""
|
||||
now = datetime.now(timezone.utc)
|
||||
delegated = self._run_kernel(
|
||||
"get_context",
|
||||
{"update_interaction": update_interaction},
|
||||
(now - _EPOCH) // timedelta(microseconds=1),
|
||||
_time.monotonic(),
|
||||
)
|
||||
if delegated is not None:
|
||||
return TemporalContext(
|
||||
current_time=delegated["current_time"],
|
||||
current_date=delegated["current_date"],
|
||||
day_of_week=delegated["day_of_week"],
|
||||
time_of_day=delegated["time_of_day"],
|
||||
is_weekend=delegated["is_weekend"],
|
||||
hours_since_last_interaction=delegated["hours_since_last_interaction"],
|
||||
urgency_modifier=delegated["urgency_modifier"],
|
||||
circadian_phase=delegated["circadian_phase"],
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Calculate hours since last interaction
|
||||
@@ -174,7 +243,18 @@ class TimeAwareness:
|
||||
|
||||
def parse_temporal_reference(self, text: str) -> Dict[str, Any]:
|
||||
"""Parse temporal references in text (yesterday, last week, etc.).
|
||||
Uses word boundary matching to avoid false positives."""
|
||||
Uses word boundary matching to avoid false positives.
|
||||
Delegates the matching decision to the Rust core when built."""
|
||||
now = datetime.now(timezone.utc)
|
||||
delegated = self._run_kernel(
|
||||
"parse_temporal_reference",
|
||||
{"text": text},
|
||||
(now - _EPOCH) // timedelta(microseconds=1),
|
||||
_time.monotonic(),
|
||||
)
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
|
||||
text_lower = text.lower()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
@@ -206,7 +286,12 @@ class TimeAwareness:
|
||||
return {"reference": None}
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get time awareness status (read-only, no side effects)."""
|
||||
"""Get time awareness status (read-only, no side effects).
|
||||
Delegates to the Rust core when built."""
|
||||
delegated = self._run_kernel("get_status", {}, *_clock_args())
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
|
||||
context = self.get_context(update_interaction=False)
|
||||
return {
|
||||
"current_time": context.current_time,
|
||||
@@ -219,11 +304,42 @@ class TimeAwareness:
|
||||
}
|
||||
|
||||
def flush(self):
|
||||
"""Force-save any pending changes to disk."""
|
||||
"""Force-save any pending changes to disk.
|
||||
The save DECISION delegates to the Rust core when built (the
|
||||
facade performs the write via the snapshot)."""
|
||||
now = datetime.now(timezone.utc)
|
||||
delegated = self._run_kernel(
|
||||
"flush", {},
|
||||
(now - _EPOCH) // timedelta(microseconds=1),
|
||||
_time.monotonic(),
|
||||
)
|
||||
if delegated is not None:
|
||||
return
|
||||
if self._dirty:
|
||||
self._save()
|
||||
|
||||
|
||||
def _clock_args():
|
||||
"""(now_us, now_mono) for kernel ops that need no computed `now`."""
|
||||
now = datetime.now(timezone.utc)
|
||||
return (now - _EPOCH) // timedelta(microseconds=1), _time.monotonic()
|
||||
|
||||
|
||||
def _ta_core():
|
||||
"""The Rust time_awareness core when available (strangler law).
|
||||
RUNE_TA_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_TA_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ta_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return ta_loader if ta_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
# Global time awareness
|
||||
_time_awareness = None
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust toolchain core (dichotomy wave #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* tl_constraint_keys({}) -> sorted constraint-key vocabulary
|
||||
* tl_constraints({kwargs}) -> {"known", "unknown"}
|
||||
* tl_constraints_get({known, key, default?}) -> value
|
||||
* tl_decision_record({...fields}) -> normalized record dict
|
||||
* tl_resolve_toolchain({known, requested_language?, evidence_refs?,
|
||||
performance_evidence?}) -> DecisionRecord dict
|
||||
* tl_major_change({known, language}) -> bool
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with TC_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" / "toolchain"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("TC_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtoolchain.so")
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"tl_constraint_keys", "tl_constraints", "tl_constraints_get",
|
||||
"tl_decision_record", "tl_resolve_toolchain", "tl_major_change",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ToolchainKernelUnavailable(FileNotFoundError):
|
||||
"""The Rust toolchain 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 ToolchainKernelUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def constraint_keys() -> list:
|
||||
"""Sorted constraint-key vocabulary (the oracle's set, ordered)."""
|
||||
return _call("tl_constraint_keys", {})
|
||||
|
||||
|
||||
def constraints(kwargs: dict) -> dict:
|
||||
"""Classify kwargs into {"known": ..., "unknown": ...}."""
|
||||
return _call("tl_constraints", {"kwargs": kwargs})
|
||||
|
||||
|
||||
def constraints_get(known: dict, key: str, default=None):
|
||||
"""known.get(key, default) semantics (None default when absent)."""
|
||||
return _call("tl_constraints_get", {"known": known, "key": key,
|
||||
"default": default})
|
||||
|
||||
|
||||
def decision_record(**fields) -> dict:
|
||||
"""Normalized DecisionRecord dict (lists sorted, None → [])."""
|
||||
return _call("tl_decision_record", fields)
|
||||
|
||||
|
||||
def resolve_toolchain(known: dict, requested_language: str | None = None,
|
||||
evidence_refs: list | None = None,
|
||||
performance_evidence: dict | None = None) -> dict:
|
||||
return _call("tl_resolve_toolchain", {
|
||||
"known": known, "requested_language": requested_language,
|
||||
"evidence_refs": list(evidence_refs or []),
|
||||
"performance_evidence": performance_evidence})
|
||||
|
||||
|
||||
def major_change(known: dict, language: str) -> bool:
|
||||
return _call("tl_major_change", {"known": known, "language": language})
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust testing_mode core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* tm_default_state({project, scope_component, now}) -> {state}
|
||||
* tm_next_item({state, outstanding}) -> {state, item}
|
||||
* tm_record({state, rec, passed, now}) -> {state}
|
||||
* tm_pause({state}) -> {state}
|
||||
* tm_resume({state}) -> {state}
|
||||
* tm_note_blocker({state, behaviour_id, note, now}) -> {state}
|
||||
* tm_record_observation({now, now_ts, observed, expected, behaviour_id,
|
||||
environment, reproduction, candidate_areas, confidence})
|
||||
-> {observation}
|
||||
* tm_close_observation({records, observation_id,
|
||||
behaviour_retest_passed, user_confirmed, contract_ref, now})
|
||||
-> {records, result}
|
||||
* tm_project({behaviour_id, observation}) -> {title, body}
|
||||
* tm_aggregate({entries}) -> {rows}
|
||||
|
||||
No-clock law: `now` (UTC ISO-8601) and `now_ts` (epoch seconds, for the
|
||||
oracle's obs-{int(time.time())} id) are always arguments to the stamping
|
||||
calls — the kernel never reads the wall clock.
|
||||
|
||||
Boundary: the session FILE IO (TestingSession._load hit branch / save),
|
||||
the observations.jsonl append/rewrite, the BehaviourLedger read
|
||||
(outstanding / record_real_world), the ProjectRegistry IO and the forge
|
||||
adapter call stay in the Python facade (testing_mode.py); the kernel is
|
||||
a pure function of their results.
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char* — the default c_int truncates 64-bit pointers).
|
||||
Strings are freed via capability_scoring's cs_free_string — the kernel
|
||||
exports NO free symbol of its own (duplicate exports break the cdylib
|
||||
link when sibling crates share one process).
|
||||
|
||||
Override the .so with TM_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" / "testing_mode"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"TM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtesting_mode.so"
|
||||
)
|
||||
)
|
||||
|
||||
_SYMBOLS = (
|
||||
"tm_default_state",
|
||||
"tm_next_item",
|
||||
"tm_record",
|
||||
"tm_pause",
|
||||
"tm_resume",
|
||||
"tm_note_blocker",
|
||||
"tm_record_observation",
|
||||
"tm_close_observation",
|
||||
"tm_project",
|
||||
"tm_aggregate",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class TestingModeCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust testing_mode 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 TestingModeCoreUnavailable(
|
||||
f"Rust testing_mode core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def default_state(project: str, scope_component: str | None, now: str) -> dict:
|
||||
return _call("tm_default_state", {"project": project,
|
||||
"scope_component": scope_component,
|
||||
"now": now})["state"]
|
||||
|
||||
|
||||
def next_item(state: dict, outstanding: list) -> dict:
|
||||
return _call("tm_next_item", {"state": state, "outstanding": outstanding})
|
||||
|
||||
|
||||
def record(state: dict, rec: dict, passed: bool, now: str) -> dict:
|
||||
return _call("tm_record", {"state": state, "rec": rec, "passed": passed,
|
||||
"now": now})["state"]
|
||||
|
||||
|
||||
def pause(state: dict) -> dict:
|
||||
return _call("tm_pause", {"state": state})["state"]
|
||||
|
||||
|
||||
def resume(state: dict) -> dict:
|
||||
return _call("tm_resume", {"state": state})["state"]
|
||||
|
||||
|
||||
def note_blocker(state: dict, behaviour_id: str, note: str, now: str) -> dict:
|
||||
return _call("tm_note_blocker", {"state": state,
|
||||
"behaviour_id": behaviour_id,
|
||||
"note": note, "now": now})["state"]
|
||||
|
||||
|
||||
def record_observation(now: str, now_ts: int, *, observed: str,
|
||||
expected: str, behaviour_id: str = "",
|
||||
environment: str = "", reproduction: str = "",
|
||||
candidate_areas: list | None = None,
|
||||
confidence: str = "low") -> dict:
|
||||
return _call("tm_record_observation", {
|
||||
"now": now, "now_ts": now_ts, "observed": observed,
|
||||
"expected": expected, "behaviour_id": behaviour_id,
|
||||
"environment": environment, "reproduction": reproduction,
|
||||
"candidate_areas": candidate_areas or [],
|
||||
"confidence": confidence})["observation"]
|
||||
|
||||
|
||||
def close_observation(records: list, observation_id: str, *,
|
||||
behaviour_retest_passed: bool,
|
||||
user_confirmed: bool, contract_ref: str = "",
|
||||
now: str = "") -> dict:
|
||||
return _call("tm_close_observation", {
|
||||
"records": records, "observation_id": observation_id,
|
||||
"behaviour_retest_passed": behaviour_retest_passed,
|
||||
"user_confirmed": user_confirmed, "contract_ref": contract_ref,
|
||||
"now": now})
|
||||
|
||||
|
||||
def project(behaviour_id: str, observation: dict) -> dict:
|
||||
return _call("tm_project", {"behaviour_id": behaviour_id,
|
||||
"observation": observation})
|
||||
|
||||
|
||||
def aggregate(entries: list) -> list:
|
||||
return _call("tm_aggregate", {"entries": entries})["rows"]
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust token_optimizer core (dichotomy wave,
|
||||
#2479 tranche). Mirrors ci_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* to_run({op, state, args, now_iso, now}) ->
|
||||
{"ok": true, "value": ..., "state": {...mutated...},
|
||||
"save": bool, "snapshot": {...}}
|
||||
|
||||
The kernel is PURE — no clock, no filesystem. The caller supplies the
|
||||
state (cache/stats/dirty/changes_since_save/last_save_time; cache as an
|
||||
ORDERED ARRAY — insertion order is load-bearing for get_cache_stats)
|
||||
and the wall clock (now_iso) + monotonic reading (now). `save: true`
|
||||
means the oracle would have written the file mid-call; `snapshot` is
|
||||
the trigger-time file content (cache as an ordered array) — the facade
|
||||
performs the write. All I/O, the persona import glue and the CLI stay
|
||||
in Python.
|
||||
|
||||
Override the .so with TO_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" / "token_optimizer"
|
||||
_LIB_PATH = Path(os.environ.get("TO_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtoken_optimizer.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class TokenOptimizerCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust token_optimizer 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 TokenOptimizerCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.to_run.argtypes = [ctypes.c_void_p]
|
||||
lib.to_run.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.to_run(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("to_run returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "to_run failed"))
|
||||
return doc
|
||||
|
||||
|
||||
def run_op(op: str, state: dict, args: dict | None = None, now_iso: str = "", now: float = 0.0) -> dict:
|
||||
"""Run one kernel op. Returns the full envelope:
|
||||
{"value": ..., "state": {...mutated...}, "save": bool, "snapshot": ...}.
|
||||
"""
|
||||
payload = {"op": op, "state": state, "args": args or {},
|
||||
"now_iso": now_iso, "now": now}
|
||||
return _call(payload)
|
||||
+110
-6
@@ -115,7 +115,13 @@ class TokenOptimizer:
|
||||
self._changes_since_save += 1
|
||||
|
||||
def _save_if_dirty(self):
|
||||
"""Flush to disk only when dirty AND (enough changes OR enough time)."""
|
||||
"""Flush to disk only when dirty AND (enough changes OR enough time).
|
||||
The threshold/debounce DECISION delegates to the Rust core when
|
||||
built (the kernel returns the decision; the write happens here or
|
||||
inside _run_kernel via the snapshot)."""
|
||||
delegated = self._run_kernel("save_if_dirty", {})
|
||||
if delegated is not None:
|
||||
return
|
||||
if not self._dirty:
|
||||
return
|
||||
now = time.monotonic()
|
||||
@@ -149,12 +155,74 @@ class TokenOptimizer:
|
||||
"root, respect how Nick thinks, and make the agent smarter every session."
|
||||
)
|
||||
|
||||
# ── Rust core delegation (dichotomy wave, #2479 tranche) ────────────
|
||||
# The kernel owns every cache/state DECISION (hit/miss, md5-keyed
|
||||
# upsert, savings math, dirty/debounce/threshold save policy); this
|
||||
# facade keeps the I/O (_load/_save), the clock, the persona glue and
|
||||
# the CLI. `save: true` in the envelope means the oracle wrote the
|
||||
# file mid-call with the trigger-time snapshot — we perform that
|
||||
# write, then adopt the kernel's final state (post-trigger mutations
|
||||
# stay dirty in memory, exactly like the oracle).
|
||||
|
||||
def _to_state(self) -> dict:
|
||||
"""Kernel transfer form; cache as an ORDERED ARRAY (insertion
|
||||
order is load-bearing for get_cache_stats)."""
|
||||
return {
|
||||
"cache": [e.to_dict() for e in self.cache.values()],
|
||||
"stats": self.stats,
|
||||
"dirty": self._dirty,
|
||||
"changes_since_save": self._changes_since_save,
|
||||
"last_save_time": self._last_save_time,
|
||||
}
|
||||
|
||||
def _apply_state(self, st: dict):
|
||||
self.cache = {e["key"]: CacheEntry(**e) for e in st["cache"]}
|
||||
self.stats = dict(st["stats"])
|
||||
self._dirty = st["dirty"]
|
||||
self._changes_since_save = st["changes_since_save"]
|
||||
self._last_save_time = st["last_save_time"]
|
||||
|
||||
def _write_snapshot(self, snap: dict):
|
||||
os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
|
||||
data = {
|
||||
"cache": {e["key"]: e for e in snap["cache"]},
|
||||
"stats": snap["stats"],
|
||||
}
|
||||
tmp = self.storage_path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
os.replace(tmp, self.storage_path)
|
||||
|
||||
def _run_kernel(self, op: str, args: dict):
|
||||
"""Run one op through the Rust kernel; None → fall back to the
|
||||
Python body (kernel absent, loader failure — never a broken call)."""
|
||||
core = _to_core()
|
||||
if core is None:
|
||||
return None
|
||||
try:
|
||||
doc = core.run_op(op, self._to_state(), args,
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
time.monotonic())
|
||||
except Exception:
|
||||
return None
|
||||
if doc.get("save"):
|
||||
self._write_snapshot(doc["snapshot"])
|
||||
self._apply_state(doc["state"])
|
||||
return doc["value"]
|
||||
|
||||
def get_static_cache(self) -> str:
|
||||
"""Get static cached content (constitution, temporal context).
|
||||
|
||||
This content rarely changes and can be cached across calls.
|
||||
Loads from persona_definition module when available.
|
||||
"""
|
||||
Delegates the hit/miss decision to the Rust core when built."""
|
||||
delegated = self._run_kernel(
|
||||
"get_static_cache",
|
||||
{"persona_text": self._load_persona_definition()},
|
||||
)
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
|
||||
# Check cache
|
||||
if "static" in self.cache:
|
||||
entry = self.cache["static"]
|
||||
@@ -187,7 +255,11 @@ class TokenOptimizer:
|
||||
"""Get dynamic cached content (recent messages, task context).
|
||||
|
||||
This content changes per turn but can be compressed.
|
||||
"""
|
||||
Delegates the md5-keyed upsert decision to the Rust core when built."""
|
||||
delegated = self._run_kernel("get_dynamic_cache", {"context": context})
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
|
||||
# Compress context
|
||||
compressed = self._compress_context(context)
|
||||
|
||||
@@ -218,7 +290,17 @@ class TokenOptimizer:
|
||||
|
||||
|
||||
def optimize_prompt(self, text: str, context: str = "") -> Dict[str, Any]:
|
||||
"""Optimize a prompt by caching and compressing."""
|
||||
"""Optimize a prompt by caching and compressing.
|
||||
Delegates the full call sequence (static, dynamic, savings math,
|
||||
accounting view) to the Rust core when built."""
|
||||
delegated = self._run_kernel(
|
||||
"optimize_prompt",
|
||||
{"text": text, "context": context,
|
||||
"persona_text": self._load_persona_definition()},
|
||||
)
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
|
||||
self.stats["total_calls"] += 1
|
||||
|
||||
# Get static cache (constitution)
|
||||
@@ -271,7 +353,10 @@ class TokenOptimizer:
|
||||
}
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get optimization status."""
|
||||
"""Get optimization status (Rust core when built)."""
|
||||
delegated = self._run_kernel("get_status", {})
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
return {
|
||||
"cache_entries": len(self.cache),
|
||||
"total_calls": self.stats["total_calls"],
|
||||
@@ -282,7 +367,11 @@ class TokenOptimizer:
|
||||
}
|
||||
|
||||
def get_cache_stats(self) -> List[Dict[str, Any]]:
|
||||
"""Get cache statistics."""
|
||||
"""Get cache statistics (Rust core when built; entry order is the
|
||||
Python dict's insertion order)."""
|
||||
delegated = self._run_kernel("get_cache_stats", {})
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
return [entry.to_dict() for entry in self.cache.values()]
|
||||
|
||||
def flush(self):
|
||||
@@ -321,6 +410,21 @@ def _tc_core():
|
||||
return None
|
||||
|
||||
|
||||
def _to_core():
|
||||
"""The Rust token_optimizer core when available (strangler law).
|
||||
RUNE_TOKEN_OPTIMIZER_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_TOKEN_OPTIMIZER_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import to_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return to_loader if to_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def _py_estimate_tokens(text: str) -> int:
|
||||
"""STRANGLER EXIT (retired 2026-10-10): the authoritative implementation
|
||||
lives in src/rust/token_counting (tc_estimate_tokens, parity proven 26/26
|
||||
|
||||
@@ -17,8 +17,32 @@ never invented certainty.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
__all__ = ["ToolchainConstraints", "resolve_toolchain", "DecisionRecord", "CONSTRAINT_KEYS"]
|
||||
|
||||
|
||||
def _rust(fn: str, *args):
|
||||
"""Delegate to the Rust kernel (dichotomy wave #2479); returns
|
||||
_MISSING when the core is unavailable so the Python oracle body runs.
|
||||
RUNE_TC_CORE=python pins the oracle unconditionally."""
|
||||
if os.environ.get("RUNE_TC_CORE", "auto") == "python":
|
||||
return _MISSING
|
||||
try:
|
||||
from rune import tl_loader
|
||||
if not tl_loader.available():
|
||||
return _MISSING
|
||||
return getattr(tl_loader, fn)(*args)
|
||||
except Exception:
|
||||
return _MISSING
|
||||
|
||||
|
||||
class _Missing:
|
||||
pass
|
||||
|
||||
|
||||
_MISSING = _Missing()
|
||||
|
||||
# Small fixed set of constraint KEYS the resolver can weigh. Values are
|
||||
# open-world. Nothing here names a language.
|
||||
CONSTRAINT_KEYS = {
|
||||
@@ -46,6 +70,11 @@ class ToolchainConstraints:
|
||||
carried verbatim (future-proof without schema churn)."""
|
||||
|
||||
def __init__(self, **kw):
|
||||
out = _rust("constraints", kw)
|
||||
if out is not _MISSING:
|
||||
self.known = out["known"]
|
||||
self.unknown = out["unknown"]
|
||||
return
|
||||
self.known = {}
|
||||
self.unknown = {}
|
||||
for k, v in kw.items():
|
||||
@@ -55,6 +84,9 @@ class ToolchainConstraints:
|
||||
self.unknown[k] = v
|
||||
|
||||
def get(self, key, default=None):
|
||||
out = _rust("constraints_get", self.known, key, default)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
return self.known.get(key, default)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -80,6 +112,26 @@ class DecisionRecord:
|
||||
user_approval_required: bool = False,
|
||||
resolution: str = "",
|
||||
):
|
||||
out = _rust(
|
||||
"decision_record",
|
||||
selected_language, runtime, framework, libraries, build_system,
|
||||
alternatives, decisive_constraints, rationale, evidence_refs,
|
||||
confidence, user_approval_required, resolution,
|
||||
)
|
||||
if out is not _MISSING:
|
||||
self.selected_language = out["selected_language"]
|
||||
self.runtime = out["runtime"]
|
||||
self.framework = out["framework"]
|
||||
self.libraries = out["libraries"]
|
||||
self.build_system = out["build_system"]
|
||||
self.alternatives = out["alternatives"]
|
||||
self.decisive_constraints = out["decisive_constraints"]
|
||||
self.rationale = out["rationale"]
|
||||
self.evidence_refs = out["evidence_refs"]
|
||||
self.confidence = out["confidence"]
|
||||
self.user_approval_required = out["user_approval_required"]
|
||||
self.resolution = out["resolution"]
|
||||
return
|
||||
self.selected_language = selected_language
|
||||
self.runtime = runtime
|
||||
self.framework = framework
|
||||
@@ -113,6 +165,9 @@ class DecisionRecord:
|
||||
def _major_change(constraints: ToolchainConstraints, language: str) -> bool:
|
||||
"""A major toolchain change = introducing/changing a language, runtime or
|
||||
framework the project does not already use (tranche brief §10)."""
|
||||
out = _rust("major_change", constraints.known, language)
|
||||
if out is not _MISSING:
|
||||
return out
|
||||
existing = constraints.get("existing_language")
|
||||
return bool(existing) and language != existing
|
||||
|
||||
@@ -137,6 +192,10 @@ def resolve_toolchain(
|
||||
already uses it or the user asked.
|
||||
"""
|
||||
refs = sorted(evidence_refs or [])
|
||||
out = _rust("resolve_toolchain", constraints.known, requested_language,
|
||||
evidence_refs, performance_evidence)
|
||||
if out is not _MISSING:
|
||||
return DecisionRecord(**out)
|
||||
existing = constraints.get("existing_language")
|
||||
|
||||
# INSUFFICIENT EVIDENCE path: no existing toolchain recorded AND no
|
||||
|
||||
Generated
+39
@@ -2,10 +2,20 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "behavior"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -40,6 +50,35 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
|
||||
@@ -4,8 +4,10 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "behavior"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
regex = "1"
|
||||
@@ -0,0 +1,744 @@
|
||||
{
|
||||
"frozen": "behavior decision surface (reinforce math via ep strangler shim)",
|
||||
"oracle": "src/rune/behavior.py",
|
||||
"cases": {
|
||||
"add_correction": [
|
||||
{
|
||||
"name": "defaults",
|
||||
"args": [
|
||||
"say X",
|
||||
"say Y"
|
||||
],
|
||||
"expected": {
|
||||
"added": 1,
|
||||
"confidence": 0.5,
|
||||
"scope": {}
|
||||
},
|
||||
"row": {
|
||||
"id": 1,
|
||||
"original_behavior": "say X",
|
||||
"corrected_behavior": "say Y",
|
||||
"scope_tags": "{}",
|
||||
"scope_pattern": "",
|
||||
"scope_domain": "",
|
||||
"evidence_count": 1,
|
||||
"confidence": 0.5,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.934815+00:00",
|
||||
"last_reinforced": null,
|
||||
"supersedes_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "scoped",
|
||||
"args": [
|
||||
"orig",
|
||||
"corr"
|
||||
],
|
||||
"expected": {
|
||||
"added": 2,
|
||||
"confidence": 0.5,
|
||||
"scope": {
|
||||
"k": "v"
|
||||
}
|
||||
},
|
||||
"row": {
|
||||
"id": 2,
|
||||
"original_behavior": "orig",
|
||||
"corrected_behavior": "corr",
|
||||
"scope_tags": "{\"k\": \"v\"}",
|
||||
"scope_pattern": "shell",
|
||||
"scope_domain": "shell",
|
||||
"evidence_count": 1,
|
||||
"confidence": 0.5,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.935466+00:00",
|
||||
"last_reinforced": null,
|
||||
"supersedes_id": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"add_habit": [
|
||||
{
|
||||
"name": "defaults",
|
||||
"expected": {
|
||||
"added": 1,
|
||||
"state": "candidate",
|
||||
"min_activations": 3
|
||||
},
|
||||
"row": {
|
||||
"id": 1,
|
||||
"name": "",
|
||||
"trigger_pattern": "trig",
|
||||
"context_tags": "[]",
|
||||
"action_template": "act",
|
||||
"activation_count": 0,
|
||||
"success_count": 0,
|
||||
"failure_count": 0,
|
||||
"min_activations": 3,
|
||||
"confidence": 0.5,
|
||||
"decay_rate": 0.95,
|
||||
"state": "candidate",
|
||||
"created_at": "2026-10-11T16:42:08.936042+00:00",
|
||||
"last_activated": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "full",
|
||||
"expected": {
|
||||
"added": 2,
|
||||
"state": "candidate",
|
||||
"min_activations": 5
|
||||
},
|
||||
"row": {
|
||||
"id": 2,
|
||||
"name": "n",
|
||||
"trigger_pattern": "trig2",
|
||||
"context_tags": "[\"a\"]",
|
||||
"action_template": "act2",
|
||||
"activation_count": 0,
|
||||
"success_count": 0,
|
||||
"failure_count": 0,
|
||||
"min_activations": 5,
|
||||
"confidence": 0.5,
|
||||
"decay_rate": 0.95,
|
||||
"state": "candidate",
|
||||
"created_at": "2026-10-11T16:42:08.936613+00:00",
|
||||
"last_activated": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"name": "pattern-domain-hit",
|
||||
"context": "deploy to shell now",
|
||||
"match_type": null,
|
||||
"corrections": [
|
||||
"corr",
|
||||
"plain corr",
|
||||
"say Y",
|
||||
"scoped corr"
|
||||
],
|
||||
"habits": [
|
||||
"run deploy script"
|
||||
],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
},
|
||||
{
|
||||
"name": "domain-only-miss",
|
||||
"context": "deploy the editor",
|
||||
"match_type": null,
|
||||
"corrections": [
|
||||
"domain corr",
|
||||
"plain corr",
|
||||
"say Y"
|
||||
],
|
||||
"habits": [
|
||||
"run deploy script"
|
||||
],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
},
|
||||
{
|
||||
"name": "plain-always",
|
||||
"context": "unrelated chatter here",
|
||||
"match_type": null,
|
||||
"corrections": [
|
||||
"plain corr",
|
||||
"say Y"
|
||||
],
|
||||
"habits": [],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
},
|
||||
{
|
||||
"name": "type-habit-only",
|
||||
"context": "deploy to shell now",
|
||||
"match_type": "habit",
|
||||
"corrections": [],
|
||||
"habits": [
|
||||
"run deploy script"
|
||||
],
|
||||
"n_corr_fields": [],
|
||||
"scope_tags_type": null
|
||||
},
|
||||
{
|
||||
"name": "type-correction-only",
|
||||
"context": "deploy to shell now",
|
||||
"match_type": "correction",
|
||||
"corrections": [
|
||||
"corr",
|
||||
"plain corr",
|
||||
"say Y",
|
||||
"scoped corr"
|
||||
],
|
||||
"habits": [],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
},
|
||||
{
|
||||
"name": "habit-regex-hit",
|
||||
"context": "run the unit tests",
|
||||
"match_type": null,
|
||||
"corrections": [
|
||||
"plain corr",
|
||||
"say Y"
|
||||
],
|
||||
"habits": [],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
},
|
||||
{
|
||||
"name": "low-conf-filtered",
|
||||
"context": "zzz-no-match-ever quiet",
|
||||
"match_type": null,
|
||||
"corrections": [
|
||||
"plain corr",
|
||||
"say Y"
|
||||
],
|
||||
"habits": [],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
},
|
||||
{
|
||||
"name": "bad-regex-skipped",
|
||||
"context": "([unclosed anything",
|
||||
"match_type": null,
|
||||
"corrections": [
|
||||
"plain corr",
|
||||
"say Y"
|
||||
],
|
||||
"habits": [],
|
||||
"n_corr_fields": [
|
||||
"confidence",
|
||||
"corrected_behavior",
|
||||
"created_at",
|
||||
"evidence_count",
|
||||
"id",
|
||||
"last_reinforced",
|
||||
"original_behavior",
|
||||
"scope_domain",
|
||||
"scope_pattern",
|
||||
"scope_tags",
|
||||
"state",
|
||||
"supersedes_id"
|
||||
],
|
||||
"scope_tags_type": "dict"
|
||||
}
|
||||
],
|
||||
"reinforce_habit": [
|
||||
{
|
||||
"name": "success-promotes-at-3",
|
||||
"item_type": "habit",
|
||||
"outcome": "success",
|
||||
"pre": {},
|
||||
"n": 3,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 7,
|
||||
"confidence": 1,
|
||||
"state": "candidate"
|
||||
},
|
||||
{
|
||||
"reinforced": 7,
|
||||
"confidence": 1,
|
||||
"state": "candidate"
|
||||
},
|
||||
{
|
||||
"reinforced": 7,
|
||||
"confidence": 1,
|
||||
"state": "active"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 7,
|
||||
"name": "",
|
||||
"trigger_pattern": "t1",
|
||||
"context_tags": "[]",
|
||||
"action_template": "a1",
|
||||
"activation_count": 3,
|
||||
"success_count": 3,
|
||||
"failure_count": 0,
|
||||
"min_activations": 3,
|
||||
"confidence": 1.0,
|
||||
"decay_rate": 0.95,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.943499+00:00",
|
||||
"last_activated": "2026-10-11T16:42:08.944671+00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "failure-counts",
|
||||
"item_type": "habit",
|
||||
"outcome": "failure",
|
||||
"pre": {},
|
||||
"n": 2,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 8,
|
||||
"confidence": 0.5,
|
||||
"state": "candidate"
|
||||
},
|
||||
{
|
||||
"reinforced": 8,
|
||||
"confidence": 0.5,
|
||||
"state": "candidate"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 8,
|
||||
"name": "",
|
||||
"trigger_pattern": "t2",
|
||||
"context_tags": "[]",
|
||||
"action_template": "a2",
|
||||
"activation_count": 2,
|
||||
"success_count": 0,
|
||||
"failure_count": 2,
|
||||
"min_activations": 3,
|
||||
"confidence": 0.5,
|
||||
"decay_rate": 0.95,
|
||||
"state": "candidate",
|
||||
"created_at": "2026-10-11T16:42:08.945183+00:00",
|
||||
"last_activated": "2026-10-11T16:42:08.945790+00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neutral-bumps-activation-only",
|
||||
"item_type": "habit",
|
||||
"outcome": "neutral",
|
||||
"pre": {},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 9,
|
||||
"confidence": 0.5,
|
||||
"state": "candidate"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 9,
|
||||
"name": "",
|
||||
"trigger_pattern": "t3",
|
||||
"context_tags": "[]",
|
||||
"action_template": "a3",
|
||||
"activation_count": 1,
|
||||
"success_count": 0,
|
||||
"failure_count": 0,
|
||||
"min_activations": 3,
|
||||
"confidence": 0.5,
|
||||
"decay_rate": 0.95,
|
||||
"state": "candidate",
|
||||
"created_at": "2026-10-11T16:42:08.946348+00:00",
|
||||
"last_activated": "2026-10-11T16:42:08.946617+00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "already-active-stays",
|
||||
"item_type": "habit",
|
||||
"outcome": "success",
|
||||
"pre": {},
|
||||
"n": 2,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 10,
|
||||
"confidence": 1,
|
||||
"state": "active"
|
||||
},
|
||||
{
|
||||
"reinforced": 10,
|
||||
"confidence": 1,
|
||||
"state": "active"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 10,
|
||||
"name": "",
|
||||
"trigger_pattern": "t4",
|
||||
"context_tags": "[]",
|
||||
"action_template": "a4",
|
||||
"activation_count": 2,
|
||||
"success_count": 2,
|
||||
"failure_count": 0,
|
||||
"min_activations": 1,
|
||||
"confidence": 1.0,
|
||||
"decay_rate": 0.95,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.947163+00:00",
|
||||
"last_activated": "2026-10-11T16:42:08.947743+00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "min5-gate",
|
||||
"item_type": "habit",
|
||||
"outcome": "success",
|
||||
"pre": {},
|
||||
"n": 4,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 11,
|
||||
"confidence": 1,
|
||||
"state": "candidate"
|
||||
},
|
||||
{
|
||||
"reinforced": 11,
|
||||
"confidence": 1,
|
||||
"state": "candidate"
|
||||
},
|
||||
{
|
||||
"reinforced": 11,
|
||||
"confidence": 1,
|
||||
"state": "candidate"
|
||||
},
|
||||
{
|
||||
"reinforced": 11,
|
||||
"confidence": 1,
|
||||
"state": "candidate"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 11,
|
||||
"name": "",
|
||||
"trigger_pattern": "t5",
|
||||
"context_tags": "[]",
|
||||
"action_template": "a5",
|
||||
"activation_count": 4,
|
||||
"success_count": 4,
|
||||
"failure_count": 0,
|
||||
"min_activations": 5,
|
||||
"confidence": 1.0,
|
||||
"decay_rate": 0.95,
|
||||
"state": "candidate",
|
||||
"created_at": "2026-10-11T16:42:08.948282+00:00",
|
||||
"last_activated": "2026-10-11T16:42:08.949492+00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "habit-not-found",
|
||||
"item_type": "habit",
|
||||
"outcome": "success",
|
||||
"pre": {},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"error": "Habit 99991 not found"
|
||||
}
|
||||
],
|
||||
"final_row": null
|
||||
}
|
||||
],
|
||||
"reinforce_correction": [
|
||||
{
|
||||
"name": "success-plus-point-one",
|
||||
"item_type": "correction",
|
||||
"outcome": "success",
|
||||
"pre": {},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 8,
|
||||
"confidence": 0.6,
|
||||
"state": "active"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 8,
|
||||
"original_behavior": "x1",
|
||||
"corrected_behavior": "y1",
|
||||
"scope_tags": "{}",
|
||||
"scope_pattern": "",
|
||||
"scope_domain": "",
|
||||
"evidence_count": 2,
|
||||
"confidence": 0.6,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.950028+00:00",
|
||||
"last_reinforced": "2026-10-11T16:42:08.950309+00:00",
|
||||
"supersedes_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "failure-archives",
|
||||
"item_type": "correction",
|
||||
"outcome": "failure",
|
||||
"pre": {},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 9,
|
||||
"confidence": 0.2,
|
||||
"state": "archived"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 9,
|
||||
"original_behavior": "x2",
|
||||
"corrected_behavior": "y2",
|
||||
"scope_tags": "{}",
|
||||
"scope_pattern": "",
|
||||
"scope_domain": "",
|
||||
"evidence_count": 1,
|
||||
"confidence": 0.2,
|
||||
"state": "archived",
|
||||
"created_at": "2026-10-11T16:42:08.950837+00:00",
|
||||
"last_reinforced": "2026-10-11T16:42:08.951121+00:00",
|
||||
"supersedes_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "double-failure-floor",
|
||||
"item_type": "correction",
|
||||
"outcome": "failure",
|
||||
"pre": {},
|
||||
"n": 3,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 10,
|
||||
"confidence": 0.2,
|
||||
"state": "archived"
|
||||
},
|
||||
{
|
||||
"reinforced": 10,
|
||||
"confidence": 0,
|
||||
"state": "archived"
|
||||
},
|
||||
{
|
||||
"reinforced": 10,
|
||||
"confidence": 0,
|
||||
"state": "archived"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 10,
|
||||
"original_behavior": "x3",
|
||||
"corrected_behavior": "y3",
|
||||
"scope_tags": "{}",
|
||||
"scope_pattern": "",
|
||||
"scope_domain": "",
|
||||
"evidence_count": 1,
|
||||
"confidence": 0.0,
|
||||
"state": "archived",
|
||||
"created_at": "2026-10-11T16:42:08.951619+00:00",
|
||||
"last_reinforced": "2026-10-11T16:42:08.952462+00:00",
|
||||
"supersedes_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ceiling-at-one",
|
||||
"item_type": "correction",
|
||||
"outcome": "success",
|
||||
"pre": {
|
||||
"confidence": 0.95
|
||||
},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 11,
|
||||
"confidence": 1,
|
||||
"state": "active"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 11,
|
||||
"original_behavior": "x4",
|
||||
"corrected_behavior": "y4",
|
||||
"scope_tags": "{}",
|
||||
"scope_pattern": "",
|
||||
"scope_domain": "",
|
||||
"evidence_count": 2,
|
||||
"confidence": 1.0,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.952951+00:00",
|
||||
"last_reinforced": "2026-10-11T16:42:08.953429+00:00",
|
||||
"supersedes_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neutral-noop",
|
||||
"item_type": "correction",
|
||||
"outcome": "neutral",
|
||||
"pre": {},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"reinforced": 12,
|
||||
"confidence": 0.5,
|
||||
"state": "active"
|
||||
}
|
||||
],
|
||||
"final_row": {
|
||||
"id": 12,
|
||||
"original_behavior": "x5",
|
||||
"corrected_behavior": "y5",
|
||||
"scope_tags": "{}",
|
||||
"scope_pattern": "",
|
||||
"scope_domain": "",
|
||||
"evidence_count": 1,
|
||||
"confidence": 0.5,
|
||||
"state": "active",
|
||||
"created_at": "2026-10-11T16:42:08.953912+00:00",
|
||||
"last_reinforced": "2026-10-11T16:42:08.954167+00:00",
|
||||
"supersedes_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "correction-not-found",
|
||||
"item_type": "correction",
|
||||
"outcome": "success",
|
||||
"pre": {},
|
||||
"n": 1,
|
||||
"results": [
|
||||
{
|
||||
"error": "Correction 99992 not found"
|
||||
}
|
||||
],
|
||||
"final_row": null
|
||||
}
|
||||
],
|
||||
"list": [
|
||||
{
|
||||
"name": "all-keys",
|
||||
"result_keys": [
|
||||
"corrections",
|
||||
"habits"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "type-all",
|
||||
"keys": [
|
||||
"corrections",
|
||||
"habits"
|
||||
],
|
||||
"n_corr": 10,
|
||||
"n_hab": 11
|
||||
},
|
||||
{
|
||||
"name": "type-correction",
|
||||
"keys": [
|
||||
"corrections"
|
||||
],
|
||||
"n_corr": 10,
|
||||
"n_hab": 0
|
||||
},
|
||||
{
|
||||
"name": "type-habit",
|
||||
"keys": [
|
||||
"habits"
|
||||
],
|
||||
"n_corr": 0,
|
||||
"n_hab": 11
|
||||
},
|
||||
{
|
||||
"name": "limit-1",
|
||||
"n_corr": 1,
|
||||
"n_hab": 1
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"name": "counts",
|
||||
"expected": {
|
||||
"corrections_active": 10,
|
||||
"corrections_archived": 2,
|
||||
"habits_active": 2,
|
||||
"habits_candidate": 9,
|
||||
"total_activations": 12
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugin": {
|
||||
"name": "behavior",
|
||||
"version": "0.1.0",
|
||||
"description": "Behavioral patterns \u2014 scoped corrections and habit formation.",
|
||||
"capabilities": [
|
||||
"corrections",
|
||||
"habits",
|
||||
"behavioral_patterns"
|
||||
],
|
||||
"hooks": {},
|
||||
"dependencies": [],
|
||||
"provides": [
|
||||
"behavior"
|
||||
],
|
||||
"priority": 50,
|
||||
"auto_activate": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
//! bh_battery — frozen-oracle battery for the behavior core.
|
||||
//! The truth JSON is the LIVE oracle's output (generated by running
|
||||
//! scripts/gen_behavior_truth.py against src/rune/behavior.py, the
|
||||
//! strangler shim serving ep_loader decision math) and is replayed
|
||||
//! VERBATIM: add/insert shapes, the match_context branch matrix, the
|
||||
//! reinforce matrix (habit/correction x outcome x promotion/archival),
|
||||
//! list filters, and stats counts.
|
||||
|
||||
use super::{correction_matches, correction_reinforce, correction_visible, habit_matches,
|
||||
habit_reinforce, habit_visible, parse_scope_tags, plugin, HabitRow};
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("bh_truth.json");
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(TRUTH).expect("bh_truth.json parses")
|
||||
}
|
||||
|
||||
fn habit_row(row: &Value) -> HabitRow {
|
||||
serde_json::from_value(row.clone()).expect("habit row deserializes")
|
||||
}
|
||||
|
||||
fn corr_row(row: &Value) -> (String, String, f64, String, i64) {
|
||||
(
|
||||
row["scope_pattern"].as_str().unwrap_or("").into(),
|
||||
row["scope_domain"].as_str().unwrap_or("").into(),
|
||||
row["confidence"].as_f64().unwrap_or(0.5),
|
||||
row["state"].as_str().unwrap_or("").into(),
|
||||
row["evidence_count"].as_i64().unwrap_or(1),
|
||||
)
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn habit_upd(row: &Value, outcome: &str) -> Value {
|
||||
let r = habit_row(row);
|
||||
serde_json::to_value(habit_reinforce(&r, outcome)).unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn corr_upd(row: &Value, outcome: &str) -> Value {
|
||||
let (_pat, _domain, conf, _state, count) = corr_row(row);
|
||||
let (nc, ncnt, ns) = correction_reinforce(conf, count, outcome);
|
||||
serde_json::json!({"confidence": nc, "evidence_count": ncnt as f64, "state": ns.as_str()})
|
||||
}
|
||||
|
||||
/// add/insert shapes: the returned dict + the full stored row (timestamps
|
||||
/// and ids replayed verbatim — the frozen truth pins them).
|
||||
#[test]
|
||||
fn test_add_shapes() {
|
||||
let t = truth();
|
||||
for c in t["cases"]["add_correction"].as_array().unwrap() {
|
||||
let want = &c["expected"];
|
||||
assert_eq!(want["confidence"].as_f64(), Some(0.5), "{}", c["name"]);
|
||||
assert!(want["added"].as_i64().is_some(), "{}: has id", c["name"]);
|
||||
let row = &c["row"];
|
||||
assert_eq!(row["state"].as_str(), Some("active"), "{}", c["name"]);
|
||||
assert_eq!(row["confidence"].as_f64(), Some(0.5));
|
||||
assert_eq!(row["evidence_count"].as_i64(), Some(1));
|
||||
assert_eq!(
|
||||
row["scope_tags"].as_str(),
|
||||
Some(if c["name"] == "scoped" { r#"{"k": "v"}"# } else { "{}" }),
|
||||
"{}: scope_tags stored as JSON string",
|
||||
c["name"]
|
||||
);
|
||||
}
|
||||
for c in t["cases"]["add_habit"].as_array().unwrap() {
|
||||
let want = &c["expected"];
|
||||
assert_eq!(want["state"].as_str(), Some("candidate"), "{}", c["name"]);
|
||||
let row = &c["row"];
|
||||
assert_eq!(row["state"].as_str(), Some("candidate"));
|
||||
assert_eq!(row["confidence"].as_f64(), Some(0.5));
|
||||
assert_eq!(
|
||||
row["min_activations"].as_i64(),
|
||||
Some(if c["name"] == "full" { 5 } else { 3 })
|
||||
);
|
||||
assert_eq!(
|
||||
row["context_tags"].as_str(),
|
||||
Some(if c["name"] == "full" { r#"["a"]"# } else { "[]" }),
|
||||
"{}: context_tags stored as JSON string",
|
||||
c["name"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The match_context branch matrix: the frozen truth pins the survivor
|
||||
/// action-text sets and the row shape invariants per context.
|
||||
#[test]
|
||||
fn test_match_matrix() {
|
||||
let t = truth();
|
||||
for case in t["cases"]["match"].as_array().unwrap() {
|
||||
// scope_tags_type is only recorded on cases with a correction
|
||||
// survivor (no-correction cases can't probe the row shape).
|
||||
if let Some(st) = case["scope_tags_type"].as_str() {
|
||||
assert_eq!(st, "dict", "{}: scope_tags parsed to dict", case["name"]);
|
||||
assert!(
|
||||
case["n_corr_fields"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|f| f.as_str() == Some("scope_tags")),
|
||||
"{}: scope_tags in row fields",
|
||||
case["name"]
|
||||
);
|
||||
}
|
||||
// Survivor invariants re-derived through the predicates:
|
||||
// every frozen habit survivor text corresponds to a trigger that
|
||||
// matches the context; corrections need BOTH pattern and domain.
|
||||
if case["name"].as_str() == Some("pattern-domain-hit") {
|
||||
assert!(correction_matches("deploy", "shell", case["context"].as_str().unwrap()));
|
||||
assert!(habit_matches("deploy", case["context"].as_str().unwrap()));
|
||||
}
|
||||
if case["name"].as_str() == Some("domain-only-miss") {
|
||||
// "deploy the editor": editor-domain correction (no pattern)
|
||||
// survives, shell-domain one dies on the domain filter.
|
||||
assert!(correction_matches("", "editor", case["context"].as_str().unwrap()));
|
||||
assert!(!correction_matches("", "shell", case["context"].as_str().unwrap()));
|
||||
}
|
||||
if case["name"].as_str() == Some("low-conf-filtered") {
|
||||
assert!(!habit_visible("candidate", 0.2), "confidence gate");
|
||||
}
|
||||
if case["name"].as_str() == Some("bad-regex-skipped") {
|
||||
assert!(!correction_matches("([unclosed", "", case["context"].as_str().unwrap()));
|
||||
assert!(!habit_matches("([broken", case["context"].as_str().unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// reinforce matrix: every frozen habit case replays through the math —
|
||||
/// the results sequence and the final row state/counts.
|
||||
#[test]
|
||||
fn test_reinforce_habit_matrix() {
|
||||
let t = truth();
|
||||
for case in t["cases"]["reinforce_habit"].as_array().unwrap() {
|
||||
let name = case["name"].as_str().unwrap();
|
||||
if case["final_row"].is_null() {
|
||||
// not-found branch: {"error": "Habit N not found"}
|
||||
let r = &case["results"][0];
|
||||
assert!(r["error"].as_str().unwrap().starts_with("Habit "), "{name}");
|
||||
continue;
|
||||
}
|
||||
let row = habit_row(&case["final_row"]);
|
||||
// Replay forward from the reconstructed PRE row (final counts
|
||||
// minus the n recorded steps). Pre state: the oracle's state
|
||||
// never reverts, so the pre state is the first recorded result's
|
||||
// state — except when promotion fired ON step 0 (pre 'candidate'
|
||||
// but result[0] 'active' can only happen if pre count already
|
||||
// met min_activations, in which case 'candidate' still holds as
|
||||
// the add-time state; the promote-at-step-0 case is covered by
|
||||
// already-active-stays whose pre row re-derives to 'candidate').
|
||||
let outcome = case["outcome"].as_str().unwrap();
|
||||
let n = case["n"].as_i64().unwrap();
|
||||
let mut cur = row.clone();
|
||||
cur.activation_count -= n;
|
||||
cur.success_count -= if outcome == "success" { n } else { 0 };
|
||||
cur.failure_count -= if outcome == "failure" { n } else { 0 };
|
||||
cur.state = case["results"][0]["state"].as_str().unwrap().to_string();
|
||||
let mut states = vec![];
|
||||
for _ in 0..n {
|
||||
let upd = habit_reinforce(&cur, outcome);
|
||||
states.push((
|
||||
upd["confidence"].as_f64().unwrap(),
|
||||
upd["state"].as_str().unwrap().to_string(),
|
||||
));
|
||||
cur.activation_count = upd["activation_count"].as_f64().unwrap() as i64;
|
||||
cur.success_count = upd["success_count"].as_f64().unwrap() as i64;
|
||||
cur.failure_count = upd["failure_count"].as_f64().unwrap() as i64;
|
||||
cur.state = upd["state"].as_str().unwrap().to_string();
|
||||
}
|
||||
// Final row must equal the replay's end state (confidences
|
||||
// compared at 1e-12; the oracle REAL is f64 both sides).
|
||||
let final_conf = row.confidence;
|
||||
let replay_conf = cur.confidence;
|
||||
assert!(
|
||||
(final_conf - replay_conf).abs() < 1e-12,
|
||||
"{name}: confidence divergence {final_conf} vs {replay_conf}"
|
||||
);
|
||||
assert_eq!(cur.state, row.state, "{name}: final state");
|
||||
for (i, (conf, state)) in states.iter().enumerate() {
|
||||
let want = &case["results"][i];
|
||||
assert_eq!(
|
||||
want["state"].as_str(),
|
||||
Some(state.as_str()),
|
||||
"{name}: step {i} state"
|
||||
);
|
||||
if let Some(wc) = want["confidence"].as_f64() {
|
||||
assert!(
|
||||
(wc - conf).abs() < 1e-12,
|
||||
"{name}: step {i} confidence {wc} vs {conf}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// reinforce matrix: corrections — confidence ± steps, evidence counting,
|
||||
/// active/archived threshold, floor/ceiling.
|
||||
#[test]
|
||||
fn test_reinforce_correction_matrix() {
|
||||
let t = truth();
|
||||
for case in t["cases"]["reinforce_correction"].as_array().unwrap() {
|
||||
let name = case["name"].as_str().unwrap();
|
||||
if case["final_row"].is_null() {
|
||||
let r = &case["results"][0];
|
||||
assert!(
|
||||
r["error"].as_str().unwrap().starts_with("Correction "),
|
||||
"{name}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let row = &case["final_row"];
|
||||
let outcome = case["outcome"].as_str().unwrap();
|
||||
let n = case["n"].as_i64().unwrap();
|
||||
// Seed the pre-state: recorded `pre` overrides when the generator
|
||||
// pinned one (ceiling case); else back out one outcome step from
|
||||
// the FIRST recorded result (the floor/ceiling clamps make
|
||||
// reversal from the final row lossy).
|
||||
let mut conf = match case["pre"].get("confidence").and_then(|v| v.as_f64()) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
let first = case["results"][0]["confidence"].as_f64().unwrap();
|
||||
match outcome {
|
||||
"success" => first - 0.1,
|
||||
"failure" => first + 0.3,
|
||||
_ => first,
|
||||
}
|
||||
}
|
||||
};
|
||||
let pre_count = row["evidence_count"].as_i64().unwrap()
|
||||
- i64::from(outcome == "success") * n;
|
||||
// Replay and compare every step + final row.
|
||||
let mut count = pre_count;
|
||||
for i in 0..n {
|
||||
let (nc, ncnt, ns) = correction_reinforce(conf, count, outcome);
|
||||
let want = &case["results"][i as usize];
|
||||
assert_eq!(want["state"].as_str(), Some(ns.as_str()), "{name}: step {i} state");
|
||||
let wconf = want["confidence"].as_f64().unwrap();
|
||||
assert!(
|
||||
(wconf - nc).abs() < 1e-12,
|
||||
"{name}: step {i} confidence {wconf} vs {nc}"
|
||||
);
|
||||
conf = nc;
|
||||
count = ncnt;
|
||||
}
|
||||
let fr_conf = row["confidence"].as_f64().unwrap();
|
||||
assert!((fr_conf - conf).abs() < 1e-12, "{name}: final confidence");
|
||||
assert_eq!(
|
||||
row["state"].as_str().unwrap(),
|
||||
if fr_conf > 0.3 { "active" } else { "archived" },
|
||||
"{name}: final state consistency"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The WHERE-clause predicates: frozen list/stats counts re-derived from
|
||||
/// the seeded state; every visible row respects the gates.
|
||||
#[test]
|
||||
fn test_visibility_gates_and_list_counts() {
|
||||
let t = truth();
|
||||
for c in t["cases"]["list"].as_array().unwrap() {
|
||||
match c["name"].as_str().unwrap() {
|
||||
"all-keys" => {
|
||||
let keys: Vec<&str> = c["result_keys"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|k| k.as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(keys, vec!["corrections", "habits"]);
|
||||
}
|
||||
"limit-1" => {
|
||||
assert_eq!(c["n_corr"].as_i64(), Some(1), "LIMIT 1 applies");
|
||||
}
|
||||
"type-correction" => {
|
||||
assert!(c["keys"].as_array().unwrap().iter().all(|k| k.as_str() == Some("corrections")));
|
||||
assert!(c["n_hab"].as_i64().unwrap() == 0 || c.get("n_hab").is_none());
|
||||
}
|
||||
"type-habit" => {
|
||||
assert!(c["keys"].as_array().unwrap().iter().all(|k| k.as_str() == Some("habits")));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Gates (unit-level, oracle-probed formulas):
|
||||
assert!(correction_visible("active", 0.31));
|
||||
assert!(!correction_visible("active", 0.3), "confidence > 0.3 strict");
|
||||
assert!(!correction_visible("archived", 0.9));
|
||||
assert!(habit_visible("candidate", 0.31));
|
||||
assert!(habit_visible("active", 0.31));
|
||||
assert!(!habit_visible("candidate", 0.3), "confidence > 0.3 strict");
|
||||
}
|
||||
|
||||
/// Regex/domain filter semantics (oracle-probed): pattern AND domain must
|
||||
/// both pass; invalid regex skips the row; domain is a case-insensitive
|
||||
/// substring; empty pattern/domain never block.
|
||||
#[test]
|
||||
fn test_match_predicates() {
|
||||
assert!(correction_matches("deploy", "shell", "deploy to shell now"));
|
||||
assert!(!correction_matches("deploy", "editor", "deploy to shell now"),
|
||||
"domain miss filters");
|
||||
assert!(!correction_matches("editor", "shell", "deploy to shell now"),
|
||||
"pattern miss filters");
|
||||
assert!(!correction_matches("([unclosed", "", "anything"),
|
||||
"invalid regex skips");
|
||||
assert!(correction_matches("", "", "anything"), "empty filters never block");
|
||||
assert!(correction_matches("", "SHELL", "deploy to shell now"),
|
||||
"domain case-insensitive");
|
||||
assert!(habit_matches("deploy", "DEPLOY the thing"), "trigger IGNORECASE");
|
||||
assert!(!habit_matches("test (unit|e2e)", "run the unit tests"),
|
||||
"unescaped parens match nothing (frozen)");
|
||||
assert!(!habit_matches("([broken", "anything"), "invalid trigger skips");
|
||||
}
|
||||
|
||||
/// scope_tags parse fallback: valid JSON parses, corrupt input → {}.
|
||||
#[test]
|
||||
fn test_scope_tags_fallback() {
|
||||
assert_eq!(parse_scope_tags(r#"{"a": 1}"#), serde_json::json!({"a": 1}));
|
||||
assert_eq!(parse_scope_tags(r#"["a"]"#), serde_json::json!(["a"]));
|
||||
assert_eq!(parse_scope_tags("not json"), serde_json::json!({}));
|
||||
assert_eq!(parse_scope_tags(""), serde_json::json!({}));
|
||||
}
|
||||
|
||||
/// The PLUGIN descriptor data port is verbatim.
|
||||
#[test]
|
||||
fn test_plugin_frozen() {
|
||||
let t = truth();
|
||||
assert_eq!(plugin(), t["plugin"], "PLUGIN descriptor verbatim");
|
||||
}
|
||||
|
||||
/// Full update-dict replay for one habit case through the FFI-shaped
|
||||
/// path (envelope handling is exercised via the FFI symbols in the
|
||||
/// loader tests; here the math contract is pinned).
|
||||
#[test]
|
||||
fn test_habit_update_dict_shape() {
|
||||
let t = truth();
|
||||
let case = &t["cases"]["reinforce_habit"][0];
|
||||
let row = habit_row(&case["final_row"]);
|
||||
let mut cur = row.clone();
|
||||
cur.activation_count = 0;
|
||||
cur.success_count = 0;
|
||||
cur.failure_count = 0;
|
||||
cur.state = "candidate".into();
|
||||
let upd = habit_reinforce(&cur, "success");
|
||||
assert_eq!(upd["activation_count"].as_f64(), Some(1.0));
|
||||
assert_eq!(upd["success_count"].as_f64(), Some(1.0));
|
||||
assert_eq!(upd["failure_count"].as_f64(), Some(0.0));
|
||||
assert_eq!(upd["state"].as_str(), Some("candidate"), "min 3 gate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_correction_update_dict_shape() {
|
||||
// failure-archives: confidence 0.5 → 0.2, evidence_count UNCHANGED.
|
||||
let upd = corr_upd(&serde_json::json!({"confidence": 0.5, "evidence_count": 1}), "failure");
|
||||
assert_eq!(upd["state"].as_str(), Some("archived"));
|
||||
assert_eq!(upd["confidence"].as_f64(), Some(0.2));
|
||||
assert_eq!(upd["evidence_count"].as_f64(), Some(1.0), "failure keeps count");
|
||||
}
|
||||
@@ -1,12 +1,409 @@
|
||||
//! behavior — behaviour definition system (W6).
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! behavior — behavioral patterns: scoped corrections and habit formation.
|
||||
//!
|
||||
//! Dichotomy-wave port (#2479 tranche) of `src/rune/behavior.py`'s FULL
|
||||
//! decision surface. The oracle is a strangler shim: SQLite I/O + regex
|
||||
//! matching live in Python while the reinforce counting/confidence math
|
||||
//! runs in-process through the `evidence_provenance` cdylib. This crate
|
||||
//! reunites the WHOLE surface in Rust:
|
||||
//!
|
||||
//! * `habit_reinforce` / `correction_reinforce` — the decision math
|
||||
//! (name-preserving port of evidence_provenance's habitmath, which is
|
||||
//! itself the port of this module's formulas),
|
||||
//! * `match_row` — the per-row match_context predicates (scope-pattern
|
||||
//! regex, domain substring, confidence gate, invalid-regex skip),
|
||||
//! * `correction_visible` / `habit_visible` — the SQL WHERE clauses as
|
||||
//! pure predicates (state + confidence gates),
|
||||
//! * `PLUGIN` — the module plugin descriptor (frozen data),
|
||||
//! * `db_path` — the memory-dir path resolution (RUNE_MEMORY_DIR override
|
||||
//! → else rune_home()/memory, mirroring memory_env).
|
||||
//!
|
||||
//! IO stays Python: SQLite open/exec (system libsqlite3 has no write
|
||||
//! path in the memory_storage precedent — this crate links NO sqlite and
|
||||
//! owns zero SQL text), wall-clock `_now`, argparse CLI, DB_PATH constant.
|
||||
//! The facade passes ROWS (fetched Python-side) and receives decisions.
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
//! * bh_habit_reinforce({row, outcome}) -> update dict
|
||||
//! * bh_correction_reinforce({row, outcome}) -> update dict
|
||||
//! * bh_match_row({kind, row, context}) -> {"visible", "matched"}
|
||||
//! * bh_plugin() -> the PLUGIN descriptor
|
||||
//!
|
||||
//! NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
//! the whole process — duplicate symbols break the cdylib link).
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Behavior {
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// ── row shapes ──────────────────────────────────────────────────────────
|
||||
|
||||
/// A corrections row as fetched (row_factory=Row → dict(c)). scope_tags
|
||||
/// arrives as a JSON STRING (json.dumps at insert); the oracle parses it
|
||||
/// back to a dict for the match output ({} on corrupt input).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CorrectionRow {
|
||||
pub id: i64,
|
||||
#[serde(default)]
|
||||
pub evidence_refs: Vec<String>,
|
||||
pub original_behavior: Option<String>,
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
pub corrected_behavior: String,
|
||||
#[serde(default)]
|
||||
pub scope_tags: String,
|
||||
#[serde(default)]
|
||||
pub scope_pattern: String,
|
||||
#[serde(default)]
|
||||
pub scope_domain: String,
|
||||
#[serde(default = "default_evidence")]
|
||||
pub evidence_count: i64,
|
||||
#[serde(default = "default_confidence")]
|
||||
pub confidence: f64,
|
||||
#[serde(default)]
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
fn default_evidence() -> i64 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_confidence() -> f64 {
|
||||
0.5
|
||||
}
|
||||
|
||||
/// A habits row as fetched.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HabitRow {
|
||||
pub id: i64,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub trigger_pattern: String,
|
||||
#[serde(default)]
|
||||
pub context_tags: String,
|
||||
#[serde(default)]
|
||||
pub action_template: String,
|
||||
#[serde(default)]
|
||||
pub activation_count: i64,
|
||||
#[serde(default)]
|
||||
pub success_count: i64,
|
||||
#[serde(default)]
|
||||
pub failure_count: i64,
|
||||
#[serde(default = "default_min_act")]
|
||||
pub min_activations: i64,
|
||||
#[serde(default = "default_confidence")]
|
||||
pub confidence: f64,
|
||||
#[serde(default)]
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
fn default_min_act() -> i64 {
|
||||
3
|
||||
}
|
||||
|
||||
// ── reinforce decision math ─────────────────────────────────────────────
|
||||
// (Name-preserving port of evidence_provenance habitmath — the formulas
|
||||
// this module's shim delegates to. Python semantics preserved exactly:
|
||||
// confidence is REAL (f64); success_rate = success/count at the NEW
|
||||
// counts; failure keeps evidence_count unchanged; neutral leaves counts
|
||||
// (habits still +1 activation); candidate→active iff new_count >= min.)
|
||||
|
||||
/// Reinforcement outcome lattice (Phase B: enum over string soup; the
|
||||
/// FFI boundary parses once, the math matches exhaustively).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Outcome {
|
||||
Success,
|
||||
Failure,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
fn parse(outcome: &str) -> Self {
|
||||
match outcome {
|
||||
"success" => Outcome::Success,
|
||||
"failure" => Outcome::Failure,
|
||||
_ => Outcome::Neutral,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Correction lifecycle state lattice (Phase B: enum, serialized at the
|
||||
/// JSON boundary via exhaustive as_str).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CorrectionState {
|
||||
Active,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl CorrectionState {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
CorrectionState::Active => "active",
|
||||
CorrectionState::Archived => "archived",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Habit promotion state lattice (Phase B: enum; the candidate→active
|
||||
/// edge is the only legal transition, and only at promote time).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HabitState {
|
||||
Candidate,
|
||||
Active,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl HabitState {
|
||||
fn parse(state: &str) -> Self {
|
||||
match state {
|
||||
"candidate" => HabitState::Candidate,
|
||||
"active" => HabitState::Active,
|
||||
_ => HabitState::Other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
HabitState::Candidate => "candidate",
|
||||
HabitState::Active => "active",
|
||||
HabitState::Other => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// habit_reinforce decision math → {activation_count, success_count,
|
||||
/// failure_count, confidence, state}.
|
||||
pub fn habit_reinforce(row: &HabitRow, outcome: &str) -> BTreeMap<String, serde_json::Value> {
|
||||
let outcome = Outcome::parse(outcome);
|
||||
let new_count = row.activation_count + 1;
|
||||
let new_success = row.success_count + i64::from(outcome == Outcome::Success);
|
||||
let new_failure = row.failure_count + i64::from(outcome == Outcome::Failure);
|
||||
// The oracle guards `if new_count > 0` — always true (activation+1).
|
||||
let success_rate = new_success as f64 / new_count as f64;
|
||||
let new_confidence = (0.5 + success_rate * 0.5).min(1.0);
|
||||
let new_state = match HabitState::parse(&row.state) {
|
||||
// candidate→active iff new_count >= min_activations; every other
|
||||
// state passes through unchanged (the oracle never demotes).
|
||||
HabitState::Candidate if new_count >= row.min_activations => "active".to_string(),
|
||||
HabitState::Candidate => "candidate".to_string(),
|
||||
HabitState::Active => "active".to_string(),
|
||||
// Unknown states carry their original string through.
|
||||
HabitState::Other => row.state.clone(),
|
||||
};
|
||||
let mut out = BTreeMap::new();
|
||||
out.insert("activation_count".into(), (new_count as f64).into());
|
||||
out.insert("success_count".into(), (new_success as f64).into());
|
||||
out.insert("failure_count".into(), (new_failure as f64).into());
|
||||
out.insert("confidence".into(), new_confidence.into());
|
||||
out.insert("state".into(), new_state.into());
|
||||
out
|
||||
}
|
||||
|
||||
/// correction_reinforce decision math → {confidence, evidence_count, state}.
|
||||
pub fn correction_reinforce(
|
||||
confidence: f64,
|
||||
evidence_count: i64,
|
||||
outcome: &str,
|
||||
) -> (f64, i64, CorrectionState) {
|
||||
let new_confidence = match Outcome::parse(outcome) {
|
||||
Outcome::Success => (confidence + 0.1).min(1.0),
|
||||
Outcome::Failure => (confidence - 0.3).max(0.0),
|
||||
Outcome::Neutral => confidence,
|
||||
};
|
||||
let new_count = match Outcome::parse(outcome) {
|
||||
// Only success grows the evidence count; failure/neutral keep it.
|
||||
Outcome::Success => evidence_count + 1,
|
||||
Outcome::Failure | Outcome::Neutral => evidence_count,
|
||||
};
|
||||
let new_state = if new_confidence > 0.3 {
|
||||
CorrectionState::Active
|
||||
} else {
|
||||
CorrectionState::Archived
|
||||
};
|
||||
(new_confidence, new_count, new_state)
|
||||
}
|
||||
|
||||
// ── match predicates ────────────────────────────────────────────────────
|
||||
// (The per-row half of match_context: SQL WHERE → *_visible, then the
|
||||
// regex/domain filters. Row ORDER (ORDER BY confidence DESC /
|
||||
// confidence*activation_count DESC) stays Python-side — SQL owns order.)
|
||||
|
||||
/// The corrections WHERE clause as a predicate: state='active' AND
|
||||
/// confidence > 0.3.
|
||||
pub fn correction_visible(state: &str, confidence: f64) -> bool {
|
||||
state == "active" && confidence > 0.3
|
||||
}
|
||||
|
||||
/// The habits WHERE clause as a predicate: state IN ('active',
|
||||
/// 'candidate') AND confidence > 0.3.
|
||||
pub fn habit_visible(state: &str, confidence: f64) -> bool {
|
||||
(state == "active" || state == "candidate") && confidence > 0.3
|
||||
}
|
||||
|
||||
/// A correction row's scope filters against the context: the scope
|
||||
/// pattern must `re.search` (IGNORECASE; invalid regex → row skipped)
|
||||
/// and the domain must occur as a case-insensitive substring.
|
||||
pub fn correction_matches(pattern: &str, domain: &str, context: &str) -> bool {
|
||||
if !pattern.is_empty() {
|
||||
match Regex::new(&format!("(?i){pattern}")) {
|
||||
Ok(re) => {
|
||||
if !re.is_match(context) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
if !domain.is_empty() && !context.to_lowercase().contains(&domain.to_lowercase()) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A habit row's trigger filter: `re.search(trigger, context,
|
||||
/// IGNORECASE)`; invalid regex → row skipped.
|
||||
pub fn habit_matches(trigger: &str, context: &str) -> bool {
|
||||
match Regex::new(&format!("(?i){trigger}")) {
|
||||
Ok(re) => re.is_match(context),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse scope_tags from its stored JSON-string form ({} on corrupt
|
||||
/// input — mirrors the oracle's `json.loads(...) or {}` fallback).
|
||||
pub fn parse_scope_tags(raw: &str) -> serde_json::Value {
|
||||
serde_json::from_str(raw).unwrap_or(serde_json::json!({}))
|
||||
}
|
||||
|
||||
// ── path + plugin data ──────────────────────────────────────────────────
|
||||
|
||||
/// behavior.db path resolution (mirrors memory_env: RUNE_MEMORY_DIR
|
||||
/// override → else rune_home()/memory; rune_home defaults to
|
||||
/// /mnt/workdrive/rune).
|
||||
pub fn db_path() -> String {
|
||||
let mem = std::env::var("RUNE_MEMORY_DIR").unwrap_or_else(|_| {
|
||||
let home = std::env::var("RUNE_HOME").unwrap_or_else(|_| "/mnt/workdrive/rune".into());
|
||||
format!("{home}/memory")
|
||||
});
|
||||
format!("{mem}/behavior.db")
|
||||
}
|
||||
|
||||
/// The module PLUGIN descriptor (frozen data port).
|
||||
pub fn plugin() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"name": "behavior",
|
||||
"version": "0.1.0",
|
||||
"description": "Behavioral patterns — scoped corrections and habit formation.",
|
||||
"capabilities": ["corrections", "habits", "behavioral_patterns"],
|
||||
"hooks": {},
|
||||
"dependencies": [],
|
||||
"provides": ["behavior"],
|
||||
"priority": 50,
|
||||
"auto_activate": true,
|
||||
})
|
||||
}
|
||||
|
||||
// ── FFI ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const std::os::raw::c_char) -> Result<serde_json::Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<serde_json::Value, String>) -> *mut std::os::raw::c_char {
|
||||
let v = match r {
|
||||
Ok(x) => serde_json::json!({"ok": true, "value": x}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn num(row: &serde_json::Value, key: &str, default: f64) -> f64 {
|
||||
match row.get(key) {
|
||||
Some(serde_json::Value::Number(n)) => n.as_f64().unwrap_or(default),
|
||||
Some(serde_json::Value::Bool(b)) => f64::from(u8::from(*b)),
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
|
||||
fn str_field(row: &serde_json::Value, key: &str) -> String {
|
||||
match row.get(key) {
|
||||
Some(serde_json::Value::String(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// bh_habit_reinforce({row, outcome}) -> the update dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bh_habit_reinforce(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let row_v = a.get("row").cloned().unwrap_or(serde_json::json!({}));
|
||||
let outcome = str_field(&a, "outcome");
|
||||
let row: HabitRow = serde_json::from_value(row_v).map_err(|e| e.to_string())?;
|
||||
let upd = habit_reinforce(&row, &outcome);
|
||||
serde_json::to_value(upd).map_err(|e| e.to_string())
|
||||
}))
|
||||
}
|
||||
|
||||
/// bh_correction_reinforce({row, outcome}) -> the update dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bh_correction_reinforce(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let row_v = a.get("row").cloned().unwrap_or(serde_json::json!({}));
|
||||
let outcome = str_field(&a, "outcome");
|
||||
let (new_confidence, new_count, new_state) = correction_reinforce(
|
||||
num(&row_v, "confidence", 0.5),
|
||||
num(&row_v, "evidence_count", 1.0) as i64,
|
||||
&outcome,
|
||||
);
|
||||
serde_json::json!({
|
||||
"confidence": new_confidence,
|
||||
"evidence_count": new_count as f64,
|
||||
"state": new_state.as_str(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// bh_match_row({kind, row, context}) -> {"visible", "matched"}.
|
||||
/// kind: "correction" | "habit". Visibility = the SQL WHERE predicate;
|
||||
/// matched = the regex/domain filters (only meaningful when visible).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bh_match_row(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let kind = str_field(&a, "kind");
|
||||
let row_v = a.get("row").cloned().unwrap_or(serde_json::json!({}));
|
||||
let context = str_field(&a, "context");
|
||||
let (visible, matched) = if kind == "habit" {
|
||||
let v = habit_visible(&str_field(&row_v, "state"), num(&row_v, "confidence", 0.5));
|
||||
let m = v && habit_matches(&str_field(&row_v, "trigger_pattern"), &context);
|
||||
(v, m)
|
||||
} else {
|
||||
let v = correction_visible(&str_field(&row_v, "state"), num(&row_v, "confidence", 0.5));
|
||||
let m = v
|
||||
&& correction_matches(
|
||||
&str_field(&row_v, "scope_pattern"),
|
||||
&str_field(&row_v, "scope_domain"),
|
||||
&context,
|
||||
);
|
||||
(v, m)
|
||||
};
|
||||
serde_json::json!({"visible": visible, "matched": matched})
|
||||
}))
|
||||
}
|
||||
|
||||
/// bh_plugin() -> the PLUGIN descriptor.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bh_plugin() -> *mut std::os::raw::c_char {
|
||||
envelope(Ok(plugin()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "branch_battery.rs"]
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1,215 @@
|
||||
//! build_basis branch battery — parity vs the frozen Python-oracle truth
|
||||
//! (frozen_bb_truth.json, 68 cases across every public function and
|
||||
//! branch, frozen 2026-10-11). Embedded VERBATIM via include_str!.
|
||||
//!
|
||||
//! Timestamp fields the kernel generates (decided_at, created_at,
|
||||
//! revised_at, plan_generated_at) are masked on both sides — wall clock,
|
||||
//! not logic. created_at values inside basis INPUTS are fixed
|
||||
//! pass-through and compare exactly.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::*;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_bb_truth.json");
|
||||
|
||||
/// Remove kernel-generated wall-clock fields wherever they appear.
|
||||
fn mask(v: Value) -> Value {
|
||||
match v {
|
||||
Value::Object(map) => Value::Object(
|
||||
map.into_iter()
|
||||
.filter(|(k, _)| {
|
||||
!matches!(
|
||||
k.as_str(),
|
||||
"decided_at" | "created_at" | "revised_at" | "plan_generated_at"
|
||||
)
|
||||
})
|
||||
.map(|(k, v)| (k, mask(v)))
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(items) => Value::Array(items.into_iter().map(mask).collect()),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn cases(fn_name: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][fn_name]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn input(case: &Value) -> Value {
|
||||
case["input"].clone()
|
||||
}
|
||||
|
||||
fn expected(case: &Value) -> &Value {
|
||||
case.get("expected").expect("case must carry expected")
|
||||
}
|
||||
|
||||
fn check(name: &str, actual: Value, case: &Value) {
|
||||
let masked = mask(actual);
|
||||
assert!(
|
||||
&masked == expected(case),
|
||||
"case {name} diverged:\n actual: {masked}\n expected: {}",
|
||||
expected(case)
|
||||
);
|
||||
}
|
||||
|
||||
fn check_err(name: &str, actual: Result<Value, String>, case: &Value) {
|
||||
let err = case.get("error").expect("case must carry error");
|
||||
match actual {
|
||||
Ok(_) => panic!("case {name} should have raised: {err}"),
|
||||
Err(e) => assert_eq!(&e, err, "case {name} error message diverged"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn research_depth_parity_battery() {
|
||||
for case in cases("research_depth") {
|
||||
let a = input(&case);
|
||||
let out = research_depth(
|
||||
a["route"].as_str().unwrap(),
|
||||
a.get("mode").and_then(Value::as_str).unwrap_or(""),
|
||||
a.get("justification").and_then(Value::as_str).unwrap_or(""),
|
||||
);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_basis_parity_battery() {
|
||||
for case in cases("new_basis") {
|
||||
let a = input(&case);
|
||||
let out = new_basis(
|
||||
a["question"].as_str().unwrap(),
|
||||
&a["depth_decision"],
|
||||
a.get("issue_ref").and_then(Value::as_str).unwrap_or(""),
|
||||
);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_dispositions_parity_battery() {
|
||||
for case in cases("set_dispositions") {
|
||||
let a = input(&case);
|
||||
let mut basis = a["basis"].clone();
|
||||
let errors = set_dispositions(&mut basis, &a["dispositions"]);
|
||||
check(
|
||||
case["name"].as_str().unwrap(),
|
||||
serde_json::json!({"basis": basis, "errors": errors}),
|
||||
&case,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_basis_parity_battery() {
|
||||
for case in cases("validate_basis") {
|
||||
let a = input(&case);
|
||||
let errors = validate_basis(&a["basis"]);
|
||||
check(
|
||||
case["name"].as_str().unwrap(),
|
||||
serde_json::to_value(errors).unwrap(),
|
||||
&case,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_qualified_parity_battery() {
|
||||
for case in cases("is_qualified") {
|
||||
let a = input(&case);
|
||||
let out = is_qualified(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), Value::Bool(out), &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_evidence_ref_parity_battery() {
|
||||
for case in cases("attach_evidence_ref") {
|
||||
let a = input(&case);
|
||||
let out = attach_evidence_ref(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), Value::String(out), &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_parity_battery() {
|
||||
for case in cases("summary") {
|
||||
let a = input(&case);
|
||||
let out = summary(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revise_depth_parity_battery() {
|
||||
for case in cases("revise_depth") {
|
||||
let a = input(&case);
|
||||
let mut basis = a["basis"].clone();
|
||||
let r = revise_depth(&mut basis, a["new_depth"].as_str().unwrap(), a["reason"].as_str().unwrap());
|
||||
match r {
|
||||
Ok(revision) => check(
|
||||
case["name"].as_str().unwrap(),
|
||||
serde_json::json!({"basis": basis, "revision": revision}),
|
||||
&case,
|
||||
),
|
||||
Err(e) => check_err(case["name"].as_str().unwrap(), Err(e), &case),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_json_parity_battery() {
|
||||
for case in cases("to_json") {
|
||||
let a = input(&case);
|
||||
let out = to_json(&a["basis"]);
|
||||
check(case["name"].as_str().unwrap(), Value::String(out), &case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_json_parity_battery() {
|
||||
for case in cases("from_json") {
|
||||
let a = input(&case);
|
||||
match from_json(a["text"].as_str().unwrap()) {
|
||||
Ok(v) => check(case["name"].as_str().unwrap(), v, &case),
|
||||
Err(_) => assert!(
|
||||
case.get("error_kind").is_some(),
|
||||
"case {} unexpected parse failure",
|
||||
case["name"].as_str().unwrap()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_research_plan_parity_battery() {
|
||||
for case in cases("generate_research_plan") {
|
||||
let a = input(&case);
|
||||
let strs = |k: &str| -> Vec<String> {
|
||||
a.get(k)
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let out = generate_research_plan(
|
||||
a["intent"].as_str().unwrap(),
|
||||
a.get("target").and_then(Value::as_str).unwrap_or(""),
|
||||
a.get("current_reality").and_then(Value::as_str).unwrap_or(""),
|
||||
&strs("unknowns"),
|
||||
&strs("decisions"),
|
||||
&strs("risks"),
|
||||
a.get("depth").and_then(Value::as_str),
|
||||
);
|
||||
check(case["name"].as_str().unwrap(), out, &case);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+876
-38
@@ -1,50 +1,888 @@
|
||||
//! build_basis — build basis summary and evidence (W6).
|
||||
//! PARITY port of src/rune/build_basis.py.
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! build_basis — T18 §18C: durable versioned RESEARCH BASIS for
|
||||
//! construction (V1 rule F: RESEARCH INFORMS STRUCTURE).
|
||||
//!
|
||||
//! FULL parity port of src/rune/build_basis.py (dichotomy wave, #2479
|
||||
//! tranche, RUST side verdict). A Build Basis is a durable, schema-
|
||||
//! versioned document capturing WHAT research decided and WHY, with
|
||||
//! references — never research dumps. This module NEVER researches
|
||||
//! (rule C): it records the depth decision derived from the canonical
|
||||
//! route authority and structures adopt/adapt/combine/reject/innovate
|
||||
//! decisions (rule E).
|
||||
//!
|
||||
//! FFI (doc-envelope law): every `bb_*` symbol takes one JSON arg and
|
||||
//! returns {"ok": true, "value": …} or {"ok": false, "err": msg}.
|
||||
//! NOTE: no bb_free_string export — sibling crates sharing the Python
|
||||
//! process already export identical free symbols; a duplicate breaks the
|
||||
//! cdylib link. Callers free through capability_scoring's cs_free_string.
|
||||
//!
|
||||
//! to_json reproduces the oracle byte-for-byte:
|
||||
//! json.dumps(basis, indent=1, sort_keys=True) — including ensure_ascii
|
||||
//! \uXXXX escaping (lowercase hex, surrogate pairs for astral chars),
|
||||
//! code-point key sorting, and Python repr() strings inside error
|
||||
//! messages.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BuildBasis {
|
||||
#[serde(default)]
|
||||
pub depth: String,
|
||||
#[serde(default)]
|
||||
pub implications: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub risks: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub evidence: Vec<String>,
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub const SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
pub const OUTWARD_PRIOR_ART_QUESTIONS: &[&str] = &[
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required",
|
||||
];
|
||||
|
||||
// ── decision lattices (enums with exhaustive matches at the JSON
|
||||
// boundary — the wire always carries the oracle's exact strings) ─────
|
||||
|
||||
/// Research-depth lattice (rule C: derived from adaptive_router ROUTES,
|
||||
/// never a second router).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum DepthTier {
|
||||
None,
|
||||
Inward,
|
||||
Outward,
|
||||
Full,
|
||||
}
|
||||
|
||||
pub fn summarize(depth: &str, risks: Vec<String>, implications: serde_json::Value) -> BuildBasis {
|
||||
BuildBasis {
|
||||
depth: depth.to_string(),
|
||||
implications,
|
||||
risks,
|
||||
evidence: vec![],
|
||||
impl DepthTier {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
DepthTier::None => "NONE",
|
||||
DepthTier::Inward => "INWARD",
|
||||
DepthTier::Outward => "OUTWARD",
|
||||
DepthTier::Full => "FULL",
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's DEPTH_TIERS tuple, rendered as Python repr.
|
||||
pub const PY_TIERS_REPR: &'static str =
|
||||
"('NONE', 'INWARD', 'OUTWARD', 'FULL')";
|
||||
|
||||
pub fn parse(s: &str) -> Option<DepthTier> {
|
||||
match s {
|
||||
"NONE" => Some(DepthTier::None),
|
||||
"INWARD" => Some(DepthTier::Inward),
|
||||
"OUTWARD" => Some(DepthTier::Outward),
|
||||
"FULL" => Some(DepthTier::Full),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// Disposition lattice (rule E): adopt/adapt/combine/reject/innovate.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Disposition {
|
||||
Adopt,
|
||||
Adapt,
|
||||
Combine,
|
||||
Reject,
|
||||
Innovate,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summarize() {
|
||||
let b = summarize(
|
||||
"STANDARD",
|
||||
vec!["risk1".into()],
|
||||
serde_json::json!({"migration": []}),
|
||||
impl Disposition {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Disposition::Adopt => "ADOPT",
|
||||
Disposition::Adapt => "ADAPT",
|
||||
Disposition::Combine => "COMBINE",
|
||||
Disposition::Reject => "REJECT",
|
||||
Disposition::Innovate => "INNOVATE",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Disposition> {
|
||||
match s {
|
||||
"ADOPT" => Some(Disposition::Adopt),
|
||||
"ADAPT" => Some(Disposition::Adapt),
|
||||
"COMBINE" => Some(Disposition::Combine),
|
||||
"REJECT" => Some(Disposition::Reject),
|
||||
"INNOVATE" => Some(Disposition::Innovate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── time (matches time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())) ──
|
||||
|
||||
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
let z = z + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z.rem_euclid(146_097);
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
|
||||
(if m <= 2 { y + 1 } else { y }, m, d)
|
||||
}
|
||||
|
||||
pub fn now() -> String {
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
let days = secs.div_euclid(86400);
|
||||
let rem = secs.rem_euclid(86400);
|
||||
let (y, m, d) = civil_from_days(days);
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
rem / 3600,
|
||||
(rem % 3600) / 60,
|
||||
rem % 60
|
||||
)
|
||||
}
|
||||
|
||||
// ── Python semantics helpers ────────────────────────────────────────────
|
||||
|
||||
/// Python str repr: single quotes unless the string contains ' and not ".
|
||||
fn py_repr_str(s: &str) -> String {
|
||||
let quote = if s.contains('\'') && !s.contains('"') {
|
||||
'"'
|
||||
} else {
|
||||
'\''
|
||||
};
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push(quote);
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if c == quote => {
|
||||
out.push('\\');
|
||||
out.push(c);
|
||||
}
|
||||
c if (c as u32) < 0x20 || (c as u32) == 0x7f => {
|
||||
out.push_str(&format!("\\x{:02x}", c as u32));
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push(quote);
|
||||
out
|
||||
}
|
||||
|
||||
/// Python repr() of a JSON value (as f"{x!r}" renders it).
|
||||
fn py_repr(v: Option<&Value>) -> String {
|
||||
match v {
|
||||
None | Some(Value::Null) => "None".into(),
|
||||
Some(Value::Bool(b)) => if *b { "True" } else { "False" }.into(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
Some(Value::String(s)) => py_repr_str(s),
|
||||
Some(other) => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness for JSON values (0, "", [], {}, null, False are falsy).
|
||||
fn is_truthy(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
|
||||
Some(Value::String(s)) => !s.is_empty(),
|
||||
Some(Value::Array(a)) => !a.is_empty(),
|
||||
Some(Value::Object(o)) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python str[:n] — code points, not bytes.
|
||||
fn truncate_chars(s: &str, n: usize) -> String {
|
||||
s.chars().take(n).collect()
|
||||
}
|
||||
|
||||
/// schema_version equality with Python semantics: True == 1.
|
||||
fn schema_version_ok(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_i64() == Some(SCHEMA_VERSION),
|
||||
Some(Value::Bool(b)) => *b, // Python True == 1
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn route_default_depth(route: &str) -> Option<DepthTier> {
|
||||
match route {
|
||||
"LOCAL_FIX" => Some(DepthTier::None),
|
||||
"STANDARD_CHANGE" => Some(DepthTier::Inward),
|
||||
"RESEARCHED_CHANGE" => Some(DepthTier::Full),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── research_depth ──────────────────────────────────────────────────────
|
||||
|
||||
/// The research-depth DECISION for a construction need. Derived from the
|
||||
/// canonical route authority; `justification` is REQUIRED when deviating
|
||||
/// from the route default — recorded, never silent (rule D).
|
||||
pub fn research_depth(route: &str, mode: &str, justification: &str) -> Value {
|
||||
let default = route_default_depth(route);
|
||||
let mut depth = default.unwrap_or(DepthTier::Inward);
|
||||
let mut why = format!("route {route} default");
|
||||
if !justification.trim().is_empty() {
|
||||
// an explicit justification selects a tier by name if given;
|
||||
// otherwise it annotates the default (zero-copy: candidate
|
||||
// borrows the trimmed justification)
|
||||
let j = justification.trim();
|
||||
let candidate = j.split(':').next().unwrap_or("").trim();
|
||||
if let Some(tier) = DepthTier::parse(candidate) {
|
||||
depth = tier;
|
||||
why = j.to_string();
|
||||
} else {
|
||||
why = format!("route {route} default; note: {j}");
|
||||
}
|
||||
} else if default.is_none() {
|
||||
why = format!(
|
||||
"route {} unknown — INWARD floor (never silent NONE)",
|
||||
py_repr_str(route)
|
||||
);
|
||||
assert_eq!(b.depth, "STANDARD");
|
||||
assert_eq!(b.risks.len(), 1);
|
||||
depth = DepthTier::Inward;
|
||||
}
|
||||
json!({"depth": depth.as_str(), "why": why, "route": route, "mode": mode,
|
||||
"decided_at": now()})
|
||||
}
|
||||
|
||||
// ── new_basis ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Create an empty-but-valid Build Basis. References only — full research
|
||||
/// dumps live in the Research Core's artifact dirs.
|
||||
pub fn new_basis(question: &str, depth_decision: &Value, issue_ref: &str) -> Value {
|
||||
json!({
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
"created_at": now(),
|
||||
"question": question,
|
||||
"issue_ref": issue_ref,
|
||||
"depth": depth_decision,
|
||||
"inward": {"findings": [], "refs": []},
|
||||
"outward": {"prior_art_questions_answered": [],
|
||||
"findings": [], "refs": []},
|
||||
"deeper": {"used": false, "findings": [], "refs": []},
|
||||
"dispositions": [],
|
||||
"alternatives_considered": [],
|
||||
"uncertainty": [],
|
||||
"risks": [],
|
||||
"implications": {"architecture": [], "structural": [],
|
||||
"migration": [], "verification": []},
|
||||
})
|
||||
}
|
||||
|
||||
// ── set_dispositions / validate_basis / is_qualified ────────────────────
|
||||
|
||||
/// Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
/// Mutates `basis` and returns validation errors; the basis stays
|
||||
/// unqualified until valid.
|
||||
pub fn set_dispositions(basis: &mut Value, dispositions: &Value) -> Vec<String> {
|
||||
let rows: Vec<Value> = dispositions
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|d| {
|
||||
let get = |k: &str| d.get(k).cloned().unwrap_or(json!(""));
|
||||
json!({"item": get("item"), "disposition": get("disposition"),
|
||||
"why": get("why"), "ref": get("ref")})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
basis["dispositions"] = Value::Array(rows);
|
||||
validate_basis(basis)
|
||||
}
|
||||
|
||||
/// Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
/// decision with why, at least one outward prior-art answer when depth is
|
||||
/// OUTWARD/FULL, and every disposition uses a legal disposition.
|
||||
pub fn validate_basis(basis: &Value) -> Vec<String> {
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
if !schema_version_ok(basis.get("schema_version")) {
|
||||
errors.push(format!(
|
||||
"schema_version {}",
|
||||
py_repr(basis.get("schema_version"))
|
||||
));
|
||||
}
|
||||
// Python: (basis.get("depth") or {}) — falsy depth slots fall back to {}
|
||||
let depth_obj = basis
|
||||
.get("depth")
|
||||
.filter(|d| is_truthy(Some(d)))
|
||||
.and_then(|d| d.as_object());
|
||||
let empty = Map::new();
|
||||
let depth_obj = depth_obj.unwrap_or(&empty);
|
||||
let depth = depth_obj.get("depth");
|
||||
let depth_str = depth.and_then(Value::as_str);
|
||||
let depth_tier = depth_str.and_then(DepthTier::parse);
|
||||
if depth_tier.is_none() {
|
||||
errors.push(format!(
|
||||
"depth {} not in {}",
|
||||
py_repr(depth),
|
||||
DepthTier::PY_TIERS_REPR
|
||||
));
|
||||
}
|
||||
if !is_truthy(depth_obj.get("why")) {
|
||||
errors.push("depth decision lacks WHY (rule D)".into());
|
||||
}
|
||||
if matches!(depth_tier, Some(DepthTier::Outward) | Some(DepthTier::Full)) {
|
||||
let outward = basis
|
||||
.get("outward")
|
||||
.filter(|o| is_truthy(Some(o)))
|
||||
.and_then(Value::as_object);
|
||||
let has_evidence = outward
|
||||
.map(|o| {
|
||||
is_truthy(o.get("prior_art_questions_answered"))
|
||||
|| is_truthy(o.get("findings"))
|
||||
|| is_truthy(o.get("refs"))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !has_evidence {
|
||||
errors.push("OUTWARD/FULL depth requires prior-art evidence".into());
|
||||
}
|
||||
}
|
||||
if depth_tier == Some(DepthTier::Inward) {
|
||||
let inward = basis
|
||||
.get("inward")
|
||||
.filter(|o| is_truthy(Some(o)))
|
||||
.and_then(Value::as_object);
|
||||
let has_evidence = inward
|
||||
.map(|o| is_truthy(o.get("findings")) || is_truthy(o.get("refs")))
|
||||
.unwrap_or(false)
|
||||
|| is_truthy(basis.get("dispositions"));
|
||||
if !has_evidence {
|
||||
errors.push(
|
||||
"INWARD depth requires recorded evidence (findings/refs) — \
|
||||
an empty basis is not qualified"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for d in basis.get("dispositions").and_then(Value::as_array).unwrap_or(&vec![]) {
|
||||
let disp = d.get("disposition");
|
||||
if !disp
|
||||
.and_then(Value::as_str)
|
||||
.and_then(Disposition::parse)
|
||||
.is_some()
|
||||
{
|
||||
let item = d.get("item").and_then(Value::as_str).unwrap_or("");
|
||||
errors.push(format!(
|
||||
"illegal disposition {} for {}",
|
||||
py_repr(disp),
|
||||
py_repr_str(&truncate_chars(item, 40))
|
||||
));
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
pub fn is_qualified(basis: &Value) -> bool {
|
||||
validate_basis(basis).is_empty()
|
||||
}
|
||||
|
||||
// ── attach_evidence_ref / summary ───────────────────────────────────────
|
||||
|
||||
/// The evidence_ref string a contract carries to point at this basis
|
||||
/// (durable pointer, not a copy — stores stay independent).
|
||||
pub fn attach_evidence_ref(basis: &Value) -> String {
|
||||
// f-string uses str(): missing/null created_at renders as "None"
|
||||
let created = match basis.get("created_at") {
|
||||
None | Some(Value::Null) => "None".to_string(),
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(other) => other.to_string(),
|
||||
};
|
||||
let issue_ref = match basis.get("issue_ref") {
|
||||
Some(Value::String(s)) if !s.is_empty() => s.clone(),
|
||||
_ => "adhoc".to_string(),
|
||||
};
|
||||
format!("build_basis:{issue_ref}:{created}")
|
||||
}
|
||||
|
||||
fn len_of(v: Option<&Value>) -> usize {
|
||||
match v {
|
||||
Some(Value::Array(a)) => a.len(),
|
||||
Some(Value::String(s)) => s.chars().count(),
|
||||
Some(Value::Object(o)) => o.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact projection for prompts/previews (no private reasoning —
|
||||
/// structured conclusions only, rule 18G).
|
||||
pub fn summary(basis: &Value) -> Value {
|
||||
let empty_vec: Vec<Value> = Vec::new();
|
||||
json!({
|
||||
"schema_version": basis.get("schema_version").cloned().unwrap_or(Value::Null),
|
||||
"question": basis.get("question").cloned().unwrap_or(Value::Null),
|
||||
"issue_ref": basis.get("issue_ref").cloned().unwrap_or(Value::Null),
|
||||
"depth": basis.get("depth").cloned().unwrap_or(Value::Null),
|
||||
"qualified": is_qualified(basis),
|
||||
"validation": validate_basis(basis),
|
||||
"dispositions": basis
|
||||
.get("dispositions")
|
||||
.and_then(Value::as_array)
|
||||
.unwrap_or(&empty_vec)
|
||||
.iter()
|
||||
.map(|d| {
|
||||
json!({
|
||||
"item": truncate_chars(d.get("item").and_then(Value::as_str).unwrap_or(""), 80),
|
||||
"disposition": d.get("disposition").cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<Value>>(),
|
||||
"uncertainty_count": len_of(basis.get("uncertainty")),
|
||||
"risks_count": len_of(basis.get("risks")),
|
||||
"implications": match basis.get("implications").and_then(Value::as_object) {
|
||||
Some(o) => o
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), json!(len_of(Some(v)))))
|
||||
.collect::<Map<String, Value>>(),
|
||||
None => Map::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── revise_depth ────────────────────────────────────────────────────────
|
||||
|
||||
/// §21.1G: record a justified depth revision. Original + revised depths
|
||||
/// both persist. An OUTWARD→INWARD/NONE downgrade requires explicit
|
||||
/// resolution evidence — never a bare skip.
|
||||
pub fn revise_depth(basis: &mut Value, new_depth: &str, reason: &str) -> Result<Value, String> {
|
||||
let tier = DepthTier::parse(new_depth)
|
||||
.ok_or_else(|| format!("unknown depth {}", py_repr_str(new_depth)))?;
|
||||
let original = basis
|
||||
.get("depth")
|
||||
.and_then(|d| d.get("depth"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
if original == new_depth {
|
||||
return Err("no-op revision: depth unchanged".into());
|
||||
}
|
||||
// §21.1G: consequential origins mix route names and tiers — the
|
||||
// oracle compares the raw stored string, so parity keeps that.
|
||||
let consequential = matches!(original.as_str(), "RESEARCHED_CHANGE" | "FULL" | "OUTWARD");
|
||||
if consequential && matches!(tier, DepthTier::None | DepthTier::Inward) {
|
||||
let low = reason.to_lowercase();
|
||||
let justified = [
|
||||
"resolved",
|
||||
"non-material",
|
||||
"discovery",
|
||||
"unnecessary",
|
||||
"prior-art question",
|
||||
]
|
||||
.iter()
|
||||
.any(|k| low.contains(k));
|
||||
if !justified {
|
||||
return Err(
|
||||
"OUTWARD downgrade requires explicit evidence: the prior-art \
|
||||
question must be resolved as non-material (not merely skipped)"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let revision = json!({
|
||||
"original_depth": original,
|
||||
"revised_depth": new_depth,
|
||||
"reason": truncate_chars(reason, 200),
|
||||
"revised_at": now(),
|
||||
});
|
||||
basis["depth_revision"] = revision.clone();
|
||||
basis["depth"]["depth"] = json!(new_depth);
|
||||
basis["depth"]["why"] = json!(format!("revised from {original}: {reason}"));
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
// ── to_json / from_json (Python json.dumps/loads semantics) ─────────────
|
||||
|
||||
fn escape_py_json_string(s: &str, out: &mut String) {
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{08}' => out.push_str("\\b"),
|
||||
'\u{0c}' => out.push_str("\\f"),
|
||||
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c if (c as u32) > 0x7e => {
|
||||
let cp = c as u32;
|
||||
if cp <= 0xFFFF {
|
||||
out.push_str(&format!("\\u{:04x}", cp));
|
||||
} else {
|
||||
let v = cp - 0x10000;
|
||||
out.push_str(&format!(
|
||||
"\\u{:04x}\\u{:04x}",
|
||||
0xD800 + (v >> 10),
|
||||
0xDC00 + (v & 0x3FF)
|
||||
));
|
||||
}
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
/// json.dumps(value, indent=1, sort_keys=True) — byte-for-byte with the
|
||||
/// oracle (ensure_ascii, code-point key order, 1-space indent steps).
|
||||
pub fn to_json(basis: &Value) -> String {
|
||||
let mut out = String::new();
|
||||
write_py_json(basis, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_py_json(v: &Value, indent: usize, out: &mut String) {
|
||||
match v {
|
||||
Value::Object(map) => {
|
||||
if map.is_empty() {
|
||||
out.push_str("{}");
|
||||
return;
|
||||
}
|
||||
let mut keys: Vec<&String> = map.keys().collect();
|
||||
keys.sort(); // UTF-8 byte order == code-point order
|
||||
out.push_str("{\n");
|
||||
for (i, k) in keys.iter().enumerate() {
|
||||
for _ in 0..indent + 1 {
|
||||
out.push(' ');
|
||||
}
|
||||
escape_py_json_string(k, out);
|
||||
out.push_str(": ");
|
||||
write_py_json(&map[k.as_str()], indent + 1, out);
|
||||
if i + 1 < keys.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
for _ in 0..indent {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
Value::Array(items) => {
|
||||
if items.is_empty() {
|
||||
out.push_str("[]");
|
||||
return;
|
||||
}
|
||||
out.push_str("[\n");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
for _ in 0..indent + 1 {
|
||||
out.push(' ');
|
||||
}
|
||||
write_py_json(item, indent + 1, out);
|
||||
if i + 1 < items.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
for _ in 0..indent {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Value::String(s) => escape_py_json_string(s, out),
|
||||
Value::Null => out.push_str("null"),
|
||||
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
||||
Value::Number(n) => out.push_str(&n.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_json(text: &str) -> Result<Value, String> {
|
||||
serde_json::from_str(text).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ── generate_research_plan ──────────────────────────────────────────────
|
||||
|
||||
const OUTWARD_KEYWORDS: &[&str] = &[
|
||||
"exist",
|
||||
"existing",
|
||||
"prior art",
|
||||
"precedent",
|
||||
"standard",
|
||||
"convention",
|
||||
"library",
|
||||
"tool",
|
||||
"framework",
|
||||
"approach",
|
||||
];
|
||||
|
||||
const CONSEQUENTIAL_KEYWORDS: &[&str] = &[
|
||||
"architecture",
|
||||
"migration",
|
||||
"replace",
|
||||
"rewrite",
|
||||
"security",
|
||||
"schema",
|
||||
"delete",
|
||||
"deploy",
|
||||
"breaking",
|
||||
];
|
||||
|
||||
/// Generate a CONTEXT-SPECIFIC research plan for one construction intent.
|
||||
/// Topics are synthesized from the material unknowns/decisions/risks of
|
||||
/// THIS intent — no topic vocabulary, no permanent category list.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn generate_research_plan(
|
||||
intent: &str,
|
||||
target: &str,
|
||||
current_reality: &str,
|
||||
unknowns: &[String],
|
||||
decisions: &[String],
|
||||
risks: &[String],
|
||||
depth: Option<&str>,
|
||||
) -> Value {
|
||||
let unknowns: Vec<&str> = unknowns.iter().map(String::as_str).filter(|u| !u.trim().is_empty()).collect();
|
||||
let decisions: Vec<&str> = decisions.iter().map(String::as_str).filter(|d| !d.trim().is_empty()).collect();
|
||||
let risks: Vec<&str> = risks.iter().map(String::as_str).filter(|r| !r.trim().is_empty()).collect();
|
||||
let mut topics: Vec<Value> = Vec::new();
|
||||
// each material unknown becomes a research topic aimed at resolving it
|
||||
for u in &unknowns {
|
||||
topics.push(json!({
|
||||
"topic": format!("resolve unknown: {u}"),
|
||||
"kind": "unknown-resolution",
|
||||
"why": format!("material unknown blocks confident construction: {}",
|
||||
truncate_chars(u, 80)),
|
||||
}));
|
||||
}
|
||||
for d in &decisions {
|
||||
topics.push(json!({
|
||||
"topic": format!("inform decision: {d}"),
|
||||
"kind": "decision-support",
|
||||
"why": format!("consequential decision needs precedent/evidence: {}",
|
||||
truncate_chars(d, 80)),
|
||||
}));
|
||||
}
|
||||
for r in &risks {
|
||||
topics.push(json!({
|
||||
"topic": format!("de-risk: {r}"),
|
||||
"kind": "risk-investigation",
|
||||
"why": format!("identified risk needs verification/mitigation evidence: {}",
|
||||
truncate_chars(r, 80)),
|
||||
}));
|
||||
}
|
||||
// precedent question (rule E) attaches ONLY when outward evidence is
|
||||
// plausibly relevant — an unknown/decision about external prior art.
|
||||
let joined: String = unknowns
|
||||
.iter()
|
||||
.chain(decisions.iter())
|
||||
.copied()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ")
|
||||
.to_lowercase();
|
||||
let needs_outward = OUTWARD_KEYWORDS.iter().any(|w| joined.contains(w));
|
||||
if needs_outward {
|
||||
topics.push(json!({
|
||||
"topic": "what already exists that is relevant to THIS intent \
|
||||
(adopt/adapt/combine/reject/innovate)",
|
||||
"kind": "prior-art",
|
||||
"why": "material unknown references existing practice — \
|
||||
precedent discovery required before deeper research",
|
||||
}));
|
||||
}
|
||||
let (depth, why) = match depth {
|
||||
Some(d) => (d.to_string(), "depth supplied by caller (recorded, caller accountable)".to_string()),
|
||||
None => {
|
||||
// consequence heuristics: novel/architectural/unsafe → deeper
|
||||
let hay = format!("{intent} {}", risks.join(" ")).to_lowercase();
|
||||
let consequential = CONSEQUENTIAL_KEYWORDS.iter().any(|w| hay.contains(w));
|
||||
let d = if consequential && !topics.is_empty() {
|
||||
"FULL"
|
||||
} else if needs_outward && !topics.is_empty() {
|
||||
"OUTWARD"
|
||||
} else if !topics.is_empty() {
|
||||
"INWARD"
|
||||
} else {
|
||||
"NONE"
|
||||
};
|
||||
let why = match d {
|
||||
"FULL" => "consequence heuristics: consequential keywords + research topics present",
|
||||
"OUTWARD" => "precedent-relevant unknowns present",
|
||||
"INWARD" => "local understanding suffices",
|
||||
_ => "no material unknowns — local evidence only",
|
||||
};
|
||||
(d.to_string(), why.to_string())
|
||||
}
|
||||
};
|
||||
json!({
|
||||
"intent": intent,
|
||||
"target": target,
|
||||
"current_reality_note": truncate_chars(current_reality, 200),
|
||||
"depth": {"depth": depth, "why": why},
|
||||
"topics": topics,
|
||||
"plan_generated_at": now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn arg_str<'a>(a: &'a Value, k: &str) -> &'a str {
|
||||
a.get(k).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
fn arg_str_list(a: &Value, k: &str) -> Vec<String> {
|
||||
a.get(k)
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// bb_research_depth({"route", "mode"?, "justification"?}) → depth decision dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_research_depth(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
research_depth(
|
||||
arg_str(&a, "route"),
|
||||
arg_str(&a, "mode"),
|
||||
arg_str(&a, "justification"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_new_basis({"question", "depth_decision", "issue_ref"?}) → basis dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_new_basis(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let dd = a
|
||||
.get("depth_decision")
|
||||
.cloned()
|
||||
.ok_or_else(|| "missing depth_decision".to_string())?;
|
||||
Ok(new_basis(arg_str(&a, "question"), &dd, arg_str(&a, "issue_ref")))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_set_dispositions({"basis", "dispositions"}) → {"basis", "errors"}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_set_dispositions(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut basis = a
|
||||
.get("basis")
|
||||
.cloned()
|
||||
.ok_or_else(|| "missing basis".to_string())?;
|
||||
let dispositions = a
|
||||
.get("dispositions")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(vec![]));
|
||||
let errors = set_dispositions(&mut basis, &dispositions);
|
||||
Ok(json!({"basis": basis, "errors": errors}))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_validate_basis({"basis"}) → [errors].
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_validate_basis(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(serde_json::to_value(validate_basis(basis)).unwrap_or_default())
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_is_qualified({"basis"}) → bool.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_is_qualified(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(Value::Bool(is_qualified(basis)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_attach_evidence_ref({"basis"}) → evidence_ref string.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_attach_evidence_ref(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(Value::String(attach_evidence_ref(basis)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_summary({"basis"}) → summary dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_summary(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(summary(basis))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_revise_depth({"basis", "new_depth", "reason"}) → {"basis", "revision"}
|
||||
/// or {"ok": false, "err": <exact Python ValueError message>}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_revise_depth(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let mut basis = a
|
||||
.get("basis")
|
||||
.cloned()
|
||||
.ok_or_else(|| "missing basis".to_string())?;
|
||||
let revision = revise_depth(&mut basis, arg_str(&a, "new_depth"), arg_str(&a, "reason"))?;
|
||||
Ok(json!({"basis": basis, "revision": revision}))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_to_json({"basis"}) → JSON string (json.dumps indent=1 sort_keys).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_to_json(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let basis = a.get("basis").ok_or_else(|| "missing basis".to_string())?;
|
||||
Ok(Value::String(to_json(basis)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// bb_from_json({"text"}) → parsed value.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_from_json(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| from_json(arg_str(&a, "text"))))
|
||||
}
|
||||
|
||||
/// bb_generate_research_plan({"intent", "target"?, "current_reality"?,
|
||||
/// "unknowns"?, "decisions"?, "risks"?, "depth"?}) → plan dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bb_generate_research_plan(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
generate_research_plan(
|
||||
arg_str(&a, "intent"),
|
||||
arg_str(&a, "target"),
|
||||
arg_str(&a, "current_reality"),
|
||||
&arg_str_list(&a, "unknowns"),
|
||||
&arg_str_list(&a, "decisions"),
|
||||
&arg_str_list(&a, "risks"),
|
||||
a.get("depth").and_then(Value::as_str),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn crate_exists_and_compiles() {
|
||||
assert!(true);
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user