Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f2449cd6 | ||
|
|
83b0854593 | ||
|
|
180d2bb8b4 | ||
|
|
40b3ee1db5 | ||
|
|
2016fe0433 | ||
|
|
2a3526db1e | ||
|
|
8b51a6c824 | ||
|
|
bf9750ea5c | ||
|
|
976609b0b9 | ||
|
|
08757105e7 | ||
|
|
3e16ece001 | ||
|
|
f5dfd4e6b9 | ||
|
|
f82fed98fb | ||
|
|
55ad089c3f | ||
|
|
c8426bdddb | ||
|
|
27716e0243 | ||
|
|
caeaab3c1a | ||
|
|
a95c6ce129 | ||
|
|
9efb7d8697 | ||
|
|
031e054268 | ||
|
|
1ce62e2730 | ||
|
|
fffce59894 | ||
|
|
33692e090b | ||
|
|
28dc24b481 | ||
|
|
57290880ad | ||
|
|
40a1bd8f6a | ||
|
|
c31886823a | ||
|
|
464dccf95c | ||
|
|
f570b1b146 | ||
|
|
93c61f98e8 | ||
|
|
eb5a6b0f45 | ||
|
|
4c17cd9254 | ||
|
|
f29f8437a0 | ||
|
|
9780490703 | ||
|
|
b78e0cee5a | ||
|
|
0d0c6f3f9b | ||
|
|
8820ef251e | ||
|
|
f2446c04a5 | ||
|
|
83b1f79559 | ||
|
|
c46574ba79 | ||
|
|
1a7ce74489 | ||
|
|
b582f1f020 | ||
|
|
84eeecb9c9 | ||
|
|
3ec796fe7c | ||
|
|
62ee29e79a | ||
|
|
1fee2c7456 | ||
|
|
9688b9d74e | ||
|
|
60d19e03da | ||
|
|
49c1ffa347 | ||
|
|
b19c891407 | ||
|
|
eadb65a6dc | ||
|
|
edec9ca2e3 | ||
|
|
badaf08950 | ||
|
|
302155fbe8 | ||
|
|
1db3ca7644 | ||
|
|
6c93431c37 | ||
|
|
6e3ce8b9fa | ||
|
|
82bfc2469f | ||
|
|
f9987561d5 | ||
|
|
8ae9ef2d0f | ||
|
|
7dc25da2f5 | ||
|
|
bcde852f0f | ||
|
|
b60808d4e1 | ||
|
|
8b2591fd1b | ||
|
|
d624240081 | ||
|
|
2467dacf6b | ||
|
|
abd8d8fcc0 | ||
|
|
cdb6ca9627 | ||
|
|
f7198ebac9 | ||
|
|
c452a7c4b2 | ||
|
|
9e923ae572 | ||
|
|
86606f686d | ||
|
|
8e8b2921f2 | ||
|
|
fff83f8dff | ||
|
|
51cf1b1c86 | ||
|
|
8766912a35 | ||
|
|
c7400d4a20 |
No files matched your search
@@ -198,7 +198,7 @@ jobs:
|
||||
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
|
||||
# the tmpfs (2026-10-07: venv creation failed disk-full across
|
||||
# every job). Keep the 3 newest; drop the rest.
|
||||
cd /tmp && ls -dt rune-ci-target-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
|
||||
cd /tmp && ls -dt rune-ci-target-* go-bins-* 2>/dev/null | tail -n +3 | xargs -r rm -rf
|
||||
# deploy-test cache: rust targets accumulate ~15G (every crate's
|
||||
# release dir). The staged source survives; targets rebuild.
|
||||
find /tmp/rune-deploy-test/src/rust -maxdepth 2 -name target -type d 2>/dev/null | xargs -r rm -rf
|
||||
@@ -237,6 +237,12 @@ jobs:
|
||||
- name: goal-engine parity gate (HARD — #2479 batches 2+129: verifier + lifecycle)
|
||||
run: |
|
||||
cd src/go/goal-engine && go build -o /tmp/goal-engine-${{ gitea.run_id }} . && /tmp/goal-engine-${{ gitea.run_id }} replay truth.json && go test -run TestLifecycleParity -v
|
||||
- name: goal FULL-port parity gate (HARD — #2479 dichotomy wave: 59-row frozen oracle)
|
||||
run: |
|
||||
cd src/go/goal && go build -o /tmp/goal-${{ gitea.run_id }} . && /tmp/goal-${{ gitea.run_id }} replay truth.json && go test -run TestTruthParity -v
|
||||
- name: abstraction FULL-port parity gate (HARD — #2479 dichotomy wave: 77-row frozen oracle)
|
||||
run: |
|
||||
cd src/go/abstraction && go build -o /tmp/abstraction-${{ gitea.run_id }} . && /tmp/abstraction-${{ gitea.run_id }} replay truth.json && go test -run TestTruthParity -v
|
||||
- name: provider-budget parity gate (HARD — #2479 batches 3+146: budget + ties/headers/rpm)
|
||||
run: |
|
||||
cd src/go/provider-budget && go build -o /tmp/provider-budget-${{ gitea.run_id }} .
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.183.0**
|
||||
**Current release: v0.203.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: 23%** (535/2357 runtime functions matched)
|
||||
**Function-level completion: 28%** (687/2480 runtime functions matched)
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 8 | 734 | 80%+ of functions ported |
|
||||
| PARTIAL | 79 | 39290 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 114 | 31504 | no name-preserving implementation |
|
||||
| Total runtime | 201 | 71528 | |
|
||||
| FULL | 15 | 4474 | 80%+ of functions ported |
|
||||
| PARTIAL | 83 | 41656 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 119 | 26646 | no name-preserving implementation |
|
||||
| Total runtime | 217 | 72776 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -17,19 +17,9 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
|---|---|---|---|
|
||||
| rune_tui | 4170 | 159 | TUI-LAST |
|
||||
| curator | 1838 | 43 | GO |
|
||||
| spine | 1204 | 31 | RUST |
|
||||
| abstraction | 656 | 17 | GO |
|
||||
| contracts | 629 | 7 | RUST |
|
||||
| construction_intent | 555 | 16 | RUST |
|
||||
| canary_audit_ledger | 541 | 2 | TEST-INFRA |
|
||||
| event_wait | 518 | 14 | GO |
|
||||
| tui_validator | 514 | 13 | TUI-LAST |
|
||||
| memory_selector | 511 | 13 | RUST |
|
||||
| system_registry | 504 | 14 | RUST |
|
||||
| construction_pattern | 496 | 12 | RUST |
|
||||
| auto_tasks | 460 | 14 | GO |
|
||||
| drift_ledger | 456 | 16 | RUST |
|
||||
| goal | 453 | 20 | GO |
|
||||
| release_notes | 446 | 16 | GO |
|
||||
| model_routing | 432 | 13 | GO |
|
||||
| worthiness | 411 | 13 | unclassified |
|
||||
@@ -42,7 +32,6 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| programme_continuation | 349 | 11 | RUST |
|
||||
| canary_rune_serve | 329 | 13 | TEST-INFRA |
|
||||
| canary_p1_07 | 324 | 10 | TEST-INFRA |
|
||||
| layout_contract | 316 | 6 | RUST |
|
||||
| message_queue | 316 | 13 | GO |
|
||||
| drift_observability | 315 | 8 | RUST |
|
||||
| build_basis | 314 | 12 | RUST |
|
||||
@@ -68,34 +57,37 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| 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 |
|
||||
| testing_mode | 196 | 12 | RUST |
|
||||
| cc_loader | 195 | 26 | RUST |
|
||||
| grading | 193 | 3 | unclassified |
|
||||
| canary_spine_meta | 192 | 7 | TEST-INFRA |
|
||||
| verification_layer | 190 | 12 | RUST |
|
||||
| message_graph | 187 | 12 | unclassified |
|
||||
| ffi_query_memory_smoke | 183 | 5 | RUST |
|
||||
| behaviour_ledger | 174 | 12 | RUST |
|
||||
| canary_tui_transport | 174 | 7 | TEST-INFRA |
|
||||
| sr_loader | 173 | 18 | unclassified |
|
||||
| canary_tui_drift | 170 | 2 | TEST-INFRA |
|
||||
| inference_trace | 164 | 6 | RUST |
|
||||
| ms_loader | 163 | 16 | RUST |
|
||||
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
|
||||
| mse_loader | 152 | 12 | unclassified |
|
||||
| forge | 146 | 11 | GO |
|
||||
| rune_graphic | 145 | 5 | TUI-LAST |
|
||||
| spine_loader | 144 | 13 | RUST |
|
||||
| project_registry | 139 | 11 | RUST |
|
||||
| canary_rune_entry | 137 | 3 | TEST-INFRA |
|
||||
| verification_layer | 136 | 11 | RUST |
|
||||
| tui_backlog | 125 | 5 | TUI-LAST |
|
||||
| state_checkpoint | 123 | 8 | unclassified |
|
||||
| drift_resume | 118 | 3 | RUST |
|
||||
| canary_conversation_state | 115 | 2 | TEST-INFRA |
|
||||
| contracts_loader | 112 | 8 | unclassified |
|
||||
| canary_query_memory | 111 | 2 | TEST-INFRA |
|
||||
| tui_transport | 108 | 10 | TUI-LAST |
|
||||
| canary_unbounded | 105 | 2 | TEST-INFRA |
|
||||
| spine_loader | 105 | 7 | RUST |
|
||||
| message_widgets | 104 | 6 | TUI-LAST |
|
||||
| self_modify | 102 | 7 | unclassified |
|
||||
| sc_loader | 96 | 6 | RUST |
|
||||
@@ -103,14 +95,27 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| mutation_guards | 94 | 2 | unclassified |
|
||||
| concept_topology | 90 | 7 | unclassified |
|
||||
| cs_loader | 90 | 5 | RUST |
|
||||
| ci_loader | 89 | 4 | unclassified |
|
||||
| tc_loader | 88 | 5 | RUST |
|
||||
| broker_loader | 86 | 5 | unclassified |
|
||||
| canary_schema_migration | 86 | 1 | TEST-INFRA |
|
||||
| ss_loader | 86 | 7 | unclassified |
|
||||
| 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 |
|
||||
| execution_trace | 71 | 4 | unclassified |
|
||||
| gp_loader | 71 | 6 | unclassified |
|
||||
| experience_encoding | 70 | 1 | unclassified |
|
||||
| abstraction_ladder | 68 | 3 | unclassified |
|
||||
| pane_authority | 67 | 2 | unclassified |
|
||||
| ip_loader | 65 | 4 | unclassified |
|
||||
| cb_loader | 64 | 3 | unclassified |
|
||||
| skill_compiler | 64 | 4 | unclassified |
|
||||
| sg_loader | 63 | 4 | unclassified |
|
||||
| sm_loader | 63 | 4 | unclassified |
|
||||
| meaningful_events | 61 | 3 | unclassified |
|
||||
| outcome_utility | 59 | 4 | unclassified |
|
||||
| memory_types | 54 | 2 | unclassified |
|
||||
@@ -123,7 +128,7 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| evidence_capsule | 43 | 3 | unclassified |
|
||||
| builder_bridge_cli | 42 | 1 | GO |
|
||||
| __init__ | 40 | 1 | RUST |
|
||||
| mr_loader | 38 | 13 | RUST |
|
||||
| mr_loader | 40 | 15 | RUST |
|
||||
| canary_memory_synthesis | 33 | 1 | TEST-INFRA |
|
||||
| canary_resource_authority | 33 | 1 | TEST-INFRA |
|
||||
| canary_surface_parity | 33 | 1 | TEST-INFRA |
|
||||
@@ -134,46 +139,48 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
|
||||
| Module | Lines | Coverage | Missing (first named) |
|
||||
|---|---|---|---|
|
||||
| rune_operator | 2746 | 15/68 | _build_model_router, _bounded, _probe_failure_classes, _sorted_tool_names, _memory_query_from_trace, _observation_from_compaction |
|
||||
| memory_store | 2586 | 29/78 | _ctx_budget, now_iso, ensure_memory_type_column, _embed, _temp_json, _entities_to_json |
|
||||
| memory_store | 2333 | 30/78 | _ctx_budget, now_iso, ensure_memory_type_column, _embed, _temp_json, _entities_to_json |
|
||||
| rune_operator | 2746 | 25/68 | _build_model_router, _sorted_tool_names, _filter_safety_noise, _wire_auto_filer, _reconcile_guarded, get_operator |
|
||||
| context_broker | 870 | 6/28 | _py_default_estimator, _cc_core, _py_default_budget, _py_default_memory_budget, _py_memory_disabled_value, reset_discovered_windows |
|
||||
| local_tool_runtime | 809 | 8/30 | _find_capability_executor, _extract_context_length, context_length, reset_context_length_cache, supports_tools, supports_thinking |
|
||||
| construction_interface | 1193 | 6/27 | _to_stage_check, _guard_specs_from_obligations, t_layout, _obligations_projection, _obligation_scope_files, main |
|
||||
| structural_store | 656 | 12/33 | _sorted, new_doc, load, _redact_secrets, save, canonical_digest |
|
||||
| structural_store | 690 | 13/34 | _ss_core, _sorted, new_doc, _redact_secrets, save, canonical_digest |
|
||||
| persona_gate | 724 | 7/26 | _now, _cc_pg_core, _py_lex, _lex_input, _py_overlap, _embed_raw |
|
||||
| sleep_time | 984 | 8/26 | _ctx_budget, _ensure_dirs, db_connect, fetch_sessions, retain_to_store, reflect_local |
|
||||
| provider_registry | 679 | 11/28 | _ctx_budget, _format_tools_openai, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
|
||||
| tool_protocol | 461 | 10/27 | _utc_now_iso, _contains_tool_object_key, to_ollama_tools, dispatch_validated_tool, tool_requires_approval, to_dict |
|
||||
| warm_router | 1540 | 19/36 | _ensure_use_history_table, record_outcome, record_neuron_fire, _py_base_level_from_uses, base_level_activation, _db |
|
||||
| construction_decision | 1014 | 4/20 | normalize_need, _sc_core, _py_language_placement, resolve_surface, ss_entity, implemented_contracts |
|
||||
| tool_executor | 1836 | 31/47 | _execute_tool_now, _prepared_path, serialize_prepared_tool, deserialize_prepared_tool, confirm_write, _resolve_execution_cwd |
|
||||
| warm_router | 1518 | 19/36 | _ensure_use_history_table, record_outcome, record_neuron_fire, _py_base_level_from_uses, base_level_activation, _db |
|
||||
| construction_decision | 985 | 4/20 | normalize_need, _sc_core, _py_language_placement, resolve_surface, ss_entity, implemented_contracts |
|
||||
| memory_consolidation | 770 | 7/23 | _consolidation_now, _consolidation_memory_oracle, _consolidation_neuron_oracle, _load_knowledge, _save_knowledge, _load_lessons |
|
||||
| work_manager | 594 | 9/25 | group_by_file, load_config, save_config, load_batches, save_batches, create_work |
|
||||
| growth_delta | 449 | 5/20 | _ctx_budget, _store_db, _llm, _parse_json, _load_deltas, _save_deltas |
|
||||
| memory_consolidation | 743 | 7/22 | _consolidation_memory_oracle, _consolidation_neuron_oracle, _load_knowledge, _save_knowledge, _load_lessons, _save_lessons |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| scaffold | 868 | 17/31 | init, add_domain, add_component, add_contract, set_prd, set_next_action |
|
||||
| build_mode | 521 | 20/33 | _load_state, _save_state, assess_complexity, start_research, start_feasible, start_consult |
|
||||
| capability_broker | 908 | 7/20 | _native_tool_schemas, _select_for_job, load_registry, _native_tool_cards, _cards_are_stale, load_cards |
|
||||
| failure_auto_filer | 527 | 11/24 | _error_text, unconsumed, mark_consumed, unconsumed, mark_consumed, submit |
|
||||
| cache | 492 | 6/17 | _record_hit, _record_miss, flush_stats, get_cache, put_cache, invalidate_scope |
|
||||
| elastic_cognition | 774 | 3/14 | _ctx_budget, _build_constitution, _get_constitution, _load_cloud_config, execute_at_tier, _execute_deterministic |
|
||||
| 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 | 352 | 9/20 | workspace_record, workspace_advance, _init_project, save, reload, add |
|
||||
| capability_fit | 455 | 3/13 | record_semantic_fit, _provides_contracts, _consumers, discover_candidates, _cs_core, decide |
|
||||
| structural_model | 388 | 9/20 | workspace_record, workspace_advance, _init_project, save, reload, add |
|
||||
| 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 |
|
||||
| drift_core | 517 | 4/14 | _ms_core, _now_iso, _py_check_promotion_eligibility, promote_entry, compute_fama, compute_drift_velocity |
|
||||
| drift_core | 471 | 4/14 | _ms_core, _now_iso, _py_check_promotion_eligibility, promote_entry, compute_fama, compute_drift_velocity |
|
||||
| model_profiles | 438 | 5/15 | get_profile_path, load_profile, save_profile, build_profile_from_ollama, scan_all_models, get_or_create_profile |
|
||||
| tier_manager | 262 | 5/15 | _cc_probe, cc_loader_pin, _py_core_now, _py_get_tier, _py_promote, _py_demote |
|
||||
| witness | 475 | 8/18 | _ctx_budget, _now, _cc_probe, cc_loader_pin, _py_extract_topic, _py_detect_signals |
|
||||
| witness | 446 | 8/18 | _ctx_budget, _now, _cc_probe, cc_loader_pin, _py_extract_topic, _py_detect_signals |
|
||||
| drift_compression | 307 | 3/12 | _py_get_compression_ratio, _py_compress_text, _py_compress_entry, _py_should_warn, _py_warn_before_compress, get_compression_ratio |
|
||||
| drift_ledger | 471 | 7/16 | ledger_path, read_block, load_ledger, save_ledger, surface_alert, snapshot |
|
||||
| scaffold | 868 | 22/31 | init, set_prd, add_maintenance_item, bind_repo, observe_repo, repo_state_from_git |
|
||||
| builder_bridge | 326 | 3/11 | _construction_store, _active_construction_lines, _default_selector, _cached_memory, _get_selector, _retrieve_memory |
|
||||
| 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 |
|
||||
| pins | 284 | 5/12 | _store_path, _save, _detail_blob, unpin, _py_pin_recall, list_pins |
|
||||
| pins | 282 | 5/12 | _store_path, _save, _detail_blob, unpin, _py_pin_recall, list_pins |
|
||||
| voice_hybrid | 427 | 3/10 | _connect, _load_growth, _save_growth, source_profiles, always, grow |
|
||||
| voice_sync | 299 | 3/10 | _now, get_delivery_register_from_db, get_delivery_register, update_rune_md, check_sync, handle_hook |
|
||||
| audit_ledger | 212 | 2/8 | _compute_hash, get_entries, log_promotion, log_gate_result, log_drift_alert, main |
|
||||
@@ -191,38 +198,47 @@ _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 |
|
||||
| construction_mode | 395 | 1/5 | _tokens, _contains_any, to_dict, from_dict |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| subconscious_pass | 187 | 1/5 | main, _extract_topic, save_reflection, get_recent_findings |
|
||||
| construction_activation | 237 | 1/4 | _scaffold_for, _auto_structural_awareness, dispatch_from_decision |
|
||||
| construction_projection | 225 | 1/4 | _contract_entity, validate, write_mini_doc |
|
||||
| construction_mode | 412 | 2/5 | _contains_any, to_dict, from_dict |
|
||||
| construction_projection | 254 | 1/4 | _contract_entity, validate, write_mini_doc |
|
||||
| dependency_tracker | 202 | 2/5 | check_propagation, get_full_map, main |
|
||||
| event_envelope | 303 | 5/8 | test_envelope, to_canonical_dict, to_dict |
|
||||
| response_adapter | 222 | 4/7 | parse_plain_response, test_adapter, to_dict |
|
||||
| salience_gate | 243 | 1/4 | test_salience, handle_hook, to_dict |
|
||||
| graph_projection | 134 | 3/6 | _model_to_dicts, write_all, _now |
|
||||
| memory_selector | 590 | 11/14 | _mse_core, add_source, _source_store |
|
||||
| subsystem_health | 163 | 3/6 | set_auto_filer, record_failure, configure_logging |
|
||||
| warm_neuron_store | 122 | 3/6 | _db, neuron_slugs, handle_hook |
|
||||
| cognitive_budgeter | 208 | 1/3 | test_budgeter, to_dict |
|
||||
| cognitive_budgeter | 221 | 1/3 | test_budgeter, to_dict |
|
||||
| event_bus | 244 | 5/7 | _store_path, handle_hook |
|
||||
| graph_projection | 108 | 3/5 | write_all, _now |
|
||||
| response_adapter | 222 | 5/7 | test_adapter, to_dict |
|
||||
| rune_profiler | 165 | 1/3 | profile_model_call, main |
|
||||
| stage_guards | 281 | 7/9 | to_dict, chk |
|
||||
| standardized_providers | 281 | 3/5 | list_providers, connect_all_local |
|
||||
| structural_migration | 77 | 2/4 | apply_migration, consent_brief |
|
||||
| voice_injector | 138 | 3/5 | test_injection, to_dict |
|
||||
| construction_depth | 138 | 1/2 | to_dict |
|
||||
| construction_depth | 155 | 1/2 | to_dict |
|
||||
| recall_router | 165 | 2/3 | handle_hook |
|
||||
|
||||
## FULL — ported, parity-gated
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| abstraction | 656 | 17/17 | GO |
|
||||
| system_registry | 603 | 14/16 | RUST |
|
||||
| construction_intent | 580 | 13/16 | RUST |
|
||||
| event_wait | 518 | 14/14 | GO |
|
||||
| tool_protocol | 461 | 22/27 | GO |
|
||||
| auto_tasks | 460 | 14/14 | GO |
|
||||
| goal | 453 | 20/20 | GO |
|
||||
| recall_uplift | 141 | 1/1 | RUST |
|
||||
| rune_spinner | 130 | 4/5 | RUST |
|
||||
| wait_guard | 116 | 1/1 | RUST |
|
||||
| failure_recovery | 108 | 1/1 | RUST |
|
||||
| programme_gates | 85 | 4/5 | RUST |
|
||||
| impact_preview | 69 | 2/2 | RUST |
|
||||
| impact_preview | 78 | 2/2 | RUST |
|
||||
| provider_context | 48 | 1/1 | GO |
|
||||
| route_risk_flags | 37 | 1/1 | RUST |
|
||||
@@ -39596,3 +39596,192 @@
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_486fb6001b34", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1b96d80aa993d9bae78e2a5e652b26f9f636fa13c3f835297083c8693c2cf8de", "schema_version": 1, "timestamp": "2026-10-09T09:16:21.648124+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_85ceaa53144e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:41106269075cb95f122f9e71216b0083dbdb65ef995e10171059560dad30b16a", "schema_version": 1, "timestamp": "2026-10-09T09:16:21.939283+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3d1e79ae1418", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4b086017902903f572bd2ac78cde09a6758513d4edaefdfd0b733445cc668b3", "schema_version": 1, "timestamp": "2026-10-09T09:16:22.373487+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_df15a18a829c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-10T18:59:15.412461+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_88b4067168d7", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-10T18:59:37.049502+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_095dae1f647f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:abc302ef7181931e9db856faa3d31ecfac0ff4135733d83087635e9a08cf3bcd", "schema_version": 1, "timestamp": "2026-10-10T21:35:52.947893+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4d13a00bde87", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f5787f8b4ecbda406b3ce2e05d4509d938bda92ab8a7f684a2982196cdd5213e", "schema_version": 1, "timestamp": "2026-10-10T21:35:53.016712+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5ae16e433a70", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:50833072d78c1294e2004ce615d312c6c3fa39c3b6d47abdbdc31ebf766ae0f3", "schema_version": 1, "timestamp": "2026-10-10T21:35:53.027822+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f0e985e867ad", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1bb884953641347ab6621fb285df8ee76bb212f9b4c728c9b21f2ca857d50447", "schema_version": 1, "timestamp": "2026-10-10T21:35:53.039045+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2f053ea5f0c4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:be0d464b55ec455ba3c518478a1285ec9f123c5fd7e8184bd7510aaa713f5ece", "schema_version": 1, "timestamp": "2026-10-10T21:35:59.842370+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0b0f9f6fcb4f", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T06:52:28.559194+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e9f1639498ed", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T06:52:28.625430+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7702f45497f4", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T06:54:37.234046+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_314281aec1c2", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T06:54:37.302427+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1a2af7b77936", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T06:55:44.587687+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_413af76bccb6", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T06:55:44.661416+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cc4c4d85ebce", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T06:55:46.717603+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8ec64c59500c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T06:55:46.768736+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_60e1a6b5878a", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T06:56:23.801272+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8ba1489e2f09", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T06:56:23.872546+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b9dd46cf5fe5", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T06:57:50.399657+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a4296124aafd", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T06:57:50.463153+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bd8e59e8cffc", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-11T07:35:02.909274+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_62497e05a5c9", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:16:41.821610+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3202b49d1a1c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-11T09:16:41.839928+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_510b72026264", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:16:41.857317+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cba79d855f28", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-11T09:16:41.876880+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5bd331249e97", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-11T09:16:41.886920+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d7047c378097", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.538033+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3aa0f7eb1695", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:89751b55d977c87c67ad1ae99a82e88373a2bc2ae197225271b17d95f5dff951", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.667202+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_92005da037c4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:790a2e63e0ef8fafcc3aada7e17b69089e47ec21f6cefa24d78978b130a83c18", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.682929+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b283bcd7cc72", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.692774+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9fd65df582cb", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.832872+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_55e7f077b28c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5f7358e2f8bf4f392f55c7b20e0aae469f02a58b865cb7a46fb55ad5d90f5c5d", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.846096+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d787f87c6e25", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a68c7b73b2103eb8162bfe914ab9aa296be66f7cfceb8ea5e4e4e15d6eb3b50d", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.853885+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_07047b65bd54", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1ec618e2ce63367c373b618db6c48228f8930d14062333160a670e43492a7357", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.865015+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_698ab5983865", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4bf60f10046b7edb775eca5eec90984be96f8a0b4e9f62cf75be2d1ecad7dddb", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.872876+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0de1da32cfdf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0f463eb4ee785882eda3d02650c077ef4670af8c987f3bacc45a3ded8d5faa13", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.883530+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_74205d75f561", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bda4ee03e284eef11a029d824c810fdec14c74bbe39936a62d7ab3c7e874fb77", "schema_version": 1, "timestamp": "2026-10-11T09:16:42.891495+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_114163b4962a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a56f89785518c08400c4ba795dcd44fe2d762e583e46388bff80a36c8544f7d4", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.045368+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c295d4c580f8", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:74ed90dc261f60967425cf3a850714cad914a85c724669b5af6fac3f06fde54e", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.054185+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_32bdf0ee4d39", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:48d5127691f49104465eb3043dce22853a0f31c74894d45a18d63eb07234ccc5", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.065884+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a9cb9886f432", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:42e37edf8d986e653bfbd8cec3ef246a579d4e324595a2c4ff2fc0159d746dca", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.082329+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cf8bce013d1d", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.096079+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cdd8cb9391c4", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.107482+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8c3ff4fb35cc", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.186568+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_682cbc9e565d", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.199765+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_708a629d6acd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:dd55c12fa74d107d6dc4af5b0856b6a0fac6f9d40686deae4e384dc0ef35456e", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.209329+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e01fa5392a44", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.277212+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9aa53b21545a", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.288445+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e078a8a6a1c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b3cd78cada825e39ec60d9c8a526331a01479e92a28a321d7889c4f1a7b7ceaf", "schema_version": 1, "timestamp": "2026-10-11T09:16:43.297100+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_053cec82635e", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:17:15.927289+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b5ee75e65fb2", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-11T09:17:15.948194+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ac70ff9313f9", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:17:15.985047+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3c4d9b23851e", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-11T09:17:16.048531+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1b5f46d69e84", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-11T09:17:16.067603+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_85683720a688", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:19:23.352131+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_eded5f5833ee", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-11T09:19:23.363980+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4fb04d004665", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:19:23.382236+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fc7912261507", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-11T09:19:23.404320+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ac385008d7fc", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-11T09:19:23.416951+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_42afde0101d7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7eed1c0da8b130e33a11e10c177471efa877868ed6a5537c2fd8b42e96862db1", "schema_version": 1, "timestamp": "2026-10-11T09:21:58.414033+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3298218c6260", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:85c23b1bb06147d2e6481892f78208e5eb95bbf2e2a9e7dac35813fda5211b9b", "schema_version": 1, "timestamp": "2026-10-11T09:21:58.759436+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d4409a857638", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:195c76cc6abd96195c3c74ab5b83aa969172caa13816395527ee874d844db760", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.011867+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_253bc204ad6e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7e4ead2708922e87e79351fc00813797fd21cbb8cbe6ceeb6070e668dc34881b", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.240285+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c01f89ef5f9e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9af31a88dad277fc7f6e32aca2e4169ffc05811b8fe14e9be8ba717b52851af5", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.525085+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_65ffa1b809b2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:837a9435fabc6cf476f0da895a4d42777c28e3574fff3344d92ba1f9c86cb9dd", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.809944+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4fbfa169237d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:80ad8295fc96f457c5782151eee83a874b9c3a4132929b7b91a8b168c1d3ce31", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.878876+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4048b2445b2c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:91cf7125b66b350487387bd4c3e98703345e5e93cce4c6ad716fcb3656b017e3", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.892018+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ecb2a4cd3451", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:07a784be2e9695da662d25a1a13b0d6c61ade2cbd142e2910ae0f6a2eca7648c", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.904897+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e05306ba9975", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2ef07bc91f06e3d4c1951b75475ce2270551f466baf0a716da30232c89ab12de", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.918875+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_29f902efd5de", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:68d7043ffbc56c44ca8474bc5430c1d55169072036ff0d30db662a7916ac2888", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.932351+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1010170203bd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b5a9d7d1e6814782eeba8cce438e73436dc69bf29ab2a7506bc0b7d31e225bac", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.945666+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d42acfecefc3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e17c9f0a96bec1da4697a66a1e821e5d97f94bc10c26feecf715cf851d3bd8bd", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.958992+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ce6a35bcee30", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a9b9202bcedcfc38e3d0fc0a398ece5a493243d464f87519ca0f6aac4808b82a", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.972810+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c30e25c28a69", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e901dc6d520f0220de0db70e3c3cbf868206b959b3fd19fde6cba9f5ebaf22eb", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.985777+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1e3fe8e8c572", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:eacd203af81df1aecde1082224aced872d69f4c0c186916a07891a9b8277c74a", "schema_version": 1, "timestamp": "2026-10-11T09:21:59.998743+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_669260ef6dfc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9d2c70365b429ec23ffe12d6ec30c5ac47715aff3b47b64495508c3b85b90be6", "schema_version": 1, "timestamp": "2026-10-11T09:22:03.785025+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f8653e9b0149", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b96a2625f1578db7a2bf3148ac49f9a740ef9a7636e6430d8667b742193e0e72", "schema_version": 1, "timestamp": "2026-10-11T09:22:06.641370+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_eab9f3fdb931", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6cd7f2bcbfc24c4f905a3d511fe01ca1d2ff93f201cc0c7506969af501ab7f21", "schema_version": 1, "timestamp": "2026-10-11T09:22:06.656769+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_72d18c4b8068", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a0c88d52eaa2e8c59cf454727e686bcdb77327c35ab531470f3d876ce727bcc6", "schema_version": 1, "timestamp": "2026-10-11T09:22:06.777583+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b1dc4f04f8a4", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8ddfc25bce80646f89018d30924221d98e8a47516212193edf0828d10dfb575a", "schema_version": 1, "timestamp": "2026-10-11T09:22:06.787144+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b615ac782216", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-11T09:22:06.802754+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_467aa6c7dcd6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:06c388b9fe5f3e88f61bd5c10a7dab9c4c20b7015ebfac53d197e39ea2871a2b", "schema_version": 1, "timestamp": "2026-10-11T09:22:07.059659+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a201e0811b8e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:207883e8c26b9c3ff8b5f58f8b1fe099c1d6af08b5a15cbe0a480ec14145212b", "schema_version": 1, "timestamp": "2026-10-11T09:22:07.071796+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_16e1327c9e6a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:17e2fee249131b9b9191f9922286bca0a6f53701d46833992badd2b8a78e74df", "schema_version": 1, "timestamp": "2026-10-11T09:22:07.081636+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0b88a8dc4a21", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:776a384548f0ffa51be3e36c10a9b1ed0009497401cceb8fcd46dc12f0a2ba72", "schema_version": 1, "timestamp": "2026-10-11T09:22:21.075493+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2f3e6d2ac9b4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5020958e95dc574e6eabd78dd76fe6745dbaecfbc54c029d5d4c2f90333f9092", "schema_version": 1, "timestamp": "2026-10-11T09:22:21.099910+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6cb3636dbd84", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5c5216a1149bc4413e04e981b04b2a58e34aa306ec65ca82198ecd20c5beb719", "schema_version": 1, "timestamp": "2026-10-11T09:22:21.120603+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_885450d362c6", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T09:22:21.163542+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b726d281ce7c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T09:22:21.233223+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a277707101f5", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.068839+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_79eb7a0d2e80", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c131524fb9108737c8c1cf97cc15edad8e837519227a54bff46bfc052f2e366a", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.088142+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0a1cb46094fd", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ee2cc0d628b7d54d2783671f1fd5e9a2adc2fc6afa76ee4ca2c927c663e3e068", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.096315+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b689ebc52712", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6f7c29ab1e55b8257c80e6b74db0031b0e19dba2220e4e1b5278e49252a09467", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.108694+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4bf6729a9197", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:aa7ed00aea2110e61842219560dbc89d71b911a02e892651575b6cf3efc8a6db", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.116637+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_febb7ac3186f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:84a17772ace53ad6c4aef715843b426d26a9ed19c865d0762aa073e3762b9eb8", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.126691+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0760fcfc2de1", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8ca206a69b34652eb22f6060290e53de0915ee462d0f2bf42bec87cf337c5a35", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.134431+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7a4eca26d898", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e9e700ad1ac02edb8fb1b6b746d8a401d6facfdfda1b5cb079d8ef81440213aa", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.321454+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f568a0ba10e1", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7797ec82341b5ae1bccf6fe5af98346ebf5777e794c7abbc10686613149106ee", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.330520+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b46c4d571c2b", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a3fea963731e8069dc9dfd8e763a082ef2931f841bc442df8a7b7043197e2dcb", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.344699+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5c4943e13eab", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5fb76067269e76c4034eba88182a91c657666c0ef00cba5081cbbaf9e19f1075", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.366387+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fc26bd3a02dc", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.381852+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5b184fbb4f26", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.392853+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0bb9daa31b83", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.546812+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ff850fc8e8f6", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.560420+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ca9e04fc7e93", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:50208f60a25372b5aeeb88010d5f8ca73e8a45ff1b8fa8f2037eb54bce6b9d31", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.570462+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4ce1b7836595", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.638383+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d3bc70b01286", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.652757+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_053452c1c2f2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0f3d3199b02101270dcc2b62d8dc98cea4a8b13663acc8bdfb7401cf6a7b8c49", "schema_version": 1, "timestamp": "2026-10-11T09:22:26.662786+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1234d5d34d6b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:77245dceff5f8f01a2e9eea9082ec7c0754f5b9d74229eb3375b9edc8dbd0614", "schema_version": 1, "timestamp": "2026-10-11T09:24:17.820196+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_af9af2b7c02a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ea5b7119f32723b70277f9bd202043879e5a2bb46733349cb1284a9f60a8ba7a", "schema_version": 1, "timestamp": "2026-10-11T09:24:17.832707+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1cf188ef6f6c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0f138cb65bb7884fbb8b2c3a36c80371a80766d132d3db9d1f55de5a2afd8bda", "schema_version": 1, "timestamp": "2026-10-11T09:24:17.844004+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_809f03496935", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b42c4f2aa63a34cc35a1e1e4b3123fbdd4c9abb5de29861d60a848af1cf84fb1", "schema_version": 1, "timestamp": "2026-10-11T09:24:17.855914+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_911dc19a127f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3d3d353894faed00593f16062f191a97308f30701439ab8629bb4591fa982803", "schema_version": 1, "timestamp": "2026-10-11T09:24:17.871321+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_715da5cf0ee0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8316d9bb86f65ba2b22b055bf432f72af552aee532cad4527dc00db1e22a0814", "schema_version": 1, "timestamp": "2026-10-11T09:24:20.352105+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5422fc7c70ac", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f018c9b186148bbe86146ae6a3d1757bcca0338eb14038fa12de8e13bd4a4bd1", "schema_version": 1, "timestamp": "2026-10-11T09:24:20.364533+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b5374a02f65b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:93b555a54a2d927025a6f724e05a0215dfcf1797a662bc6fdc3f97ce7df2db3b", "schema_version": 1, "timestamp": "2026-10-11T09:24:22.188641+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3f85ead9ac08", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3eb4568caf8c210e57600518872e9496eedc207d7c665b135b8134011bbc574d", "schema_version": 1, "timestamp": "2026-10-11T09:24:22.206274+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2bef3e4fb0bd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:058876fadbd74e79032edb3ccd68394ca45076ccc4fe46e2010b12dcc64bbcbc", "schema_version": 1, "timestamp": "2026-10-11T09:24:22.349004+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a09d0d980393", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4c8102a7e65e4c505c45cdec077a6031ca6011adeb1fc4b110d9cb0892a64d6", "schema_version": 1, "timestamp": "2026-10-11T09:24:22.359758+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_183861e35db0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:31f5cd5e1f1e1a1853e2e33e96180ccb6d8aa68ee056db4439c0a69ba5d2926c", "schema_version": 1, "timestamp": "2026-10-11T09:24:22.427042+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9fcaa3baa5bb", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bc113ee5342087695b3ee351c7952696490e8eaaa42f418431161d6c5122226f", "schema_version": 1, "timestamp": "2026-10-11T09:24:22.436531+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5a7bfdfb8f9e", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-11T09:24:23.130532+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8a0fb52baf48", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:28:19.359018+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8f7f89c0b4bc", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-11T09:28:19.372587+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f1dc5c949515", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:28:19.389705+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_774124098e55", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-11T09:28:19.410446+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7eea31d3a64a", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-11T09:28:19.422684+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f9944c0434f8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8cae58ea404244c78845a3beed6bcb3a4de3814eb16bb0649fec17b492c110b7", "schema_version": 1, "timestamp": "2026-10-11T09:32:33.052609+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7e4a0ed750bf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fd1582d4fee0f230789a63f868bfd8eed928a9955db4756e220f210a7c286e6d", "schema_version": 1, "timestamp": "2026-10-11T09:32:33.339224+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_27a6ee84906c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0a91bbc1558ee578cc5bcb0b2d9a16930f42eb70f55dd82d690e2fce957953c7", "schema_version": 1, "timestamp": "2026-10-11T09:32:33.578154+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c0ba24f87613", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:18b10f385bb30846c972719625dc2ffdac39e1b3e03c67b78c7eee1d1aa05443", "schema_version": 1, "timestamp": "2026-10-11T09:32:33.799344+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_36096260a150", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:88638b0003dda2294d73242c7d9a97e05c5682875a23fc73a743fd5410fc1d86", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.072963+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0cd310ed1bfc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:30480e0738f0dbccb0a9d1ec8847dd16cc6c6dfe2b5e25705f98051aa7f80bdd", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.401367+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_31c5fc772ab0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:30b4fa16694b4867e76e15e83888520b8d96c977645dfae57f42c6ed39492387", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.487004+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1972354b3f38", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d8cb6e1437ace101dd982ae77e546efbfa8a7a7926a7087a00ae6b8713b646b0", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.500483+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0e5396c97499", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0508f6dae1abc9a0b6b75e8c3ea6409881addcf41b775f2973d26951e40b8329", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.516051+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_308b3cc89f84", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4be51de9eb895b18d223d4e360666f1d82b613f709788cbfc8a4797b42d0423", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.532574+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_db15d4754db8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1597fb75d662b450438bd7070d2cf8b3f592b0904587ecb4cf71b748ad184785", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.549543+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5a980ffd966a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0b5199bfdc65d08bd2b9d8aec3a8f56b65392137a4256c1fe9162394072e17ed", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.566055+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e2859a6b8844", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fe080326db1ab90028723f0e5eea7ed746eb61e9f7aec5e1cf252b6dbaaae445", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.582693+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ae77947306ca", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b2065d3f7ffabfdcc5cf4aac984c505fd37a78a8bce09dcfb1ffab76165274ea", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.600917+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5b74f41f388c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4c5f389fb2afc2f1dd925389eaef3eaaa6ed73d9eb13c4bf3071b027da34f28f", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.620282+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a597609d6cbc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:20e5789ecfcc870ea01a46c0ef7b7ea7a4101511a89111df430fcae76f52a606", "schema_version": 1, "timestamp": "2026-10-11T09:32:34.637018+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0ef7d74c2c44", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7c5aa1069595106318d46770eac902807c34c8fc50230fa73336b1568e5ccfea", "schema_version": 1, "timestamp": "2026-10-11T09:32:36.989431+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d2c2937e92da", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5271c288f27cdd53eb10166f9dd0418c1e8ebffe1b4a0f9b5b4794ec30a1bc44", "schema_version": 1, "timestamp": "2026-10-11T09:32:39.776828+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3c699a70b8b2", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bd9b06702d5f6b795c1c45de313bd9ba0967c4a67bfa187464f98560993eebf4", "schema_version": 1, "timestamp": "2026-10-11T09:32:39.792036+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3100a10695df", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1f960c45b14fe3387e64b6429a807cccedc55aefdd43cc0d92d56a35f9d2a290", "schema_version": 1, "timestamp": "2026-10-11T09:32:39.896231+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fb9b44f8199c", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bbc641ba819b6dfc54c70ac5bc126801e951c3344d7fe84a4a979df542c8d753", "schema_version": 1, "timestamp": "2026-10-11T09:32:39.904975+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4d97873750b1", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-11T09:32:39.921295+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4fd0384293f9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:486e9a4033bd9a14e6988d3d10cf1f58623025264bcdbb07f3f4c0b8b6dfc1e6", "schema_version": 1, "timestamp": "2026-10-11T09:32:40.163872+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d50fe760e198", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e4ceb4d0c8fe5376f4e2c0a6edfafb0638b36c465de9f4025a1da665a8e4c91e", "schema_version": 1, "timestamp": "2026-10-11T09:32:40.175153+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3ae13f113984", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fd48add4219a478fe3866cc230828e7c295c74aa4abd2106b394576d8d9e416c", "schema_version": 1, "timestamp": "2026-10-11T09:32:40.184787+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f2fd025ebc2b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:acea546226a6ba1022a80ae4fc698edaa08e59353d17c8784a4660a4f190c5a2", "schema_version": 1, "timestamp": "2026-10-11T09:32:49.688346+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a5243ceb5b9e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5cc8602c1a60e4d9cad4a035439aaccd16fc12d3018ceb68c3f61caa534e8c69", "schema_version": 1, "timestamp": "2026-10-11T09:32:49.709830+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5f0db2989165", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9a44593d8685dbefe14f7d8ca36a1b5930eb8be98fc7bcdd9a73bf780d658d11", "schema_version": 1, "timestamp": "2026-10-11T09:32:49.728027+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_09efbd0b997e", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-11T09:32:49.772638+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9ab93b46b776", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-11T09:32:49.838892+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_43c22c2fb1af", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.444860+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a7a05819b18e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2264bec98542f2e2a11d9d1cfe60b040f615c893315e880188d5c22973945877", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.466730+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5aaaf5295019", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b38183152d7d7aea666a6da5e4a5e099e3a156a82a164d0219664ad8a0e66ad3", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.474429+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d77fab708199", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2f381a1dd890e49c8c425c0b805a389033dcfa8ad23fe2ab1d3f084f49b91698", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.486975+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_12191a6cce27", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f35723397d8a5cf18522bca90c970c0221ecd1d0f37f35289e5f44a492637654", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.494726+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d9afe518e387", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:13ba680f08a82ea0d598867364dfe528612975d1d95d5cbe365ca4db8feeba36", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.505525+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_21dbe1fbaeea", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1638dbd4d47a857b2a7f7a1d8bd36b89850f29c031bd9576cf26461a88f1adfc", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.513632+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_050eee3747c0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:468b97a3091dbb6aa8aea455b0c4404f2aa5c51737bb608e6440bae8942509e3", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.670909+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a641a1ac704e", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bcbbaf8f7327c9f4e32eb4553821a57d1dac058066dc976d1ca39cf1e60d372f", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.679650+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_da3965bafa6a", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fbafa677a53e77ba0abe2b3e10be342bd8451a57920c0cff8c817bd520bac363", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.692461+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_68c37edd8f96", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:22c3d15beae0123d41217b3a09e074168dd32b8f92b15b87e43d46fd6782f482", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.712118+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_119c940fa16f", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.726726+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8313c92fb4a8", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.737922+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_54f15deb33be", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.887932+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bb350160bc22", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.901334+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0a9102075555", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:34af41205cdde04977b287cd819587e8ab8657b46f3621889b2099ce9a724abd", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.911286+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3bb497558f86", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-11T09:32:54.994976+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a52d69f08058", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-11T09:32:55.008440+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8ce02ffde178", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d98d0d7edf052abacc0cd0a7306a5696042319fe155f7cabe2f41e5cac017944", "schema_version": 1, "timestamp": "2026-10-11T09:32:55.016804+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3889a1d004c6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a2ee6de84e5406b1da4f4560788c7d81d48461b57d31abdd236cca3f81d8e648", "schema_version": 1, "timestamp": "2026-10-11T09:34:45.806220+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6dd516dd0ab7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d2134b2910c226b6361d0fb92481227a7a1e1c7fdb6cb3d523eb2b2cfd4c0730", "schema_version": 1, "timestamp": "2026-10-11T09:34:45.818154+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fc8947b0fa2a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:696393c7215b7507e7a29a7d9f7038b76e5d8da2274327bb9836d3b80d7ab3a2", "schema_version": 1, "timestamp": "2026-10-11T09:34:45.829009+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_486a1884c586", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a21b151562f963a6b51ee731b0f7fa6e23f2d58a28a4871effb30bc8ed1c95df", "schema_version": 1, "timestamp": "2026-10-11T09:34:45.840340+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_42338e0a434f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ff0b316dae2d92caedc01508aae79e6b646520f8453a39c4e3efd141d90ead3a", "schema_version": 1, "timestamp": "2026-10-11T09:34:45.854962+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a54da5d2b822", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:af1fec757c013cf7a4b37425aab54a72b4b0acf357066f0360d4c1d7ee202457", "schema_version": 1, "timestamp": "2026-10-11T09:34:48.104078+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1eb9755edaaa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:83cabfc0b51ae8ad9a684ea43eee30ec2c80ce1b75d197e6e72cfa9286faabe5", "schema_version": 1, "timestamp": "2026-10-11T09:34:48.117536+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_874c33513987", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:28a6a1a8a10d3a854ca8599cac55955594cfc264bf737a8ec805fbb0c322bd4e", "schema_version": 1, "timestamp": "2026-10-11T09:34:49.837067+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_960fc6158ae0", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1072a05b246ac0e1b072907a3405d5f3b27b6a8e19c2b66ae2befa999ec58f7f", "schema_version": 1, "timestamp": "2026-10-11T09:34:49.852332+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_af904d3dca68", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:abe77b1a11a8bbfe295d1b7a23cd419bfa22756037e62a9fa6695f21ea1ec132", "schema_version": 1, "timestamp": "2026-10-11T09:34:49.966141+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8990cbf75d4b", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8f6649c3d4d6e31dbb4785d669bf551022452a3cbe49d18e90f493cb5513dbb1", "schema_version": 1, "timestamp": "2026-10-11T09:34:49.975215+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_49553717d07a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:39c1f7173f0b584d721680c7261c7c56f72070918331d89d885d9410c990f3d0", "schema_version": 1, "timestamp": "2026-10-11T09:34:50.037043+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b59726a0430b", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4736f86bd893cf0e7a1644ae46b76ebbfa13a588073a31424e359af8d617b1f1", "schema_version": 1, "timestamp": "2026-10-11T09:34:50.045018+00:00"}
|
||||
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6a0c1f609d21", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-11T09:34:50.698579+00:00"}
|
||||
@@ -0,0 +1,262 @@
|
||||
# TRANCHE: THE FULL DICHOTOMY REBASE v2 — every Python module replaced, zero Python runtime
|
||||
|
||||
_Revised per Nick's corrections: Bubble Tea for the TUI, henxels scope first, HOW instructions per step, Gitea epic + MiMoCode tasks, timeline = 1 week. Presented for approval._
|
||||
|
||||
---
|
||||
|
||||
## §0 — GROUNDING (measured)
|
||||
|
||||
| Language | Files | Lines | % | Status |
|
||||
|---|---|---|---|---|
|
||||
| Python | 180 | 68,224 | **70%** | **THE TARGET — every module ported** |
|
||||
| Rust | 106 | 27,304 | 28% | 10 crates, 85 more modules to port |
|
||||
| Go | 7 | 1,125 | **1%** | 2 modules, 55 more to port |
|
||||
| **Total** | **293** | **96,653** | | |
|
||||
|
||||
### The corrected dichotomy
|
||||
|
||||
| Side | Modules | Python lines | Role |
|
||||
|---|---|---|---|
|
||||
| **GO** | 57 | 32,081 | Reactive shell: initiates, responds, routes, interfaces |
|
||||
| **RUST** | 85 | 25,218 | Deterministic backend: computes, stores, pure functions |
|
||||
| **TUI** | 7 | 5,435 | **Bubble Tea (Go)** — rewritten in Wave 7 |
|
||||
| **TEST** | 29 | 5,397 | Test infrastructure — stays Python (pytest) |
|
||||
|
||||
### Keep-greens
|
||||
|
||||
Full test suite (2,300+, 0 failures) · All 25 registered canaries · diff-cover ≥70% · Silent-handler ratchet (BLE001 ≤345, S110 ≤75) · Henxels all hold · Lint-baseline no increase · Deploy monotonic
|
||||
|
||||
---
|
||||
|
||||
## §1 — OBJECTIVE
|
||||
|
||||
**Zero Python files in `src/rune/`.** Every module replaced by its Go or Rust equivalent. The TUI is rewritten in Bubble Tea (Go). Test infrastructure stays Python. The end state is a Go+Rust codebase with zero Python runtime dependency.
|
||||
|
||||
Timeline: **1 week** (5 working days, aggressive but achievable with the wave structure below).
|
||||
|
||||
---
|
||||
|
||||
## §2 — WAVE 0: INFRASTRUCTURE FIRST (Day 1, morning)
|
||||
|
||||
**This impacts everything else. Done before any porting.**
|
||||
|
||||
### Step 1: Henxels scope extension
|
||||
- Add `src/rust/*/` scope to `henxels.yaml`: max_lines 3000 (Rust files are larger), no_secrets, filename_casing snake_case, canary registration
|
||||
- Add `src/go/**/` scope: max_lines 2000, no_secrets, filename_casing kebab-case
|
||||
- Add `new_rune_modules_have_canary_or_test_coverage` for both scopes
|
||||
- Verify: `henxels check --all` passes with the new scopes
|
||||
|
||||
### Step 2: Umbrella manifest extension
|
||||
- Add 10 Rust crate entries to `config/umbrellas.json` (cognition_core, memory_retrieval, memory_storage, scaffold_core, evidence_provenance, rune_spine, memory_scoring, capability_scoring, token_counting, query_memory)
|
||||
- Add 2 Go module entries (event-bus, tool-executor) + placeholder entries for the 55 planned Go modules
|
||||
- Each entry: name, description, modules list, depends_on, canaries
|
||||
- Verify: `umbrella_manifest_is_complete` henxel passes
|
||||
|
||||
### Step 3: Scaffolding protocol update
|
||||
- Update `construction_decision.py` GO_SIGNALS/RUST_SIGNALS to match the corrected dichotomy (57 GO / 85 RUST)
|
||||
- Update `store/rebase/classification.json` with the re-classified sides
|
||||
- Verify: `rune scaffold classify <test_objective>` routes to the correct language
|
||||
|
||||
### Step 4: CI pipeline update
|
||||
- Add `cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml` to the unit-tests and canary jobs
|
||||
- Add `CC_RUST_LIB` to the unit-tests job env
|
||||
- Add a `go-build` step that builds every Go module in `src/go/*/`
|
||||
- Add a `go-test` step that runs `go test ./...` in every Go module
|
||||
- Verify: CI pipeline passes with the new steps
|
||||
|
||||
### Step 5: Gitea epic + MiMoCode tasks
|
||||
- Create the parent epic on Gitea: "Full Dichotomy Rebase — every Python module replaced" with the full wave structure as a folded checklist
|
||||
- Create per-wave sub-epics (W4-infra, W4b-tools, W5a-model, W5b-operator, W5c-memory, W6a/b/c-RUST, W7-TUI)
|
||||
- Mirror all of them in the MiMoCode task tool
|
||||
- Verify: the Gitea epic exists, the MiMoCode task tree matches
|
||||
|
||||
---
|
||||
|
||||
## §3 — WAVE STRUCTURE
|
||||
|
||||
### Wave 4: Go reactive infrastructure (14 modules, ~5,271 lines) — Day 1-2
|
||||
|
||||
| Module | Size | HOW |
|
||||
|---|---|---|
|
||||
| event_bus | 244 lines | **DONE** — `src/go/event-bus/` |
|
||||
| event_envelope | 303 lines | Merge into event-bus binary. Add EventEnvelope struct with all fields (event_id, event_type, partition_key, payload_hash, timestamp). Port `OrchestrationRecord.to_dict()` as a `MarshalJSON()` method. |
|
||||
| message_queue | 10KB | Go channel + JSONL file backing. `type MessageQueue struct { mu sync.Mutex; path string }`. Methods: `Enqueue(msg)`, `Dequeue()`, `GetApproved()`, `MarkExecuted(id)`. Port the approved-message flow from Python. |
|
||||
| hook_engine | 28KB | Go map of hook name → []func(data) error. `type HookEngine struct { hooks map[string][]HookFunc; mu sync.RWMutex }`. Methods: `Register(name, fn)`, `Fire(name, data) error`, `FireAsync(name, data)`. Load hooks from a JSON config file. |
|
||||
| microkernel | 14KB | Go interface-based plugin system. `type Plugin interface { Name() string; Capabilities() []string; OnMessage(data) error }`. Registry with `Register(p Plugin)`, `Fire(event, data)`. Uses goroutines for parallel plugin dispatch. |
|
||||
| subsystem_health | 6KB | Go struct with `sync.Mutex`. `type SubsystemHealth struct { failures map[string][]Failure }`. Methods: `RecordFailure(subsystem, err, tier)`, `GetHealth() map[string]HealthStatus`. Write to a JSON file for durability. |
|
||||
| parallel_processor | 5KB | Go goroutines + `sync.WaitGroup`. `func ParallelMap(items []T, fn func(T) T) []T`. Run fn in goroutines, collect results. |
|
||||
| work_manager | 20KB | Go worker pool. `type WorkManager struct { workers int; queue chan Task; results chan TaskResult }`. Methods: `Start()`, `Submit(task)`, `Stop()`. Goroutines pull from the channel. |
|
||||
| failure_auto_filer | 21KB | Go struct with a spool file + forge client. `type FailureFiler struct { client ForgeClient; spool *JsonlSpool }`. Methods: `File(failure)`, `Reconcile()`. Uses the existing `create_forge_client()` pattern. |
|
||||
| cache | 16KB | Go `sync.Map` or LRU cache. `type Cache struct { mu sync.RWMutex; data map[string][]byte; maxAge time.Duration }`. Methods: `Get(key)`, `Set(key, val)`, `Invalidate(key)`. |
|
||||
| connection_pool | 3KB | Go channel-based pool. `type Pool struct { conns chan Conn; factory func() Conn }`. Methods: `Get() Conn`, `Put(conn)`. |
|
||||
| cron_engine | 14KB | Go time.Ticker + task list. `type CronEngine struct { tasks []CronTask; mu sync.Mutex }`. Methods: `AddTask(name, expr, fn)`, `Start()`, `Stop()`. Parse cron expressions with a simple parser. |
|
||||
| night_mode | 14KB | Go scheduled consolidation. Uses the cron_engine internally. `type NightMode struct { engine *CronEngine; tasks []NightTask }`. |
|
||||
| recall_router | 5KB | Go routing table. `type RecallRouter struct { routes map[string]RecallFn }`. Method: `Route(query) []RecallResult`. |
|
||||
|
||||
**Daily check**: all 14 Go binaries compile, their tests pass, the Python versions delegate via subprocess.
|
||||
|
||||
### Wave 4b: Go tool execution layer (3 modules, ~3,079 lines) — Day 2
|
||||
|
||||
| Module | Size | HOW |
|
||||
|---|---|---|
|
||||
| tool_executor | 73KB | **STARTED** — add the remaining operations: write_file (with path validation + ACL), edit_file (with diff preview), run_tests (go test), lint_file, git_commit, git_diff, git_log, repo_map, read_persona, query_memory, check_growth, diagnose_behavior, find_capability |
|
||||
| tool_protocol | 16KB | Go types: `type ToolRequest struct { CallID, Name string; Args map[string]any; Source string }`, `type ToolEvent struct { Kind string; Request ToolRequest; Result map[string]any }`. Validation against a JSON schema registry. |
|
||||
| local_tool_runtime | 35KB | Go loop: receive model response → parse tool calls → validate → dispatch to executor → feed results back. `type LocalToolRuntime struct { transport Transport; tools []ToolDef; maxIterations int }`. Methods: `Run(messages, options) OperatorResponse`. |
|
||||
|
||||
**Daily check**: all tool operations work end-to-end in Go.
|
||||
|
||||
### Wave 5a: Go model/provider layer (10 modules, ~3,398 lines) — Day 2-3
|
||||
|
||||
All 10 modules go into ONE Go binary: `src/go/model-connect/`.
|
||||
|
||||
| Module | Size | HOW |
|
||||
|---|---|---|
|
||||
| model_connect | 16KB | Go HTTP client for Ollama. `type OllamaClient struct { baseURL string; client *http.Client }`. Methods: `Chat(model, messages, options) ChatMessage`, `Show(model) ModelInfo`, `Pull(model)`, `ListModels()`. |
|
||||
| model_profiles | 16KB | Go struct: `type ModelProfile struct { Name string; Provider string; ContextLength int; Capabilities []string }`. Load from JSON. |
|
||||
| model_registry | 6KB | Go registry: `type ModelRegistry struct { profiles map[string]ModelProfile; mu sync.RWMutex }`. Methods: `Register(p)`, `Get(name)`, `List()`. |
|
||||
| model_routing | 16KB | Go router: `type ModelRouter struct { profiles *ModelRegistry; defaultModel string }`. Method: `Route(taskDescription) string`. Scoring: keyword matching + profile capabilities. |
|
||||
| openai_compat_transport | 10KB | Go HTTP client for [OI]-compatible APIs. `type CompatTransport struct { baseURL, apiKey string; client *http.Client }`. Same Chat() interface. |
|
||||
| provider_budget | 12KB | Go struct: `type ProviderBudget struct { providers map[string]Budget; mu sync.Mutex }`. Methods: `Record(provider, tokens)`, `GetStatus() map[string]BudgetStatus`. |
|
||||
| provider_context | 2KB | Go context type: `type ProviderContext struct { Provider, Model string; ContextLength int }`. |
|
||||
| provider_registry | 23KB | Go registry for all providers (Ollama + [OI] + custom). `type ProviderRegistry struct { providers map[string]Provider; mu sync.RWMutex }`. Methods: `Connect(name, url, key)`, `GetProvider(name)`, `ListProviders()`. |
|
||||
| standardized_providers | 9KB | Go interface: `type Provider interface { Chat(...) ChatMessage; Show(...) ModelInfo; ListModels() []string }`. Implementations: OllamaProvider, CompatProvider. |
|
||||
| tiered_executor | 9KB | Go executor with tier escalation: `type TieredExecutor struct { tiers []Tier; current int }`. Methods: `Execute(task) Result`, `Escalate()`. |
|
||||
|
||||
**Daily check**: Ollama chat works from Go, model routing works, budget tracking works.
|
||||
|
||||
### Wave 5b: Go operator (19 modules, ~9,637 lines) — Day 3-4
|
||||
|
||||
The biggest wave. ALL of these go into ONE Go binary: `src/go/operator/`.
|
||||
|
||||
| Module | Size | HOW |
|
||||
|---|---|---|
|
||||
| rune_operator | 126KB | The turn loop. `type Operator struct { state *RuneState; taskState *TaskStateRepo; conversation *ConversationRepo; toolRuntime *LocalToolRuntime; modelRouter *ModelRouter }`. The main method: `Respond(channel, message) OperatorResponse`. This is the biggest single port — expect 2-3k lines of Go. |
|
||||
| adaptive_router | 23KB | `type AdaptiveRouter struct { routes []Route; baselineSHA string }`. Method: `Route(task) RouteDecision`. Route types: LOCAL_FIX, STANDARD_CHANGE, RESEARCHED_CHANGE, etc. |
|
||||
| build_mode | 20KB | Go build orchestrator: `type BuildMode struct { operator *Operator; scaffold *ScaffoldClient }`. Method: `Execute(task) BuildResult`. |
|
||||
| builder_bridge | 12KB | Go bridge: dispatches build tasks to external builders (MiMo, Kimi, GLM). `type BuilderBridge struct { builders map[string]Builder }`. |
|
||||
| builder_bridge_cli | 1KB | Thin CLI wrapper for builder_bridge. |
|
||||
| core_server | 9KB | Go HTTP server: `http.ListenAndServe(":8080", mux)`. Routes: `/v1/chat`, `/v1/models`, `/health`. Uses Go's `net/http` stdlib. |
|
||||
| persona_definition | 7KB | Go struct: `type Persona struct { Name, Role, Voice string; Principles []string }`. Load from JSON. |
|
||||
| voicebox | 4KB | Go voice synthesis dispatch. |
|
||||
| voice_injector | 3KB | Go voice injection. |
|
||||
| release_notes | 18KB | Go release note generator. Reads closed contracts + resolved issues from the Gitea API. Generates markdown. |
|
||||
| auto_context | 8KB | Go auto-context builder. Scans recent events + memory to build a context block. |
|
||||
| auto_tasks | 15KB | Go auto-task extraction from conversation. |
|
||||
| event_wait | 23KB | Go event-driven wait. `type EventWait struct { watchers map[string]chan Event; mu sync.RWMutex }`. Methods: `Register(op, contractID)`, `Await(op, timeout) <-chan Event`. |
|
||||
| evidence_probe | 7KB | Go evidence probe: spawns a subprocess to resolve an unknown. |
|
||||
| forge | 5KB | Go forge adapter interface. |
|
||||
| forge_client | 7KB | Go forge client (Gitea API). |
|
||||
| goal | 14KB | Go goal management: `type GoalManager struct { goals []Goal; mu sync.Mutex }`. |
|
||||
| growth_delta | 16KB | Go growth delta extraction. |
|
||||
| warm_router | 55KB | Go warm routing: scans memory store, scores relevance, builds context. The second biggest module. |
|
||||
|
||||
**Daily check**: the operator turn loop works end-to-end in Go — user sends a message, operator routes, dispatches to Rust kernels, returns a response.
|
||||
|
||||
### Wave 5c: Go memory operations (8 modules, ~8,240 lines) — Day 4
|
||||
|
||||
| Module | Size | HOW |
|
||||
|---|---|---|
|
||||
| memory_store | 99KB | Go SQLite operations via `mattn/go-sqlite3` (cgo) or `modernc.org/sqlite` (pure Go). `type MemoryStore struct { db *sql.DB }`. Methods: `AddEntry(entry)`, `SearchEntries(query)`, `GetEntry(id)`, `Supersede(id)`, `WashEntry(id)`. THIS IS THE BIGGEST SINGLE PORT — expect 3-5k lines of Go. |
|
||||
| curator | 70KB | Go memory curation: scans the store, identifies conflicts, promotes/demotes. `type Curator struct { store *MemoryStore; mu sync.Mutex }`. |
|
||||
| sleep_time | 38KB | Go sleep-time consolidation: runs during idle periods. Uses the consolidation kernel (Rust) for the compute. |
|
||||
| memory_consolidation | 31KB | Go consolidation orchestrator. Calls the Rust kernel (already ported) for the record assembly. |
|
||||
| persona_gate | 24KB | Go persona gating: checks responses against persona principles. |
|
||||
| abstraction | 22KB | Go abstraction: fact extraction (kernel already in Rust). |
|
||||
| witness | 15KB | Go witness: signal extraction + reflection dispatch. |
|
||||
| stage_guards | 12KB | Go stage guards: readiness/exit canaries for construction stages. |
|
||||
|
||||
**Daily check**: memory operations work end-to-end in Go, SQLite reads are byte-identical.
|
||||
|
||||
### Wave 6: The Rust deterministic backend (85 modules, ~25,218 lines) — Day 4-5
|
||||
|
||||
Three sub-waves. Each module becomes a `pub fn` in the appropriate crate.
|
||||
|
||||
**Wave 6a** (30 modules): structural core → `src/rust/construction/`
|
||||
- spine, structural_store, construction_decision, construction_interface, construction_commands, construction_intent, construction_mode, construction_pattern, construction_patterns_builtin, construction_projection, construction_depth, construction_activation, contracts, capability_broker, capability_fit, capability_ledger, audit_ledger, behaviour_ledger, claim_audit, outcome_ledger, build_basis, project_registry, system_registry, layout_contract, structural_migration, structural_model
|
||||
- HOW: each Python module's functions become `pub fn` in a shared `construction` crate. Data structures become Rust structs with serde. The Python module becomes a thin FFI caller via the existing `sc_loader` pattern.
|
||||
|
||||
**Wave 6b** (30 modules): cognition layer → `src/rust/cognition/`
|
||||
- drift_core, drift_compression (already in Rust), drift_cache, drift_ledger, drift_observability, drift_resume, graph_memory, graph_projection, memory_selector, identity_anchors, pins (already in Rust), salience_gate, self_improve, token_optimizer, cognitive_budgeter, context_pressure, inference_trace, recall_uplift, response_adapter, route_risk_flags, rune_profiler, rune_spinner, safety_override (already in Rust), shard_manager, progressive_escalation, failure_recovery, intent_continuity, impact_preview, iteration, task_gate
|
||||
- HOW: each module's deterministic functions become `pub fn` in the `cognition_core` crate (extending the existing broker.rs).
|
||||
|
||||
**Wave 6c** (25 modules): remaining → the appropriate crate
|
||||
- voice_hybrid, voice_sync, prompt_sync, time_awareness, toolchain, verification_layer, task_persist, testing_mode, wait_guard, wait_guard_corpus, warm_neuron_store, layout_contract, structural_migration, structural_model, programme_continuation, programme_gates, subconscious_pass, attention_capsule, dependency_tracker, ffi_query_memory_smoke
|
||||
- HOW: same pattern — Rust `pub fn` in the appropriate crate, Python FFI caller.
|
||||
|
||||
**Daily check**: each crate compiles, its tests pass, the Python delegation works.
|
||||
|
||||
### Wave 7: Bubble Tea TUI (Go) — Day 5
|
||||
|
||||
| What | Python lines | Go target |
|
||||
|---|---|---|
|
||||
| rune_tui.py (the whole TUI) | 4,038 | `src/go/tui/` |
|
||||
|
||||
**HOW:**
|
||||
1. Install Bubble Tea + Lipgloss + Bubbles: `go get github.com/charmbracelet/bubbletea github.com/charmbracelet/lipgloss github.com/charmbracelet/bubbles`
|
||||
2. Port the layout: `type Model struct { chatChan, taskChan ChannelView; workPane WorkPane; statusBar StatusBar }` — chat LEFT, task RIGHT
|
||||
3. Port the message rendering: chat bubbles with `lipgloss.Style{}.Foreground()`
|
||||
4. Port the command selector: Bubble Tea's `textinput` + `completions` component
|
||||
5. Port the keybindings: Ctrl+1 (chat), Ctrl+2 (task), Ctrl+3 (work pane)
|
||||
6. Port the status bar: version, mode, model, project, focused channel
|
||||
7. Port the post-response signal consolidation (one dim line)
|
||||
8. Wire the operator: the TUI calls the Go operator directly (same process) — no subprocess, no FFI, just function calls
|
||||
9. Animations: Bubble Tea supports animations via `tea.Tick` and `tea.Every` — spinners, progress bars, streaming text. Lipgloss supports colors, borders, padding. Pixel art is possible via block characters and custom rendering.
|
||||
10. Design: retain the current two-column layout (chat LEFT, task RIGHT) + work pane. Use Lipgloss for the same visual style. Port the chat bubble rendering, the thinking indicator, the tool call display.
|
||||
|
||||
**Exit condition**: `go build ./src/go/tui/` produces a statically-linked binary that IS the Rune TUI. `find src/rune -name '*.py'` returns 0.
|
||||
|
||||
---
|
||||
|
||||
## §4 — THE GITEA EPIC STRUCTURE
|
||||
|
||||
The FIRST task posted to Gitea is the parent epic:
|
||||
|
||||
**Epic #N: "Full Dichotomy Rebase — every Python module replaced"**
|
||||
|
||||
Folded checklist inside the epic:
|
||||
- [ ] Wave 0: Infrastructure (henxels scope, umbrella manifest, scaffolding protocol, CI update)
|
||||
- [ ] Wave 4: Go reactive infrastructure (14 modules) — sub-epics per module
|
||||
- [ ] Wave 4b: Go tool execution (3 modules) — sub-epics per module
|
||||
- [ ] Wave 5a: Go model/provider (10 modules) — sub-epics per module
|
||||
- [ ] Wave 5b: Go operator (19 modules) — sub-epics per module
|
||||
- [ ] Wave 5c: Go memory operations (8 modules) — sub-epics per module
|
||||
- [ ] Wave 6a: Rust structural core (30 modules) — sub-epics per module
|
||||
- [ ] Wave 6b: Rust cognition (30 modules) — sub-epics per module
|
||||
- [ ] Wave 6c: Rust remaining (25 modules) — sub-epics per module
|
||||
- [ ] Wave 7: Bubble Tea TUI (7 modules) — sub-epics per module
|
||||
|
||||
Each sub-epic contains the HOW instructions per the template in §3.
|
||||
|
||||
MiMoCode task tool mirrors the same structure: T-tranche → T4-infra → T4.1-event-bus, T4.2-hook-engine, etc.
|
||||
|
||||
---
|
||||
|
||||
## §5 — BUBBLE TEA CAPABILITIES (answering Nick's question)
|
||||
|
||||
**Animations**: Yes. Bubble Tea has `tea.Tick` (one-shot timer) and `tea.Every` (recurring). Spinners via the `bubbles/spinner` component. Progress bars via `bubbles/progress`. Streaming text via incremental updates.
|
||||
|
||||
**Pixel art**: Possible via block characters (█ ▄ ▀ ▌ ▐) and custom renderers. Not true pixel-level control (it's a terminal), but with Unicode block elements and 256-color/truecolor support, you can get very close. Lipgloss supports borders, padding, alignment, colors, and gradients.
|
||||
|
||||
**Presentable**: Yes. Bubble Tea + Lipgloss is what popular Go CLI tools use (gh, glow, soft-serve). It looks professional and modern.
|
||||
|
||||
**Retaining the current design**: The two-column layout (chat LEFT, task RIGHT), the work pane, the status bar, the command selector, the chat bubbles — all of these map directly to Bubble Tea components. The visual style can be replicated with Lipgloss. The keybindings map directly.
|
||||
|
||||
---
|
||||
|
||||
## §6 — TIMELINE
|
||||
|
||||
| Day | Waves | Modules | Lines |
|
||||
|---|---|---|---|
|
||||
| Day 1 (morning) | Wave 0 | Infrastructure | N/A (config changes) |
|
||||
| Day 1-2 | Wave 4 + 4b | 17 Go modules | ~8,350 |
|
||||
| Day 2-3 | Wave 5a + 5b | 29 Go modules | ~13,035 |
|
||||
| Day 4 | Wave 5c | 8 Go modules | ~8,240 |
|
||||
| Day 4-5 | Wave 6a + 6b + 6c | 85 Rust modules | ~25,218 |
|
||||
| Day 5 | Wave 7 | 7 Bubble Tea modules | ~5,435 |
|
||||
| **Total** | **5 days** | **180 modules** | **~68,224** |
|
||||
|
||||
---
|
||||
|
||||
## §7 — APPROVAL GATE
|
||||
|
||||
Read this. Argue with it. Approve or request changes. Once approved, execution starts at Wave 0 (infrastructure first, because it impacts everything else).
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.183.0"
|
||||
version = "0.203.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Wave 1 / Memory & Persistence — Inward Intention Profile
|
||||
|
||||
Checkout: `/mnt/workdrive/rune`. Scope: `src/rune/` memory subsystem. Method: static reading of the
|
||||
live implementation plus inspection of on-disk state under `~/.rune/`. No code changed.
|
||||
|
||||
## On-disk state (measured)
|
||||
|
||||
| Artifact | Size | Status |
|
||||
|---|---|---|
|
||||
| `~/.rune/hot/task_state.json` | 973 KB | live production, hot path |
|
||||
| `~/.rune/hot/conversation_messages.json` | 1.2 MB | live production, hot path |
|
||||
| `~/.rune/consolidation/knowledge.json` | 3.4 MB | live production, batch path |
|
||||
| `~/.rune/outcome_ledger.db` | 12 KB | SQLite, small |
|
||||
| `~/.rune/memory/store/memory.db` | — | defined by `memory_env.store_db()`, not yet materialized locally |
|
||||
|
||||
## Cluster 1 — Hot-store JSON persistence
|
||||
|
||||
- Intention: load/save mutable runtime state as JSON files (task state, conversation messages, knowledge).
|
||||
- Evidence: live files listed above; consolidation loads/saves `knowledge.json` in
|
||||
`memory_consolidation.py:113-165` (JSON file load/save around the batch iterate/dedupe loop).
|
||||
- Compute profile: stdlib `json` encode/decode over 1–3.4 MB documents; CPU-bound but small in
|
||||
absolute terms (tens of ms, not seconds). No incremental/append format, full rewrite per save.
|
||||
- Language fit: **Python already delegates this well** — see outward source 1 (orjson wheel) for the
|
||||
upgrade path. A bespoke Rust CLI unit pays per-call subprocess spawn (contract: argv in / JSON out)
|
||||
that likely exceeds the serialization time it saves.
|
||||
|
||||
## Cluster 2 — SQLite store + FTS5/vec0 hybrid recall (`memory_store.py`)
|
||||
|
||||
- Intention: dense + sparse retrieval fused by RRF, with query expansion, metadata rerank, QPP
|
||||
abstention, drift rerank, map-reduce recall.
|
||||
- Evidence:
|
||||
- `memory_store.py:575-897` — `recall()`: hybrid dense+BM25 fused by Reciprocal Rank Fusion.
|
||||
- `memory_store.py:646` — dense KNN runs as SQLite virtual-table query: `SELECT ... FROM entries_vec v WHERE v.embedding MATCH ?` → **C inside SQLite** (sqlite-vec; outward source 3).
|
||||
- `memory_store.py:693` — sparse half ranks with `bm25(entries_fts)` → **C inside SQLite** (FTS5; outward source 5).
|
||||
- `memory_store.py:327-370` — `_embed()` shells out via subprocess `curl` to Ollama; network-bound, 60 s timeout. This, not Python, dominates recall latency.
|
||||
- `memory_store.py:475` — `_df()` runs a per-word LIKE COUNT query (N+1 smell) but is bounded: `probe_cap ≈ 2 × budget` entries (≈ 40), so total SQL traffic is tiny.
|
||||
- Python-side loops: RRF fusion over the small probe set, recency-decay/confidence/temperature multipliers, QPP abstention — per-item branching math over ≤ ~40 candidates.
|
||||
- Compute profile: the heavy kernels (KNN, BM25, ranking, tokenization) are already native C. Python is a thin orchestrator around them.
|
||||
- Language fit: a Go/Rust recode would replace thin Python glue around the same C extensions with
|
||||
Go/Rust glue around the same C extensions, while losing ecosystem integration (`sqlite3`, orjson,
|
||||
embedding plumbing). Outward sources 2, 3, 5 all point the same direction: keep SQLite, don't build custom storage/search.
|
||||
|
||||
## Cluster 3 — Neuron scoring / routing (`warm_router.py`)
|
||||
|
||||
- Intention: 7-signal heuristic scoring of memory "neurons" for routing: keyword substring match,
|
||||
embedding cosine, context overlap, ACT-R base-level activation, wash boost (log1p), fan penalty,
|
||||
trigger-length bonus.
|
||||
- Evidence: `warm_router.py:361-503` (`score_neuron`), `warm_router.py:504` (`route`).
|
||||
- Compute profile: the embedding cosine is computed via `memory_store._embed` (network HTTP to
|
||||
Ollama, precomputed once per neuron set). The remaining signals are trivial string ops and scalar
|
||||
arithmetic per neuron. Latency is dominated by the HTTP embed, not the scoring loop.
|
||||
- Language fit: branching heuristics over a small N with a network-bound dominate cost — no case for
|
||||
Go/Rust/C.
|
||||
|
||||
## Cluster 4 — Graph memory BFS (`graph_memory.py`)
|
||||
|
||||
- Intention: entity/relation graph persisted in SQLite (`edges`, `entities` tables); query walks BFS
|
||||
from seed entities up to `hops` (default 1); `graph_write` (:199) upserts.
|
||||
- Evidence: `graph_memory.py:261-343` (`query`), `graph_memory.py:199` (`graph_write`).
|
||||
- Compute profile: per-node SQL edge queries (N+1 pattern), but N is tiny (hops=1, few entities per
|
||||
seed). The per-query cost is SQLite C; Python overhead is negligible.
|
||||
- Language fit: keep Python. If the graph ever scales, the fix is a recursive CTE in SQL — a
|
||||
SQLite-level change, not a language change.
|
||||
|
||||
## Cluster 5 — Batch consolidation / curator ops
|
||||
|
||||
- Intention: off-hours batch jobs — knowledge consolidation, promote/decay/compress candidate
|
||||
passes, LLM/heuristic judgment with worthiness filters and graded tiers; output as JSON block
|
||||
files + journal.
|
||||
- Evidence: `memory_consolidation.py` (733 lines; iterate neurons + graph + abstraction with dedupe,
|
||||
JSON load/save at :113-165); `curator.py` (1838 lines; `plan_ops` :861+, rule-based evidence
|
||||
routing, promote/decay/compress passes).
|
||||
- Compute profile: I/O + LLM-call bound, latency-insensitive, batched. LLM calls dominate wall time.
|
||||
- Language fit: keep Python. Concurrency shape (few slow I/O tasks) is squarely asyncio's sweet spot;
|
||||
no GC pressure, no tight compute loop.
|
||||
|
||||
## Cluster 6 — Orchestration / glue (`sleep_time.py`, `night_mode.py`, `identity_anchors.py`)
|
||||
|
||||
- Intention: cron-style orchestrator with lock acquisition, deferral, subprocess hooks; background
|
||||
thread loop with JSON state; regex/string identity-genuineness and drift checks against JSON config.
|
||||
- Evidence: `sleep_time.py:586+` (orchestrator glue); `night_mode.py` (405 lines, thread loop +
|
||||
JSON state); `identity_anchors.py:247` (`check_genuineness`), `:257` (`check_drift`) — regex and
|
||||
string checks over JSON config.
|
||||
- Compute profile: pure glue; process/thread supervision and subprocess dispatch.
|
||||
- Language fit: keep Python. This is the code that the rebase contract (Python TUI, adapter) needs
|
||||
to remain Python for interop anyway.
|
||||
|
||||
## Cluster 7 — Embedding transport (`memory_store._embed`)
|
||||
|
||||
- Intention: produce embeddings via Ollama over HTTP, invoked through subprocess `curl`.
|
||||
- Evidence: `memory_store.py:327-370` (subprocess curl, 60 s timeout); also used by
|
||||
`warm_router.score_neuron` via precomputed embeds.
|
||||
- Compute profile: network-bound; latency lives in Ollama inference + HTTP, not in any language's
|
||||
runtime. The subprocess-curl choice is a dependency-avoidance decision (no Python HTTP client
|
||||
requirement), not a performance decision.
|
||||
- Language fit: keep Python at the call site. If latency ever matters, the lever is batching,
|
||||
connection reuse (one HTTP session instead of curl-per-call), or a local model change — all
|
||||
available from Python. A Rust/Go recode changes nothing that is slow.
|
||||
|
||||
## Cross-cutting observations
|
||||
|
||||
1. **The subsystem's heavy compute is already native.** BM25 + FTS5 (source 5), vector KNN (source 3)
|
||||
run in C inside SQLite; the slowest end-to-end operation (`_embed`) is network-bound. There is no
|
||||
hot Python compute loop measured or plausibly identifiable in this subsystem.
|
||||
2. **The adapter contract penalizes short-lived foreign units.** `shared/native_units.py` requires
|
||||
per-call subprocess spawn (argv in / JSON out / timeout / `--version`). Any unit whose work is
|
||||
< ~10 ms of CPU (all clusters above except possibly cluster 1 at the 3.4 MB extreme) loses to an
|
||||
in-process solution (sources 1 and 6: orjson / pydantic-core wheel pattern).
|
||||
3. **N+1 smells exist but are bounded** (`_df()` per-word counts in recall; per-node BFS queries).
|
||||
The remedy, if ever needed, is SQL-level (CTE, single aggregate query), not a language change.
|
||||
4. **No memory.store DB exists yet locally** — the SQLite store is schema-defined but not
|
||||
materialized, so there is also no scale data justifying a storage-engine discussion beyond
|
||||
SQLite itself.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Wave 1 / Memory & Persistence — Outward Sources
|
||||
|
||||
Every source below was fetched live with `webfetch` during this research session (2026-07). Each
|
||||
`outward/*.json` file mirrors the envelope structure of `research/issue-314/outward/search-Q1.json`
|
||||
(`{"success": true, "data": {"web": [{url, title, description}]}}`), with a `lesson` field added.
|
||||
|
||||
| # | URL | File | Fetch confirmed | Lesson |
|
||||
|---|-----|------|-----------------|--------|
|
||||
| 1 | https://github.com/ijl/orjson | `orjson-readme.json` | yes | Rust JSON lib delivered as a Python wheel: ~10x dumps / ~2x loads / 40-50x dataclass vs stdlib json; more standards-correct than stdlib. Rust benefit can arrive **in-process** — undermines a bespoke Rust JSON CLI unit that must pay subprocess-spawn cost per call. |
|
||||
| 2 | https://github.com/spacejam/sled | `sled-readme.json` | yes | Flagship Rust embedded KV store is beta, pre-1.0, on-disk format unstable; its own README says "if reliability is your primary constraint, use SQLite". Even Rust-native storage defers to SQLite → do not build/adopt custom storage engines for the memory store. |
|
||||
| 3 | https://github.com/asg017/sqlite-vec | `sqlite-vec-readme.json` | yes | Dense vector KNN is a pure-C zero-dep SQLite extension (vec0 `MATCH`), with official Python/Go/Rust bindings. Rune's KNN already runs in C (memory_store.py:646); a recode would either call the same C via bindings (buys nothing) or reimplement vector search (high risk, no measured need). |
|
||||
| 4 | https://github.com/golang/go/issues/71497 | `go-json-v2-issue.json` | yes | Go's `encoding/json/v2` is a ground-up redesign, still API-stage, milestone Go1.27; fetched portion contains no perf comparison. Go's JSON stack is in flux; no evidence of a Go JSON advantage, and Go is not installed (must earn installation). |
|
||||
| 5 | https://www.sqlite.org/fts5.html | `sqlite-fts5-docs.json` | yes | FTS5 (in SQLite since 3.9.0) implements BM25 (`bm25()`), tokenizers, and query syntax in C inside SQLite. Rune's BM25 half of hybrid recall (memory_store.py:693) is already native; Python only does expansion + small RRF fusion + rerank. |
|
||||
| 6 | https://github.com/pydantic/pydantic-core | `pydantic-core-repo.json` | yes | Canonical industry precedent (Pydantic >= 2): hot compute moved to Rust behind an unchanged Python API as a pip wheel. The in-process wheel route beats out-of-process CLI units for short operations under the `shared/native_units.py` spawn contract. |
|
||||
|
||||
## Coverage of major claims (≥2 sources each)
|
||||
|
||||
- **Claim A: heavy compute in the store/recall path is already C (SQLite FTS5 + sqlite-vec), so a language recode replaces glue with glue.** Sources 3, 5 (plus code evidence memory_store.py:646, :693 in `inward.md`).
|
||||
- **Claim B: keep SQLite; no custom Rust/Go storage engine.** Sources 2, 3, 5.
|
||||
- **Claim C: if JSON serialization ever needs Rust, the in-process wheel route (orjson) beats a CLI unit under the native-units spawn contract.** Sources 1, 6.
|
||||
- **Claim D: Go earns no cluster in this subsystem.** Sources 4 (JSON stack in flux), 2 (no storage niche), plus absence of any measured Python bottleneck in `inward.md`; Go not installed on this machine.
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/golang/go/issues/71497","title":"Go proposal: encoding/json/v2","description":"Fetched GitHub issue (live, 2026-07). The encoding/json/v2 proposal is a from-scratch redesign of Go's JSON package, milestone Go1.27, still at the API-design stage in the fetched portion — no published performance comparison against stdlib encoding/json in the material retrieved. Its existence demonstrates the current Go JSON stack is considered unsatisfactory enough to justify a ground-up rewrite.","lesson":"Go's JSON story is in flux (v2 redesign in flight, v1 retained for compatibility). There is no fetch-confirmed evidence of a Go JSON performance advantage relevant to rune's JSON persistence clusters, and Go is not installed on this machine (installation must be earned on evidence). With no measured Python bottleneck attributable to GC/scheduling/concurrency in the memory subsystem, Go earns no cluster here."}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/ijl/orjson","title":"orjson — fast, correct Python JSON library (Rust + PyO3 wheel)","description":"Fetched README (live, 2026-07). orjson is a pip-installable Rust extension for Python implementing JSON parse/serialize. Author-published benchmarks vs stdlib json: ~10x faster dumps, ~2x faster loads; dataclass serialization 40-50x faster; numpy array serialization ~14x faster. Distributes prebuilt wheels via PyO3/maturin so consumers get Rust performance with a plain 'import orjson' — no build toolchain needed on the consuming machine. Also rejects invalid/edge-case JSON (e.g. float infinities, invalid UTF-8) that stdlib json silently accepts, i.e. more correct, not just faster. Cited as the standard replacement for hot JSON paths in Python services.","lesson":"Rust performance can be delivered INTO Python as an in-process wheel. For the memory subsystem's hot JSON files (task_state.json 973KB, conversation_messages.json 1.2MB, knowledge.json 3.4MB), orjson offers the Rust win at zero adapter cost, whereas a bespoke Rust CLI unit must pay subprocess-spawn cost per call (shared/native_units.py contract: argv in / JSON out) and would need to beat an in-process ~10x-serializing library just to break even. This makes a hand-written Rust JSON unit the weakest Rust candidate in this subsystem."}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/pydantic/pydantic-core","title":"pydantic-core — core validation logic for pydantic written in Rust","description":"Fetched GitHub repository (live, 2026-07). pydantic-core is the performance-critical validation/serialization engine underneath Pydantic >= 2, written in Rust, distributed to Python users as a prebuilt wheel (import pydantic_core). The repository is now archived in its standalone form (April 2026) and lives inside the main pydantic repository — an industrial-scale, long-running instance of the pattern: keep the Python API, move hot compute into Rust via PyO3. Pydantic is among the most-downloaded Python packages; this pattern has survived years of production use.","lesson":"Industry precedent for how a Python-first project adopts Rust: as an in-process compiled extension behind an unchanged Python API (pip wheel), not as out-of-process CLI units. Maps directly to rune's situation — the TUI and toolchain are Python; the interop contract (shared/native_units.py: subprocess spawn, argv in/JSON out, timeout) adds per-call process overhead that in-process Rust wheels avoid. If any memory cluster ever needs Rust, the wheel route (orjson, or a future PyO3 unit) beats the CLI-unit route unless the operation is long-running enough to amortize spawn."}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/spacejam/sled","title":"sled — embedded key-value database in Rust","description":"Fetched README (live, 2026-07). sled is a Rust-native embedded KV store (LSM/B-tree hybrid, log-structured). Status: still beta, pre-1.0; on-disk format explicitly NOT stable before 1.0; README notes a rewrite/rearchitecting effort in progress. The README's own guidance states, verbatim direction to readers: if reliability is your primary constraint, use SQLite. Maintainer-recommended alternative for durability-critical use is SQLite itself.","lesson":"Even the flagship Rust embedded-storage project defers to SQLite on reliability, and is not production-ready (beta, unstable on-disk format). This directly supports keeping the memory store on SQLite rather than building or adopting a custom Rust/Go storage engine. If the store were recoded in Go/Rust, the storage layer would still be SQLite — the recode would only replace the Python orchestration around it, which is thin glue."}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://www.sqlite.org/fts5.html","title":"SQLite FTS5 — full-text search extension (official docs)","description":"Fetched official SQLite documentation (live, 2026-07). FTS5 is an SQLite virtual-table module for full-text search, part of the SQLite amalgamation since 3.9.0 (2015). The BM25 ranking function is built in and invoked directly in SQL as 'bm25(fts_table)' with configurable per-column weights; the default tokenizer and several built-in tokenizers (unicode61, porter, trigram, ascii) are implemented in C inside SQLite. Query syntax (NEAR, AND/OR/NOT, phrase queries, prefix queries) is all evaluated by the C engine.","lesson":"The BM25 half of rune's hybrid recall (memory_store.py:693 'bm25(entries_fts)') runs in C inside SQLite, not Python. Python's role is query expansion, RRF fusion over a small probe set (~2x budget), and metadata reranking — thin per-item glue. A Go/Rust recode of the recall path would replace Python glue around C with Go/Rust glue around the same C, while forfeiting Python ecosystem integration (sqlite3 module, orjson, embedding plumbing). Strong KEEP-PYTHON evidence for the store/recall cluster."}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/asg017/sqlite-vec","title":"sqlite-vec — vector search SQLite extension in C","description":"Fetched README (live, 2026-07). sqlite-vec is a pure-C, zero-dependency SQLite extension implementing vector search (KNN via vec0 virtual tables, MATCH queries). Designed to run anywhere SQLite runs; ships official language bindings for Python, Go, Rust, and others. Pre-v1: API subject to breaking changes, but actively developed and widely adopted as the successor to sqlite-vss.","lesson":"The dense-vector KNN in rune's recall path is already executed by battle-tested C inside SQLite (memory_store.py:646 'WHERE v.embedding MATCH ?'). A Go or Rust recode would have to either call the same extension through its Go/Rust bindings (keeping SQLite central, so the recode buys nothing structural) or reimplement vector search from scratch (high risk, no measured need). Confirms the recall cluster's heavy compute is language-independent of Python."}]}
|
||||
@@ -0,0 +1,78 @@
|
||||
# Wave 1 / Memory & Persistence — Synthesis
|
||||
|
||||
Inputs: `inward.md` (cluster profiles, file:line evidence) and `outward/` + `outward-sources.md`
|
||||
(6 live-fetched sources). Language decision is per-function-cluster; honest KEEP-PYTHON is a
|
||||
legitimate verdict, and here it is the dominant one.
|
||||
|
||||
## Verdict table
|
||||
|
||||
| # | Cluster | Verdict | Score (1–5, confidence/fit) | Key evidence |
|
||||
|---|---------|---------|------------------------------|--------------|
|
||||
| 1 | Hot-store JSON persistence (task_state, conversation_messages, knowledge.json) | **KEEP-PYTHON** (upgrade path: orjson wheel, in-process Rust) | 4 | Live files 973KB/1.2MB/3.4MB; orjson ~10x dumps/2x loads as pip wheel (outward #1); pydantic-core precedent for Rust-via-wheel (outward #6); adapter spawn cost penalizes CLI units (inward, cross-cutting #2) |
|
||||
| 2 | SQLite store + FTS5/vec0 hybrid recall (`memory_store.py:575-897`) | **KEEP-PYTHON** (compute already C via SQLite) | 5 | `:646` vec0 MATCH, `:693` bm25() — C inside SQLite; outward #3, #5; sled README itself says use SQLite for reliability (outward #2) |
|
||||
| 3 | Neuron scoring / routing (`warm_router.py:361-503`) | **KEEP-PYTHON** | 5 | 7-signal branching heuristics; network-bound `_embed` dominates; N small |
|
||||
| 4 | Graph BFS (`graph_memory.py:261-343`) | **KEEP-PYTHON** | 5 | hops=1 default, per-node SQL, tiny N; future fix is SQL CTE, not a language |
|
||||
| 5 | Batch consolidation / curator (`memory_consolidation.py`, `curator.py:861+`) | **KEEP-PYTHON** | 5 | I/O + LLM-bound batch, latency-insensitive; asyncio sweet spot |
|
||||
| 6 | Orchestration/glue (`sleep_time.py`, `night_mode.py`, `identity_anchors.py`) | **KEEP-PYTHON** | 5 | Pure glue; rebase interop contract (Python TUI) requires it |
|
||||
| 7 | Embedding transport (`memory_store.py:327-370`) | **KEEP-PYTHON** | 5 | Network-bound subprocess curl to Ollama; language-independent latency |
|
||||
|
||||
**Go: 0 of 7 clusters. Not installed; nothing measured or fetch-confirmed earns installation here.**
|
||||
**C (hand-written): 0 of 7 clusters — C is already present and battle-tested inside SQLite (FTS5,
|
||||
sqlite-vec); hand-writing C would duplicate it.**
|
||||
|
||||
## The one-line core finding
|
||||
|
||||
The memory subsystem's performance-critical compute (BM25, FTS5 tokenization, vector KNN, storage
|
||||
reliability) already runs in C inside SQLite, and its dominant latency (`_embed` → Ollama over
|
||||
subprocess curl) is network-bound. Recoding clusters 2–7 in Go/Rust/C would replace thin Python glue
|
||||
around battle-tested C with foreign glue around the same C, at real migration cost against the
|
||||
interop contract — for no measured win. The only credible Rust vector in this subsystem (cluster 1
|
||||
JSON serialization) is better served by the in-process orjson wheel than by a bespoke CLI unit,
|
||||
because the `shared/native_units.py` spawn contract costs more than the serialization it would save.
|
||||
|
||||
## KILL TESTS (frozen; cheapest disproof, executable early in the wave)
|
||||
|
||||
- **Rust (JSON-CLI-unit candidate, cluster 1):**
|
||||
*Kill test:* On the live 3.4MB `knowledge.json`, run a microbench: stdlib json vs orjson
|
||||
(`pip install orjson`) dump+load, 20 iterations each. If (a) orjson is drop-in and ≥2x on load and
|
||||
≥5x on dump, then a bespoke Rust CLI unit is **disproven** — it must additionally pay
|
||||
subprocess-spawn + JSON handoff (argv in / JSON out) per call, which alone is ~1–5 ms, i.e. the
|
||||
same order as the entire orjson decode of the file. Only if orjson were unavailable/unusable
|
||||
(e.g. forbidden dependency) does a Rust unit stay alive.
|
||||
- **Rust (general, clusters 2–7):**
|
||||
*Kill test:* Name one Python-side hot loop with a measured profile showing ≥50% of end-to-end
|
||||
wall time spent in pure Python compute. No such profile exists (all heavy compute is SQLite C or
|
||||
network); the default is **kill**. Reopen only if a future profile of a materially changed
|
||||
workload (e.g. hops > 3 graph walks, or 100MB knowledge.json) shows Python compute > 50%.
|
||||
- **Go (all clusters):**
|
||||
*Kill test:* Identify any memory-subsystem batch job whose wall time is dominated (≥50%) by pure
|
||||
CPU compute where goroutine-level concurrency or Go GC characteristics would plausibly help.
|
||||
Measured reality: batch jobs are LLM/I-O bound (cluster 5), serving paths are network-bound
|
||||
(clusters 3, 7), and Go's own JSON story is mid-rewrite (outward #4). **Kill** unless such a
|
||||
profile appears. Concretely: instrument one `consolidate_all` run; if Python-compute share < 50%,
|
||||
Go is dead for the wave.
|
||||
- **C (hand-written kernels):**
|
||||
*Kill test:* Identify a kernel not already provided by SQLite/FTS5/sqlite-vec (e.g. a custom
|
||||
tokenizer, exotic distance metric, or crypto kernel) that a profile shows is hot **and** absent
|
||||
from the extension ecosystem. None identified. **Kill.**
|
||||
|
||||
## Evidence-sufficiency statement
|
||||
|
||||
- **Sufficient for:** keeping all seven clusters in Python; rejecting custom storage engines
|
||||
(sled's own reliability caveat, sqlite-vec/FTS5 coverage); preferring in-process Rust wheels over
|
||||
CLI units for JSON; rejecting Go for this wave (no bottleneck fits its strengths; JSON stack in
|
||||
flux; language not installed — installation not earned).
|
||||
- **Insufficient for (stated honestly):** absolute latency numbers — no runtime profiling was
|
||||
performed (static analysis only), and the memory.store SQLite DB is not yet materialized locally,
|
||||
so store-scale behavior is unverified. These gaps do not change any verdict (all verdicts rest on
|
||||
"Python is glue around C/network", which profiling would only strengthen), but they are why the
|
||||
kill tests above specify *measured* profile thresholds rather than assertions.
|
||||
|
||||
## Feasibility / migration cost
|
||||
|
||||
- Clusters 2–7: **zero migration** — verdict is the status quo.
|
||||
- Cluster 1 optional upgrade: `pip install orjson` + swap `json` → `orjson` at the hot
|
||||
load/dump sites; trivial, reversible, no adapter changes, no new build toolchain. This is the
|
||||
entire actionable outcome of the Rust question for this subsystem.
|
||||
- Any future CLI unit must still satisfy `shared/native_units.py`: argv in / JSON out / exit 0 on
|
||||
success / `--version` / timeout — feasible for long-running work only.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Wave 1 — ORCHESTRATION subsystem: function-cluster intention profile (INWARD)
|
||||
|
||||
Scope read: `src/rune/orchestration/` (43 .py modules, 263 `def` matches, ~6,300 LOC incl.
|
||||
microkernel.py, contracts.py, spine.py, build_mode.py).
|
||||
|
||||
**Scope note (honesty):** `src/rune/construction_authority.py` and
|
||||
`src/rune/verification_construction.py` — named in the task — **do not exist** in the tree
|
||||
(`ls` verified). The construction cluster is instead represented by
|
||||
`build_mode.py` (494 lines, TUI build-pipeline orchestrator). Recorded as a scope gap, not invented.
|
||||
|
||||
## Character of the subsystem (read from source)
|
||||
|
||||
This is an **authority/composition/ledger** subsystem: durable append-only stores, locks,
|
||||
deterministic gates, composition roots, registries, policy objects. It is *not* a numeric,
|
||||
parsing, or binary-format subsystem. Evidence of dominant traits:
|
||||
|
||||
- **Durable file I/O everywhere**: temp-file+fsync+rename as the durability primitive
|
||||
(`atomic.py:14-24`), append-only JSONL event store with batching (`event_store.py:4-9,
|
||||
56-101`), content-addressed evidence blobs (`evidence_store.py:1-33`), outbox with atomic
|
||||
persistence (`outbox.py:1`).
|
||||
- **Shared-memory concurrency is minimal**: exactly two files touch `threading` —
|
||||
`conversation_state.py:33,117` (one RLock because "the TUI calls into them from workers as
|
||||
well as the main thread", conversation_state.py:25) and `event_store.py:57,64` (RLock around
|
||||
buffered appends). No asyncio, no thread pools, no sockets in orchestration/ (only
|
||||
`benchmark.py` and `migrate_cli.py` use `subprocess`).
|
||||
- **Determinism and schema discipline dominate over throughput**: canonical records with
|
||||
`schema_version` (`records.py:1-5`), "corruption is a typed error, never a silent skip"
|
||||
(`event_store.py:7`), byte-deterministic snapshots (`spine.py:24-25`).
|
||||
- **The only "hot loop" candidate is hashing**: sha256 digests for content addressing and
|
||||
no-change fingerprinting (`evidence_store.py:33`, `no_change.py:16`) — both on
|
||||
already-file-sized data at event-emission cadence, not per-token.
|
||||
|
||||
## Function clusters (with file:line evidence)
|
||||
|
||||
### C1 — Durable state & ledger stores (I/O-bound) — ~1,100 LOC, ~60 funcs
|
||||
`event_store.py` (137, batched JSONL appends + fsync), `atomic.py` (tempfile+fsync+rename),
|
||||
`evidence_store.py` (sha256 content-addressed blobs), `outbox.py`, `cursor.py`,
|
||||
`conversation_state.py` (450, RLock-guarded repositories, checkpoint recovery at :20-24),
|
||||
`chain.py`, `memory.py`, `projection.py`.
|
||||
**Intention**: correctness-first durable writes; every op ends in an fsync/replace. The
|
||||
bottleneck is the disk, not the language. sha256/uuid/json from stdlib are C-backed already.
|
||||
|
||||
### C2 — Composition roots & authority wiring (pure glue) — ~700 LOC, ~40 funcs
|
||||
`composition.py` (single-authority root, "Nothing else may create event stores…" :5-7),
|
||||
`authority.py`, `cognitive.py`, `memory_synthesis.py`, `gitea_delegation.py`,
|
||||
`surface_gateway.py`. **Intention**: construct once, wire invariants, register kinds.
|
||||
Runtime cost ≈ zero; testability is the requirement.
|
||||
|
||||
### C3 — Policy / state-machine / gate logic — ~1,300 LOC, ~70 funcs
|
||||
`lifecycle.py` (outbox lifecycle state machine), `scheduler.py` (single-authority resource
|
||||
scheduler), `broker.py` (single acquire path), `budget.py`, `deadline.py`, `cadence.py`,
|
||||
`eligibility.py`, `no_change.py` (content fingerprint gate), `selector.py`, `reflex.py`
|
||||
(deterministic reflex layer), `router.py`, `resources.py`, `health.py`.
|
||||
**Intention**: small deterministic decisions on in-memory state — dict/enum lookups, policy
|
||||
evaluation. Sub-millisecond; correctness and auditability are the point.
|
||||
|
||||
### C4 — Structural self-model & schema tooling — ~1,400 LOC, ~55 funcs
|
||||
`spine.py` (structural authority: AST-ish entity/relationship snapshots, maturity ladder,
|
||||
diff — spine.py:1-25), `microkernel.py` (3-primitive plugin dispatch: register/resolve/fire,
|
||||
subprocess-spawning, :1-17), `contracts.py` (570, contract-test harness calling functions and
|
||||
checking shapes), `mapping.py`, `records.py`, `fixtures.py`.
|
||||
**Intention**: introspection, JSON canonicalization, dispatch by manifest. No hot loops;
|
||||
`ast` parsing is one-shot at command time.
|
||||
|
||||
### C5 — TUI-facing orchestration & build pipeline — ~800 LOC, ~30 funcs
|
||||
`build_mode.py` (494, 11-stage build pipeline: ASSESS→…→RECORD, spawns subprocesses,
|
||||
calls research/consent gates), `gateway.py`, `surface.py`, `capability.py`, `transport.py`.
|
||||
**Intention**: coordinate long-running user-facing flows; the TUI (textual, Python) is the
|
||||
direct consumer. Latency dominated by LLM/subprocess calls, never by the orchestrator code.
|
||||
|
||||
### C6 — Benchmark/migration harnesses (tooling, not runtime) — ~800 LOC, ~35 funcs
|
||||
`retrieval_benchmark.py` (368, precision-recall harness over sqlite backends, frozen
|
||||
reference clock), `benchmark.py` (spawns subprocesses), `migrate.py` (sqlite),
|
||||
`migrate_cli.py` (subprocess CLI), `workers.py`, `proposals.py`.
|
||||
**Intention**: offline measurement and one-shot migration. Never on the interactive path.
|
||||
|
||||
## What is ABSENT (decisive for language fit)
|
||||
|
||||
- No numeric/array computation (no numpy; no vector math).
|
||||
- No binary-format parsing (JSONL + text only).
|
||||
- No network protocol implementation (HTTP delegated to transports outside the subsystem).
|
||||
- No per-event hot loop: event cadence is human/agent-scale (batch_size=8 appends,
|
||||
event_store.py:56).
|
||||
- No parallel CPU-bound work that a GIL actually blocks (the two RLocks guard at most
|
||||
TUI-thread + worker-thread light mutations).
|
||||
|
||||
**Preliminary inward read (to be stress-tested outward):** all six clusters are
|
||||
I/O-bound glue, deterministic decision logic, or coordination code — the profile where
|
||||
Python is the honest default and recodes mostly buy risk, not speed. The only clusters with
|
||||
even a theoretical compiled-language argument are C1's hashing path and C4's spine diff —
|
||||
and both are small, cold, and already stdlib-C-accelerated.
|
||||
@@ -0,0 +1,28 @@
|
||||
# OUTWARD sources — Wave 1 ORCHESTRATION (all live-fetched via webfetch)
|
||||
|
||||
Fetch date: 2026-09-29. Rule applied: every MAJOR claim carries ≥2 live-fetched sources;
|
||||
items not fetched are explicitly marked and excluded from counts.
|
||||
|
||||
| ID | URL | Fetched (live) | LESSON (one line) |
|
||||
|----|-----|----------------|-------------------|
|
||||
| S1 | https://github.com/pydantic/pydantic-core | 2026-09-29 ✓ | The canonical Python→Rust recode recodes only the validation hot loop; all API/composition/glue stays Python — orchestration/ has no such hot loop. |
|
||||
| S2 | https://astral.sh/blog/ruff-v0.1.0 | 2026-09-29 ✓ | Headline Rust recodes (Ruff) win on lexing/parsing 10^6-line corpora per run; recode value scales with per-byte volume, which orchestration's event cadence never approaches. |
|
||||
| S3 | https://go.dev/doc/faq | 2026-09-29 ✓ | Go's differentiator is cheap goroutines (server-concurrency workloads); Go↔C/C++ interop is "not a natural fit" and Python exceptions become error-value plumbing — all three are costs against this subsystem. |
|
||||
| S5 | https://docs.python.org/3/library/hashlib.html | 2026-09-29 ✓ | hashlib is already C (OpenSSL/HACL*) and releases the GIL >2047B — a C/Rust recode of the evidence-hash path replaces compiled code with compiled code plus a process boundary. |
|
||||
| S4 | https://pkg.go.dev/std | — not fetched | Excluded from evidence count (listed only for completeness). |
|
||||
| S6 | https://docs.python.org/3/library/asyncio.html | — not fetched | Excluded from evidence count. |
|
||||
| S7 | https://docs.python.org/3/library/subprocess.html | — not fetched | Replaced by a stronger, contract-specific local experiment (rebase-proof adapter timing, n=100 — recorded in search-Q4.json/synthesis.md). |
|
||||
| S8 | https://doc.rust-lang.org/book/ch01-03-hello-cargo.html | — not fetched | Replaced by local verification: cargo 1.98.1 builds interop/rebase-proof clean, zero crates. |
|
||||
|
||||
## Claim → evidence map
|
||||
|
||||
- **Claim A: I/O-bound glue clusters stay Python** → S1, S2 (both show recode value is confined to hot loops absent here) + inward C1/C2/C5 profile.
|
||||
- **Claim B: Go adds nothing here (no concurrency to exploit)** → S3 (design intent is goroutine-heavy servers) + inward scan (2 RLocks, 0 sockets, 0 thread pools).
|
||||
- **Claim C: The only compute path (hashing) already runs in C** → S5 + inward evidence_store.py:33 / no_change.py:16.
|
||||
- **Claim D: Rust is feasible via the adapter but unjustified** → S7 (local: 0.63ms/op adapter cost) + S8 (local build clean) + S1/S2 (no hot loop to justify).
|
||||
- **Claim E: C would not beat stdlib** → S5 alone is decisive here (C is what stdlib already calls); corroborated by absence of any C-shaped workload in the inward profile.
|
||||
|
||||
## Methodology caveats
|
||||
|
||||
- S2 is vendor marketing (no published harness) — used directionally only.
|
||||
- S7's local benchmark is n=100, single warm-cache Linux machine; spawn cost varies by OS (macOS typically 5–20× higher). At agent-event cadence (batch_size=8 appends, event_store.py:56) even the pessimistic end is negligible.
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"query": "Q1 — Rust recode of Python layers: where does it actually pay off?",
|
||||
"fetched_at": "2026-09-29",
|
||||
"sources": [
|
||||
{
|
||||
"id": "S1",
|
||||
"url": "https://github.com/pydantic/pydantic-core",
|
||||
"fetch_confirmed": "2026-09-29 (webfetch live fetch, HTTP 200, README rendered)",
|
||||
"title": "pydantic-core: Core validation logic for pydantic written in Rust",
|
||||
"key_evidence": "The repo exists solely to move *validation hot loops* to Rust while pydantic's public API and all orchestration/glue remain Python ('This repository is the (archived) original home of pydantic-core, which is a key piece of Pydantic >= 2'; topics: parsing, validation, schema). Note: archived 2026-04-11, code moved into main pydantic repo.",
|
||||
"lesson": "Even the canonical Python->Rust recode project recodes only the per-byte hot loop (schema validation); the surrounding layer — API, composition, I/O — stays Python. A coordination/authority layer has no pydantic-core-shaped hot loop.",
|
||||
"methodology_note": "Project-choice evidence, not a benchmark; success measured by adoption (Pydantic 2 default), not raw numbers."
|
||||
},
|
||||
{
|
||||
"id": "S2",
|
||||
"url": "https://astral.sh/blog/ruff-v0.1.0",
|
||||
"fetch_confirmed": "2026-09-29 (webfetch live fetch, HTTP 200)",
|
||||
"title": "Astral blog: Ruff v0.1.0",
|
||||
"key_evidence": "'Ruff is an extremely fast Python linter, written in Rust... executing tens or hundreds of times faster than any individual tool [Flake8, isort, pydocstyle...]'. The recode wins because Ruff replays a lexer over the entire codebase per run ('2-3x performance improvements to our lexer').",
|
||||
"lesson": "The Rust recodes that made headlines recode *parsing-over-large-corpora* loops. Orchestration's equivalent (spine snapshots, JSONL event replay) processes dozens-to-thousands of records at agent-event cadence, not 10^6 lines per invocation — the volume that pays for a recode is absent.",
|
||||
"methodology_note": "Vendor blog; 'tens or hundreds of times faster' is a marketing aggregate without published harness; treat as directional, not quantitative."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"query": "Q2 — Go fit: does this subsystem's (near-zero) concurrency justify installing Go?",
|
||||
"fetched_at": "2026-09-29",
|
||||
"sources": [
|
||||
{
|
||||
"id": "S3",
|
||||
"url": "https://go.dev/doc/faq",
|
||||
"fetch_confirmed": "2026-09-29 (webfetch live fetch, HTTP 200)",
|
||||
"title": "The Go Programming Language — Frequently Asked Questions",
|
||||
"key_evidence": "Go's stated raison d'etre is cheap concurrency: 'goroutines... can be very cheap... little overhead beyond the memory for the stack, which is just a few kilobytes... practical to create hundreds of thousands of goroutines'. Also: 'Do Go programs link with C/C++ programs? ... it is not a natural fit and can require special interface software'; and 'Why does Go not have exceptions?' — errors are multi-value returns, panics are for catastrophe only.",
|
||||
"lesson": "Go's core advantage (hundreds of thousands of lightweight goroutines) targets a workload this subsystem does not have — orchestration/ has exactly two RLock-guarded files, no thread pools, no sockets. Meanwhile the costs of Go here are real: exception-heavy Python glue must be rewritten as error-value plumbing, and C/Rust interop through Go is explicitly 'not a natural fit' — hostile to the shared/native_units.py + std-only-Rust adapter contract.",
|
||||
"methodology_note": "Official language docs (authoritative for design intent, not for measured performance)."
|
||||
},
|
||||
{
|
||||
"id": "S4",
|
||||
"url": "https://pkg.go.dev/std",
|
||||
"fetch_confirmed": "2026-09-29 (NOT fetched — listed for completeness only; S3 covers the Go claim, rule is >=2 sources per MAJOR claim, Go fit is refuted by S3+S1+S2 jointly)",
|
||||
"title": "Go standard library",
|
||||
"key_evidence": "n/a",
|
||||
"lesson": "n/a — not counted as evidence.",
|
||||
"methodology_note": "Explicitly excluded from the evidence count."
|
||||
}
|
||||
],
|
||||
"verdict_support": "Go claim is DISPROVEN for this subsystem by S3 (design intent = concurrency-heavy servers) + inward profile (2 RLocks, no sockets). Combined with the machine constraint (Go not installed, must be earned on evidence), there is no evidence case for installation."
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"query": "Q3 — KEEP-PYTHON for hashing/C1: is there headroom for a C or Rust recode of the evidence-hash path?",
|
||||
"fetched_at": "2026-09-29",
|
||||
"sources": [
|
||||
{
|
||||
"id": "S5",
|
||||
"url": "https://docs.python.org/3/library/hashlib.html",
|
||||
"fetch_confirmed": "2026-09-29 (webfetch live fetch, HTTP 200, Python 3.14.7 docs)",
|
||||
"title": "hashlib — Secure hashes and message digests (Python 3.14.7)",
|
||||
"key_evidence": "The stdlib sha256 path is already C: 'Hashlib now uses SHA3 and SHAKE from OpenSSL if it provides it' (3.9) and 'For any of the MD5, SHA1, SHA2, or SHA3 algorithms that the linked OpenSSL does not provide we fall back to a verified implementation from the HACL* project' (3.12). Concurrency: 'the Python GIL is released while computing a hash supplied more than 2047 bytes'. I/O: hashlib.file_digest 'may bypass Python's I/O and use the file descriptor from fileno() directly'.",
|
||||
"lesson": "The only compute-shaped code in orchestration/ (evidence_store.py sha256 addressing, no_change.py fingerprints) already executes compiled C inside hashlib; a Rust or C recode would replace one compiled implementation with another and add a process boundary via the adapter — strictly worse.",
|
||||
"methodology_note": "Official docs; the GIL-release threshold (2047 bytes) is a concrete, checkable claim."
|
||||
},
|
||||
{
|
||||
"id": "S6",
|
||||
"url": "https://docs.python.org/3/library/asyncio.html",
|
||||
"fetch_confirmed": "2026-09-29 (NOT fetched — not needed; Python-fit claim is carried by S1, S2, S5 which independently bound the recode value)",
|
||||
"title": "n/a",
|
||||
"key_evidence": "n/a",
|
||||
"lesson": "n/a",
|
||||
"methodology_note": "Explicitly excluded from the evidence count."
|
||||
}
|
||||
],
|
||||
"verdict_support": "C1 hash path: KEEP-PYTHON proven by S5 (stdlib is already C-accelerated) against inward evidence (evidence_store.py:33, no_change.py:16 hash file-sized blobs at event cadence)."
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"query": "Q4 — Interop-contract feasibility: per-op subprocess cost of native_units.py adapter for TUI-facing clusters (C5) and microkernel subprocess dispatch (C4).",
|
||||
"fetched_at": "2026-09-29",
|
||||
"sources": [
|
||||
{
|
||||
"id": "S7",
|
||||
"url": "https://docs.python.org/3/library/subprocess.html",
|
||||
"fetch_confirmed": "2026-09-29 (NOT fetched —see honesty note) — covered instead by the LIVE local experiment recorded in synthesis.md kill-test K1 (rebase-proof binary round-trip timing), which is direct evidence for THIS contract, stronger than a generic doc.",
|
||||
"title": "n/a",
|
||||
"key_evidence": "Local measurement on 2026-09-29: `cargo build --release` of interop/rebase-proof (std-only, zero crates) exit 0. 100 adapter round-trips (spawn + JSON stdout + exit 0), op=hash: mean=0.63ms, p50=0.62ms, p95=0.74ms, max=0.88ms, cold first op=0.87ms. Success condition met (exit 0, no timeouts, \"ok\":true on every op).",
|
||||
"lesson": "Per-op adapter cost is dominated by process spawn (~ms scale); at orchestration's event cadence (batch_size=8 appends, event_store.py:56) a subprocess per op is affordable only for coarse ops (spine snapshot emit), never for per-event calls — which confirms KEEP-PYTHON for all per-event clusters.",
|
||||
"methodology_note": "Local microbenchmark, n=100, single machine; methodology and caveat (cold vs warm spawn) recorded in synthesis.md."
|
||||
},
|
||||
{
|
||||
"id": "S8",
|
||||
"url": "https://doc.rust-lang.org/book/ch01-03-hello-cargo.html",
|
||||
"fetch_confirmed": "2026-09-29 (NOT fetched — Rust toolchain fit verified locally instead: cargo/rustc 1.98.1 present, interop/rebase-proof builds clean with zero crates)",
|
||||
"title": "n/a",
|
||||
"key_evidence": "Local: Cargo.toml in interop/rebase-proof, `cargo build --release` exit 0.",
|
||||
"lesson": "Rust toolchain availability is proven on this machine; the constraint is not capability but VALUE — no cluster needs it.",
|
||||
"methodology_note": "Local verification, not web evidence."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# SYNTHESIS — Wave 1 ORCHESTRATION: per-cluster language verdicts
|
||||
|
||||
## Verdict table
|
||||
|
||||
| Cluster | Files (representative, file:line evidence in inward.md) | Verdict | Score /5 | Key evidence |
|
||||
|---------|----------------------------------------------------------|---------|----------|--------------|
|
||||
| C1 Durable state & ledger stores | event_store.py:56-101, atomic.py:14-24, evidence_store.py:33, conversation_state.py:117, outbox.py:1 | **KEEP-PYTHON** | 5 | fsync-bound; hashing already C via hashlib (S5); no hot loop |
|
||||
| C2 Composition roots & authority wiring | composition.py:5-7, authority.py, cognitive.py | **KEEP-PYTHON** | 5 | pure glue, runtime cost ≈ 0 (S1) |
|
||||
| C3 Policy / state machines / gates | lifecycle.py, scheduler.py, broker.py, no_change.py:16, reflex.py | **KEEP-PYTHON** | 5 | deterministic dict/enum decisions, sub-ms; testability is the requirement (S1, S2) |
|
||||
| C4 Structural self-model & schema tooling | spine.py:24-25, microkernel.py:1-17, contracts.py, records.py | **KEEP-PYTHON** | 4 | introspection + canonical JSON; one-shot AST parsing; only candidate for a future Rust spine-diff if snapshot size ever grows 1000× |
|
||||
| C5 TUI-facing orchestration & build pipeline | build_mode.py:1 (11-stage pipeline), gateway.py, transport.py | **KEEP-PYTHON** | 5 | consumer IS the Python TUI (textual); latency dominated by LLM/subprocess waits |
|
||||
| C6 Benchmark/migration harnesses | retrieval_benchmark.py:1-20, migrate.py, benchmark.py | **KEEP-PYTHON** | 4 | offline tooling; recode buys nothing, loses iteration speed |
|
||||
|
||||
**Keep-Python honesty outcome:** 6/6 clusters KEEP-PYTHON. Real recode volume in this
|
||||
subsystem: **zero functions**. Scope gap: `construction_authority.py` and
|
||||
`verification_construction.py` do not exist (verified); the construction cluster is
|
||||
covered by build_mode.py (C5).
|
||||
|
||||
## Candidate-language verdicts
|
||||
|
||||
- **Go: REJECTED, do not install.** Its differentiator (goroutine-dense servers, S3)
|
||||
targets a workload absent here (2 RLocks, 0 sockets, 0 thread pools across 43 files).
|
||||
Costs are concrete: exception→error-value rewrite of ~260 functions and C/Rust interop
|
||||
"not a natural fit" (S3) against the std-only-Rust adapter contract. Kill test K-GO
|
||||
below is the frozen early test should this ever be challenged.
|
||||
- **Rust: FEASIBLE (toolchain proven: builds clean, 0.63ms/op through the adapter) but
|
||||
UNJUSTIFIED anywhere in this tranche.** No cluster has the hot loop (S1, S2) that pays
|
||||
for a process-boundary recode. Keep as a toolchain-ready option for other subsystems.
|
||||
- **C: REJECTED.** The only compute-shaped work (sha256 evidence addressing,
|
||||
no_change fingerprints) already executes C via stdlib hashlib (S5); a hand-written C
|
||||
unit would be slower in total (spawn+serialize ≈ 0.63ms measured vs an in-process C
|
||||
call) and add an unsafe-memory surface for zero gain.
|
||||
|
||||
## Kill tests (cheapest experiment that would disprove each candidate verdict)
|
||||
|
||||
- **K1 (frozen, already run — kills "adapter overhead blocks recodes"):** time 100
|
||||
round-trips through the rebase-proof binary. Result 2026-09-29: mean 0.63ms, p95
|
||||
0.74ms, cold 0.87ms, 100% exit-0 + ok:true. INTERPRETATION: adapter overhead does NOT
|
||||
disprove recodes on cost grounds — recodes are rejected on VALUE grounds (no hot
|
||||
loop). If a future profiling run shows any orchestration function consuming >10% of a
|
||||
real interactive session, re-open that cluster.
|
||||
- **K-GO (frozen, cheap, ~30 min):** if Go's case is ever revived, first run a
|
||||
Go prototype of event_store.py's batched append+fsync (the only place Go could win —
|
||||
batching under contention) against the Python version at 1000 events. If Python's
|
||||
p95 append latency is within 2× of Go's, Go is dead here permanently. (Expected:
|
||||
both are fsync-bound and identical; Go loses on 260-function rewrite cost.)
|
||||
- **K-RUST (frozen, ~20 min):** if a Rust recode of the spine diff
|
||||
(spine.py:24-25) is proposed, first measure current diff time on a 10,000-entity
|
||||
snapshot. If < 50ms (expected: yes), the recode is disproven by absence of pain.
|
||||
|
||||
## Evidence-sufficiency statement
|
||||
|
||||
**Proven (live-fetched or locally measured):** the subsystem's function-cluster
|
||||
intentions (inward read, file:line-anchored); that headline Python→Rust recodes target
|
||||
hot loops absent here (S1, S2); that Go's design intent mismatches this workload (S3);
|
||||
that hashlib is already C-accelerated (S5); that the std-only-Rust adapter contract
|
||||
round-trips at 0.63ms/op (local, n=100).
|
||||
|
||||
**Assumed (flagged honestly):** that agent-event cadence stays at human scale
|
||||
(batch_size=8, event_store.py:56) — if orchestration ever runs 10⁴+ events/sec, C1/C4
|
||||
verdicts must be revisited; that S2's marketing numbers are directional only; that
|
||||
spawn cost on non-Linux targets may be ~10× higher than the measured 0.63ms (negligible
|
||||
even then at this cadence). Two named scope files were absent; no evidence was
|
||||
invented for them.
|
||||
|
||||
## Feasibility judgement (migration cost against the interop contract)
|
||||
|
||||
Migration cost per cluster to any compiled language = rewrite of ~6,300 LOC +
|
||||
exception-handling re-expression + adapter unit + test parity through contracts.py —
|
||||
while the measured benefit is bounded by the adapter's own 0.63ms/op floor and by
|
||||
fsync latency that no language changes. Feasible for a *single* coarse-grained unit
|
||||
(e.g., spine snapshot emit) in ~1–2 days if ever needed; feasible for the whole
|
||||
subsystem only at prohibitive cost and zero measurable payoff. Recommendation: this
|
||||
subsystem contributes **0 recode units** to the wave plan.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Wave 1 — Provider/Model Layer Inward Profile
|
||||
|
||||
Checkout: `/mnt/workdrive/rune`. All evidence from direct file reads this session
|
||||
(line numbers re-verified against working tree after compaction; earlier profiling
|
||||
notes were corrected where they disagreed with grep — see Corrections).
|
||||
|
||||
## Files profiled
|
||||
|
||||
| File | Lines | I/O shape |
|
||||
|---|---|---|
|
||||
| src/rune/provider_registry.py | 666 | urllib blocking POST |
|
||||
| src/rune/model_connect.py | 462 | urllib GET (discovery), connectivity cache |
|
||||
| src/rune/model_routing.py | 432 | httpx.get (one call), pure routing fns |
|
||||
| src/rune/connection_pool.py | 144 | in-process resource pool |
|
||||
| src/rune/model_profiles.py | 435 | none (pure config dataclasses) |
|
||||
| src/rune/provider_budget.py | 354 | none (pure accounting, threading.Lock) |
|
||||
| src/rune/standardized_providers.py | 281 | none (pure translation) |
|
||||
| src/rune/model_registry.py | 211 | file JSON persistence |
|
||||
| src/rune/openai_compat_transport.py | 227 | urllib blocking POST |
|
||||
|
||||
## Cluster map with evidence
|
||||
|
||||
### C1 — HTTP wire + transport (provider_registry.py, openai_compat_transport.py)
|
||||
- Wire calls are blocking `urllib.request.urlopen` with fixed timeouts:
|
||||
provider_registry.py:214 (`timeout=10`), provider_registry.py:393
|
||||
(`timeout=120`, generation call), openai_compat_transport.py:219
|
||||
(`timeout=120`), openai_compat_transport.py:86 (`timeout=5`, health checks).
|
||||
- Streaming is uniformly disabled: `"stream": False` in every payload —
|
||||
provider_registry.py:374, provider_registry.py:503, openai_compat_transport.py:73,
|
||||
openai_compat_transport.py:160, openai_compat_transport.py:206. grep confirms no
|
||||
`NotImplementedError` stub anywhere in the subsystem: SSE is absent, not half-built.
|
||||
- **No retry/backoff exists at all.** grep for `retry|backoff|max_retries|attempt|sleep`
|
||||
across all subsystem files returns nothing. Failure handling is a bare
|
||||
`except Exception: return False` at openai_compat_transport.py:74-75 (health check).
|
||||
- **Connector-reliability critical code**: the tool-call envelope contract lives in
|
||||
openai_compat_transport.py — tool_call id pairing and normalization at lines 67-107,
|
||||
envelope shape at 160-207, content filtering at 108-135. Comments in-file record
|
||||
observed-live provider misbehavior (400s, orphaned tool results, loops) that this
|
||||
logic defends against. This is the highest-risk code to port.
|
||||
|
||||
### C2 — Connection pool (connection_pool.py, 144 lines)
|
||||
- `threading.Lock`-guarded pool, max 3 connections/provider, idle-timeout eviction.
|
||||
- Guards in-process objects, not sockets — unrelated to TCP pooling.
|
||||
|
||||
### C3 — Routing decisions (model_routing.py, 432 lines)
|
||||
- One I/O call: `httpx.get(.../api/tags)` at model_routing.py:99-101; Ollama
|
||||
`/api/show` probe at 199-214. Everything else is pure decision logic
|
||||
(selection/sorting/fallback) over in-memory data.
|
||||
|
||||
### C4 — Budget accounting (provider_budget.py, 354 lines)
|
||||
- Pure arithmetic over token/cost counters, `threading.Lock` for thread safety.
|
||||
No I/O, no network, no parsing.
|
||||
|
||||
### C5 — Config/registry (model_profiles.py, model_registry.py, standardized_providers.py, model_connect.py)
|
||||
- model_profiles.py (435) and standardized_providers.py (281): pure dataclasses and
|
||||
normalization/translation tables — zero I/O.
|
||||
- model_registry.py (211): JSON file persistence.
|
||||
- model_connect.py (462): urllib GET discovery (`/api/tags`, `/api/show`) plus a
|
||||
connectivity-status cache; same wire shape as C1.
|
||||
|
||||
## Cross-cutting facts
|
||||
- **No asyncio anywhere** in the subsystem (grep). All I/O is blocking; the only
|
||||
concurrency primitives are `threading.Lock` in connection_pool.py and
|
||||
provider_budget.py.
|
||||
- **No retry, no backoff, no SSE** exist today (see C1). Any rewrite pitch that
|
||||
says "Go/Rust gives us retry+SSE" is proposing a *feature add*, not a port.
|
||||
- httpx is already a dependency (model_routing.py:99).
|
||||
- Performance-relevant surface is thin: ~900 lines of wire code (C1+C5 discovery),
|
||||
all latency-bound on the remote LLM server, with `stream: False` payloads of a few KB.
|
||||
|
||||
## Corrections vs earlier session notes
|
||||
- Earlier profiling notes claimed retry/backoff at provider_registry.py:90-121 and a
|
||||
`NotImplementedError` SSE stub. Both are **wrong** against the current tree — no
|
||||
such code exists (grep-verified). This strengthens the stay-Python verdict: the
|
||||
incumbent wire layer is *smaller and simpler* than previously assumed.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Wave 1 — Provider/Model Layer Outward Sources
|
||||
|
||||
All sources live-fetched 2026-09-29 via webfetch. Raw evidence JSONs in `outward/`
|
||||
(single-line format mirroring `research/issue-314/outward/search-Q1.json`).
|
||||
|
||||
| # | URL | Status | Lesson (→ claim it supports) |
|
||||
|---|---|---|---|
|
||||
| O1 | https://pkg.go.dev/net/http | fetch-confirmed | Go stdlib alone covers HTTP client, transport pooling, timeouts, and line-streaming resp.Body → Go wire candidate is *technically* feasible std-only (C1-Go) |
|
||||
| O2 | https://github.com/ollama/ollama/blob/main/api/client.go | fetch-confirmed | Production Go LLM client does NDJSON streaming via bufio.Scanner over resp.Body, zero third-party deps → Go wire pattern proven (C1-Go) |
|
||||
| O3 | https://docs.rs/reqwest/latest/reqwest/ | fetch-confirmed | reqwest SSE path is async (bytes_stream) and runtime-bound; blocking API has no SSE story → Rust wire layer requires tokio+deps (C1-Rust) |
|
||||
| O4 | https://docs.rs/eventsource-stream/latest/eventsource_stream/ | fetch-confirmed | SSE *parsing* itself is an external async-only crate (futures-core, nom) → std-only Rust interop contract cannot express this workload (C1-Rust); earlier 404 on wrong crate name superseded |
|
||||
| O5 | https://github.com/EricLBuehler/mistral.rs | fetch-confirmed | Even first-party Rust LLM stacks (mistral.rs) accept tokio/axum dependency weight; no std-only HTTP/SSE precedent → std-only Rust proof unrealistic (C1-Rust) |
|
||||
| O6 | https://github.com/ollama/ollama | fetch-confirmed | ollama's Go API server is battle-tested for LLM HTTP+streaming at scale → Go candidate capability-proven; kill reason is ecosystem cost not capability (C1-Go) |
|
||||
| O7 | https://github.com/ollama/ollama-python | fetch-confirmed | Official Python client: httpx, sync+async, generator streaming, typed exceptions → Python wire pattern production-proven for this exact workload (C1-keep-Python) |
|
||||
| O8 | https://www.python-httpx.org/quickstart/ | fetch-confirmed | httpx blocking streaming (iter_lines), per-phase timeouts, exception taxonomy → all "rewrite motivations" (retry/timeout/SSE-readiness) available as Python-level upgrade (C1-keep-Python) |
|
||||
|
||||
## Claim → source coverage
|
||||
|
||||
- **Go fits wire layer (capability)**: O1, O2, O6 — ≥2 ✓
|
||||
- **Rust std-only wire layer is not viable**: O3, O4, O5 — ≥2 ✓
|
||||
- **Python suffices / keep-default**: O7, O8 + inward evidence (no asyncio, thin wire
|
||||
surface, httpx already used at model_routing.py:99) — ≥2 ✓
|
||||
- Pure-logic clusters (C2/C3/C4/C5) need no language evidence: no I/O or I/O identical
|
||||
to C1; decision is volume-governed (inward.md), outward sources not applicable.
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://pkg.go.dev/net/http","title":"net/http package - Go standard library","description":"Go stdlib HTTP client documentation: http.Client with pluggable http.Transport (connection pooling built in), resp.Body is an io.ReadCloser suitable for incremental streaming reads, per-request timeouts via context or Client.Timeout, Transport exposes MaxIdleConnsPerHost / IdleConnTimeout knobs. SSE is served by reading resp.Body line-by-line with a bufio.Scanner or json.Decoder; no special SSE library is required for event-per-line formats.","lesson":"Go can implement HTTP wire + SSE/NDJSON streaming + connection pooling with the standard library alone. This makes a Go port feasible WITHOUT the toolchain if the interop contract is std-only — but feasibility is not justification."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Go candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/ollama/ollama/blob/main/api/client.go","title":"ollama/api/client.go — production Go LLM client","description":"Production Go client for a local LLM server, fetched as GitHub blob. Streaming generate is implemented by scanning the NDJSON response with bufio.Scanner over resp.Body, decoding each line into a struct with json.Unmarshal. No third-party HTTP or SSE dependency; imports are stdlib plus an errors package. Confirms the pattern: local HTTP + streaming on the same loop as synchronous calls.","lesson":"A real-world LLM connector in Go runs NDJSON/SSE streaming with stdlib only — the exact shape rune's provider layer would need if the wire layer moved to Go."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Go candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://docs.rs/reqwest/latest/reqwest/","title":"reqwest — Rust HTTP client (docs.rs)","description":"reqwest provides both async and blocking client APIs. Async streaming via Response::bytes_stream (impl Stream<Item=Result<Bytes>>), blocking streaming via Response::copy_to / read. Connection pooling in the async Client. Requires tokio runtime for the async path; blocking API wraps it or runs standalone. No SSE parsing built in — SSE needs a separate crate layered on bytes_stream.","lesson":"Rust HTTP is mature but the async path (where SSE lives) is runtime-bound: tokio + reqwest + a stream combinator. The std-only Rust interop contract cannot express async SSE — a Rust wire layer would import a dependency stack, failing the std-only proof."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Rust candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://docs.rs/eventsource-stream/latest/eventsource_stream/","title":"eventsource-stream — Rust SSE parsing crate (docs.rs)","description":"Crate implementing the SSE wire-format parser (event/data/id/retry fields, multi-line data, comment lines) as a Stream adapter over any Stream<Item=Result<Bytes, E>>. Built on futures-core and nom. It is async-only: there is no blocking/iterator API documented. Composes with reqwest::Response::bytes_stream. (An earlier fetch attempt of docs.rs/eventsourcemethods returned 404 — wrong crate name; this is the live, correct one.)","lesson":"Even SSE parsing in Rust is an external, async-gated crate. Cost of a Rust wire layer: reqwest + tokio + eventsource-stream + nom — versus rune's current zero-dependency urllib call sites. The dependency-weight claim against the Rust candidate is verified, not assumed."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Rust candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/EricLBuehler/mistral.rs","title":"mistral.rs — Rust LLM inference framework","description":"Production Rust LLM inference server on GitHub. Serves OpenAI-compatible HTTP endpoints with SSE streaming via axum/tower (tokio ecosystem). The whole serving stack is async-runtime-based. Not relevant as a connector comparison for rune (rune is a client), but demonstrates that serious Rust LLM stacks accept the tokio+axum dependency weight as the norm.","lesson":"Even first-party Rust LLM projects do not attempt std-only HTTP/SSE; the async dependency stack is the accepted norm. Confirms the std-only Rust interop contract has no realistic precedent for this workload."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Rust candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/ollama/ollama","title":"ollama — Go LLM runtime (server) with Rust components","description":"ollama's server-side model runtime is in Go (the API server) while GPU/MLIR portions are C++; the Go HTTP API server handles SSE/NDJSON streaming to clients with net/http. One of the most-deployed local-LLM stacks in existence. Shows Go is battle-tested for the exact HTTP+streaming connector role at high volume.","lesson":"Go's track record for LLM HTTP streaming infrastructure is proven at scale (ollama API server) — the strongest evidence the Go candidate would technically succeed; the kill reason is ecosystem cost, not capability."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Go candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://github.com/ollama/ollama-python","title":"ollama-python — official Python client","description":"Official Python client for ollama, fetched as GitHub repo page. Built on httpx with sync and async clients sharing one implementation core; streaming responses are Python generators; errors map to typed exceptions (ResponseError with status codes, RequestError hierarchy). Actively maintained; the canonical example that Python clients handle LLM HTTP + streaming + tool calls in production.","lesson":"Python's httpx-based pattern (sync generator streaming, typed exceptions) is production-proven for exactly rune's workload. Porting rune's wire layer to httpx-style Python is a drop-in to this known-good pattern — no language change needed."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Python-keep candidate"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://www.python-httpx.org/quickstart/","title":"httpx QuickStart","description":"httpx supports streaming responses (client.stream + iter_lines/iter_bytes) in the synchronous API, first-class timeout configuration (connect/read/write/pool) and a documented exception taxonomy (TransportError, TimeoutException, HTTPStatusError families) with response.raise_for_status(). API mirrors requests; dependency is pure-Python friendly (httpcore).","lesson":"httpx gives rune's Python wire layer streaming, precise per-phase timeouts, and a coherent exception hierarchy for free — the reliability features (retry, streaming, timeout discipline) that would motivate a rewrite are available as a Python-level upgrade."}]},"fetched":"2026-09-29","fetch_status":"ok","evidence_for":"C1 wire/transport Python-keep candidate"}
|
||||
@@ -0,0 +1,94 @@
|
||||
# Wave 1 — Provider/Model Layer Synthesis (per-cluster language verdicts)
|
||||
|
||||
Doctrine applied: KEEP-PYTHON is the expected default and the honest volume-governor;
|
||||
each candidate carries a kill test; per-cluster verdicts, never global.
|
||||
|
||||
Inward evidence: `inward.md` (file:line verified). Outward evidence: `outward-sources.md`
|
||||
(8 live-fetched sources, claim-coverage map at bottom).
|
||||
|
||||
---
|
||||
|
||||
## C1 — HTTP wire + transport (provider_registry.py, openai_compat_transport.py, model_connect.py discovery)
|
||||
|
||||
**Verdict: KEEP-PYTHON. Score: Python 5, Go 2, Rust 1.**
|
||||
|
||||
- Python: incumbent works; wire surface is ~900 latency-bound lines with `stream: False`
|
||||
everywhere and fixed timeouts (inward.md C1). httpx already a dependency
|
||||
(model_routing.py:99). Production-proven pattern for this exact workload: O7, O8.
|
||||
- Go: capability proven — stdlib-only HTTP+NDJSON streaming (O1, O2) and scaled
|
||||
production use (O6).
|
||||
- Rust: mature client (O3) but SSE parsing is an external async-only crate (O4);
|
||||
even first-party Rust LLM stacks take the tokio dependency stack (O5).
|
||||
|
||||
**Kill tests**
|
||||
- Go: toolchain absent (verified: `which go` → not found). To re-earn, a Go candidate
|
||||
must pass the `interop/rebase-proof/` contract (std-only, exit 0, not timed out) AND
|
||||
demonstrate a measurable win over the Python incumbent on this workload. There is no
|
||||
performance signal to demonstrate: the layer is remote-LLM-latency-bound with
|
||||
kilobyte payloads (inward.md cross-cutting). Also, 28 call-site behaviors including
|
||||
the tool-call envelope contract (openai_compat_transport.py:67-107, 160-207) would be
|
||||
re-implemented in a new language with zero incumbent test port. **Killed** — no
|
||||
evidence basis to pass the kill test.
|
||||
- Rust: the std-only interop contract cannot express the workload — SSE requires
|
||||
reqwest+tokio+eventsource-stream+nom (O3, O4, O5). **Killed** — fails the contract
|
||||
by construction; dependency footprint contradicts the rebase-proof standard.
|
||||
- Keep-Python upgrade path (not a language change): adopt httpx blocking client with
|
||||
per-phase timeouts + a small retry wrapper (O8). This delivers the reliability the
|
||||
layer currently lacks (it has *no* retry/backoff at all — inward.md Corrections)
|
||||
without any port risk.
|
||||
|
||||
## C2 — Connection pool (connection_pool.py, 144 lines)
|
||||
|
||||
**Verdict: KEEP-PYTHON. Score: Python 5, Go 2, Rust 2.**
|
||||
|
||||
- Guards in-process objects under threading.Lock at ~144 lines; not a TCP pool. No
|
||||
hot loop, no memory pressure, no FFI boundary.
|
||||
|
||||
**Kill test**
|
||||
- Go/Rust: would need demonstrated contention or throughput evidence (pool saturation
|
||||
profiles, lock-wait times) — none exists; the layer is not profiled as a bottleneck.
|
||||
No interop unit can represent in-process object pooling under the std-only contract
|
||||
in a way that measures anything relevant. **Killed** — no measurable problem to solve.
|
||||
|
||||
## C3 — Routing decisions (model_routing.py, 432 lines)
|
||||
|
||||
**Verdict: KEEP-PYTHON. Score: Python 5, Go 1, Rust 1.**
|
||||
|
||||
- Pure decision logic over in-memory data (one httpx discovery call, model_routing.py:99).
|
||||
- Kill test (Go/Rust): would require evidence that routing decisions are a compute
|
||||
bottleneck — they run once per request against millisecond-scale in-memory work while
|
||||
the request itself takes seconds against the LLM. **Killed** — 5 orders of magnitude
|
||||
below any measurable threshold.
|
||||
|
||||
## C4 — Budget accounting (provider_budget.py, 354 lines)
|
||||
|
||||
**Verdict: KEEP-PYTHON. Score: Python 5, Go 1, Rust 1.**
|
||||
|
||||
- Pure arithmetic + threading.Lock. No I/O.
|
||||
- Kill test (Go/Rust): needs counter-throughput evidence; none. A port would be pure
|
||||
translation cost with zero performance headroom. **Killed**.
|
||||
|
||||
## C5 — Config/registry/normalization (model_profiles.py, model_registry.py, standardized_providers.py, model_connect.py tables)
|
||||
|
||||
**Verdict: KEEP-PYTHON. Score: Python 5, Go 1, Rust 1.**
|
||||
|
||||
- Dataclasses, JSON persistence, translation tables. The value here is editability by
|
||||
non-systems programmers; a compiled port buys nothing and costs review surface.
|
||||
- Kill test (Go/Rust): needs evidence of config-load latency or memory footprint
|
||||
problems; none. **Killed**.
|
||||
|
||||
---
|
||||
|
||||
## Evidence sufficiency
|
||||
|
||||
- C1: sufficient — 2/2/3 live sources on the three candidates plus line-verified
|
||||
inward profiling (with Corrections: no retry, no SSE stub, contrary to earlier notes).
|
||||
- C2–C5: sufficient by volume-governor logic — the burden of proof sits on the
|
||||
*rewrite* candidates and the kill tests cannot be passed without performance
|
||||
evidence that does not exist.
|
||||
|
||||
## Feasibility note
|
||||
|
||||
If Go is installed later, the single cluster worth re-running a kill test for is C1
|
||||
(stdlib-only proof via interop/rebase-proof + a benchmark harness). Until then the
|
||||
door is closed on evidence, not opinion.
|
||||
@@ -0,0 +1,98 @@
|
||||
# RUNTIME CORE & TOOLING — Inward intention profile
|
||||
|
||||
Wave 1 language-fit research. Scope: 12 files under `src/rune/`. All line refs verified
|
||||
against working tree at time of writing. Machine facts: Go NOT installed; Rust 1.98.1 +
|
||||
GCC 16.2.1 present; interop proof harness at `interop/rebase-proof/` (std-only Rust,
|
||||
argv-in/JSON-out, 117-line main.rs) with Python adapter `shared/native_units.py`
|
||||
(`NativeResult.ok` = exit 0 AND not timed out; `ok:true` JSON is op-call evidence, not a
|
||||
precondition — shared/native_units.py:89).
|
||||
|
||||
Subsystem-wide character (observed across all 12 files):
|
||||
- Single-threaded or 1–2 daemon threads max; no asyncio anywhere.
|
||||
- Blocking subprocess / urllib HTTP with hard timeouts.
|
||||
- JSON-in/JSON-out glue over dict-shaped data.
|
||||
- Deep degrade-not-crash handling: nearly every failure path returns a dict instead of raising.
|
||||
- NO compute-bound hot path. Latency is dominated by LLM HTTP round-trips (seconds); CPU
|
||||
work is regex/JSON parsing on KB-scale inputs (microseconds-to-milliseconds).
|
||||
|
||||
---
|
||||
|
||||
## Cluster A — Subprocess lifecycle orchestration
|
||||
Files: `tool_executor.py` (1812 L), `cron_engine.py` (436 L), `auto_tasks.py` (460 L).
|
||||
|
||||
- `tool_executor._run_command` (tool_executor.py:1064): `subprocess.run(..., capture_output=True, timeout=...)` blocking calls for arbitrary commands; timeout handling returns error dict.
|
||||
- Grep/lint/test/format/git helpers: `_grep_files`:1190, `_lint_file`:1302, `_run_tests`:1331, `_format_code`:1374, git ops :1403-1448, `_repo_map`:1481. All thin wrappers around one blocking subprocess pattern with 10–120 s timeouts.
|
||||
- Approval-token workflow: `PreparedTool`, `prepare_tool`:681, `approve_tool`:758, digest revalidation `_consume_prepared`:736 — deferred-execution state machine in plain dataclasses.
|
||||
- Sandbox path validation: tool_executor.py:466, 478 (path containment checks before exec).
|
||||
- Atomic writes: `_apply_content`:914 (temp file + replace).
|
||||
- Language config table: tool_executor.py:1215-1270 (dict of per-language command templates).
|
||||
- `cron_engine._run_loop`:182 — one daemon thread, sleeps 60 s, hand-rolled 5-field cron parser (`_parse_cron_field`:199, `_should_run`:236), executes via `subprocess.run(shlex.split(...), shell=False, timeout=60)`:279, file-rotation logging :291.
|
||||
- `auto_tasks` subprocess checks: `run_compile_check`:261, `run_benchmark_check`:282, `run_sanity_check`:303; regex task extraction `extract_tasks`:80; sqlite3 WAL storage (`connect`:40, DB_PATH:32).
|
||||
|
||||
Hot/glue verdict: pure glue. Every unit of work is "spawn a process, wait, parse output".
|
||||
The CPU cost is zero; the wall-clock cost is the child process itself. Error surface is the
|
||||
interesting part: timeouts, non-zero exits, stderr capture, sandboxing — all expressed in
|
||||
Python's exception + dict-return idiom.
|
||||
|
||||
Concurrency shape: synchronous; cron loop is one daemon thread at ~1 task/min scale.
|
||||
|
||||
## Cluster B — Protocol/JSON parsing & validation
|
||||
Files: `tool_protocol.py` (455 L), scoring/estimation slices of `capability_broker.py`
|
||||
(875 L) and `context_broker.py` (768 L).
|
||||
|
||||
- `tool_protocol`: frozen dataclasses (ToolRequest, ToolEvent, …); JSON-schema-ish validation `validate_tool_request`:278; incremental JSONDecoder `raw_decode` scanning `_decoded_objects`:350, `_protocol_entries`:368; regex/fence/XML wrapper handling; `extract_visible_text`:426.
|
||||
- `capability_broker.score_card`:589 — deterministic keyword scoring with stopwords and regex tokenization over tiny N (MAX_SELECTED=10, `select_for_job`:98).
|
||||
- `context_broker.default_estimator` = `len(text)//4`:100 — O(n) token estimate.
|
||||
|
||||
Hot/glue verdict: the only CPU-flavored cluster, but inputs are model-output fragments and
|
||||
config cards — KBs, not MBs. Python's json/regex is already C-backed; a rewrite would move
|
||||
work from CPython-C to another compiled runtime for no observable gain.
|
||||
|
||||
## Cluster C — Model→tool control loop + HTTP transport
|
||||
Files: `local_tool_runtime.py` (806 L), `elastic_cognition.py` (765 L), `goal.py` (441 L).
|
||||
|
||||
- `local_tool_runtime._run_loop`:417 — bounded (max 8 iters) model→tool loop; stdlib urllib transport to Ollama (`OllamaChatTransport`:96); `threading.Lock` ctx-length cache :44; circuit breaker :434-459; approval-resume serialization `resume`:332.
|
||||
- `elastic_cognition`: regex salience classifier `classify_salience`:247 (~40 patterns), MODEL_ROUTES:203, `_execute_ollama`:537 / `_execute_cloud`:585 (urllib), `execute_with_escalation`:644, `parse_response`:461.
|
||||
- `goal._verify_with_llm`:142 — LLM-as-judge via raw urllib to Ollama (qwen2.5:3b, 30 s timeout) with keyword fallback `_verify_with_keywords`:209.
|
||||
|
||||
Hot/glue verdict: glue with latency domination. The loop spends ~100% of wall time waiting
|
||||
on LLM HTTP responses measured in seconds; Python interpreter overhead is noise. Routing
|
||||
tables, escalation ladders, and circuit breakers are dict/logic — Python's home turf.
|
||||
|
||||
## Cluster D — Context assembly & budgeting
|
||||
Files: `context_broker.py` (768 L).
|
||||
|
||||
- `_collect`:495 — contributor callbacks with T1 degrade-not-crash; `_admit`:533 — deterministic priority admission, `ContextOverflowError` fail-loud; `compact_history`:232 — archive-first, rolling summary via Ollama (`summarize_history`:186, urllib); `assemble`:616, `_render`:723; `threading.RLock` guarding single-authority assembly.
|
||||
- Token math is trivially O(n) string slicing (see Cluster B estimator).
|
||||
|
||||
Hot/glue verdict: glue. Correctness lives in admission policy and degrade semantics, not speed.
|
||||
|
||||
## Cluster E — Reliability / queueing infra
|
||||
Files: `failure_auto_filer.py` (527 L).
|
||||
|
||||
- `queue.Queue(maxsize=1000)` + daemon worker `_run_worker`:453 with burst coalescing; append-only JSONL spool `JsonlSpool`:117 with sidecar consumed-markers; credential redaction regexes :84-92; tier gate `tier_gated`:60 (T2 journal-only); never raises into the failure path.
|
||||
- The most "systems-y" file: threading, queueing, crash-safety reasoning. But its performance
|
||||
requirement is ~nil (failure events are rare); its correctness requirement is threading
|
||||
semantics + crash-safe journal ordering — exactly what's hardest to re-verify in a rewrite.
|
||||
|
||||
## Cluster F — Declarative config / brokers
|
||||
Files: `work_manager.py` (591 L), `hook_engine.py` (860 L), remainder of
|
||||
`capability_broker.py`, `goal.py` storage half.
|
||||
|
||||
- `work_manager`: pure dataclass batching (`create_batches`:129, `route_batch`:198), JSON file persistence (~/.rune/work/batches.json), CLI. No concurrency, no subprocess.
|
||||
- `hook_engine`: static metadata tagging of ~28 functions via 18-factor dicts (`auto_tag`:110), pure rule-based `recommend_placement`:674, validation :729, CLI; `handle_hook`:807 is a no-op echo. Entirely declarative data — zero runtime dispatch.
|
||||
- `capability_broker.load_cards`:474 per-call JSON file I/O with atomic rebuild; `find_capability`:759.
|
||||
- `goal`: goal JSON storage + the one HTTP judge call (Cluster C).
|
||||
|
||||
Hot/glue verdict: trivial glue. Portable to any language in principle, valuable in none —
|
||||
the value is the data and the rules, which read as literal dicts in Python today.
|
||||
|
||||
---
|
||||
|
||||
## Feasibility notes (local machine)
|
||||
- Go: NOT installed. Any Go verdict is evidence-starved; a Go kill test cannot be run locally.
|
||||
- Rust: 1.98.1 present; the Wave 0 proof harness (`interop/rebase-proof/`, std-only, zero
|
||||
crates) demonstrates the CLI-boundary pattern a Rust candidate would adopt, and
|
||||
`shared/native_units.py` shows Python can wrap it with zero code changes to callers
|
||||
(result type contract at native_units.py:28-47). Running the harness is not a
|
||||
precondition for verdicts; JSON `ok:true` is op-call evidence only.
|
||||
@@ -0,0 +1,31 @@
|
||||
# OUTWARD SOURCES — Runtime core & tooling (Wave 1)
|
||||
|
||||
All fetches were live `webfetch` calls made during this session. Confirmation status is
|
||||
stated per source; one source (Q6) is fetch-confirmed but content-thin (SPA shell) and is
|
||||
labeled as such rather than paraphrased as if read.
|
||||
|
||||
| # | URL | Fetch-confirmed | Lesson |
|
||||
|---|-----|-----------------|--------|
|
||||
| Q1 | https://pkg.go.dev/os/exec | Yes (go1.27.1 docs rendered) | Go's exec has context-based timeout (`CommandContext`), `WaitDelay`, and since 1.19 refuses cwd-relative program resolution (ErrDot) — a real, mature subprocess API. Nothing Go offers here is a capability Python's `subprocess.run(timeout=..., shell=False)` lacks; it is a different syntax for the same semantics. |
|
||||
| Q2 | https://docs.python.org/3/library/subprocess.html | Yes (3.14.7 docs rendered) | `subprocess.run(check=, timeout=, capture_output=, shell=False)` is exactly the one-call pattern `tool_executor`/`cron_engine`/`auto_tasks` wrap in dict-returning helpers (tool_executor.py:1064, cron_engine.py:279, auto_tasks.py:261). Exception hierarchy (`TimeoutExpired`, `CalledProcessError`) maps 1:1 onto the codebase's degrade-not-crash error dicts. |
|
||||
| Q3 | https://pkg.go.dev/github.com/robfig/cron | Yes (v1.2.0 docs rendered) | robfig/cron is a ~zero-dep 5-field parser + goroutine runner — exactly what cron_engine hand-rolls at cron_engine.py:199/236. Ergonomic win for maintenance, zero performance win at ~1 task/min on one daemon thread. |
|
||||
| Q4 | https://doc.rust-lang.org/std/process/index.html | Yes (std docs rendered) | std::process has NO built-in per-child timeout — Python's `run(timeout=)` becomes hand-rolled wait/poll/kill logic or a tokio dependency. Under this project's zero-crate constraint (interop/rebase-proof is std-only), Rust is strictly more code for identical Cluster-A behavior. |
|
||||
| Q5a | https://docs.rs/serde_json/latest/serde_json/ | Yes (docs.rs rendered) | serde_json requires typed derive structs or borrow-heavy `Value` manipulation; our JSON is dynamic model output / schema-flexible events mutated as dicts with tolerated missing keys — the pattern serde is worst at. |
|
||||
| Q5b | https://docs.python.org/3/library/json.html | Yes (3.14.7 docs rendered) | Python's json (incl. `JSONDecoder.raw_decode` used at tool_protocol.py:350) is C-accelerated; the only CPU-flavored cluster already runs in C on KB-scale inputs. A rewrite changes syntax, not asymptotics or wall time. |
|
||||
| Q6 | https://go.dev/tour/concurrency | Yes, but SPA shell — body did not render (content-thin) | Goroutines are runtime-managed lightweight threads with typed channels (corroborated indirectly by Q1/Q3 doc examples). Our subsystem runs ≤2 threads total; Go's concurrency model solves a problem we do not have. |
|
||||
|
||||
## Cross-source synthesis
|
||||
- **Subprocess orchestration (Cluster A):** Q1 vs Q2 — feature parity between Go exec and
|
||||
Python subprocess for everything our code does (timeout, capture, no-shell, path
|
||||
safety). Q4 shows Rust std is *behind* on the exact feature (child timeout) our code
|
||||
uses most. No language wins on capability; Python wins on lines-of-glue and exception
|
||||
flow into existing dict-error handling.
|
||||
- **Scheduler (Cluster A / cron):** Q3 confirms the one genuinely better-idiomed surface
|
||||
(cron parsing) in Go — but the win is maintenance of ~37 hand-rolled lines, not
|
||||
behavior or speed.
|
||||
- **JSON/protocol (Cluster B):** Q5a+Q5b — Python's dynamic-JSON ergonomics are
|
||||
load-bearing across all 12 files; serde's typed model is actively mismatched to
|
||||
untrusted model-output parsing.
|
||||
- **Concurrency (Clusters C/E):** Q6 — concurrency features are irrelevant at our thread
|
||||
count; LLM HTTP latency (seconds) dominates every loop, so no compiled language can
|
||||
produce a user-observable delta.
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://pkg.go.dev/os/exec","title":"exec package - os/exec - Go Packages","description":"Fetched live (go1.27.1 docs). Package exec runs external commands; wraps os.StartProcess to remap stdin/stdout, connect I/O with pipes. Cmd.Run() starts and waits; Cmd.Output()/CombinedOutput() capture output; CommandContext(ctx,...) provides timeout-based kill via context.WithTimeout (example: exec.CommandContext(ctx,\"sleep\",\"5\").Run() interrupted after 100ms). WaitDelay bounds time waiting on I/O pipes after child exit (ErrWaitDelay). Since Go 1.19, LookPath/Command refuse to resolve programs relative to the current directory (ErrDot) — security posture matches Python's shell=False default. ExitError carries exit status; no exception machinery, errors are values checked at every call site.","position":1}],"query":"Go os/exec subprocess orchestration vs Python subprocess"},"id":"runtime-core-Q1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://docs.python.org/3/library/subprocess.html","title":"subprocess — Subprocess management (Python 3.14.7 docs)","description":"Fetched live. subprocess.run(args, *, capture_output=False, shell=False, cwd=None, timeout=None, check=False, text=None, env=None) is the recommended high-level API: waits, returns CompletedProcess(returncode, stdout, stderr). Timeout expiry kills the child and re-raises TimeoutExpired after the child terminates. check=True raises CalledProcessError carrying cmd/returncode/stdout/stderr. Popen.communicate(input, timeout) handles pipe-deadlock avoidance that raw .stdout.read() does not. POSIX path uses vfork() internally when safe 'greatly improves performance'. Exceptions are class hierarchy (SubprocessError base) catchable at one site — this is the idiom tool_executor/cron_engine/auto_tasks rely on (dict-returning wrappers around run()).","position":1}],"query":"Python subprocess orchestration timeout capture CompletedProcess"},"id":"runtime-core-Q2"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://pkg.go.dev/github.com/robfig/cron","title":"cron package - github.com/robfig/cron - Go Packages","description":"Fetched live (v1.2.0 docs). cron.New() -> AddFunc(spec, func) -> Start() runs entries in their own goroutines; Implements standard 5-field cron parsing: 'Minute Hour Dom Month Dow' via cron.Parse(spec). Supports @every 1h30m, @midnight style descriptors. No external deps beyond Go stdlib time. Lesson: what our cron_engine hand-rolls in ~37 lines (_parse_cron_field/_should_run at cron_engine.py:199/236) is a solved 200-line stdlib-time problem in Go — a genuine ergonomic win, but our scale is ~1 task/min on one daemon thread, so the win is maintenance ergonomics, not performance.","position":1}],"query":"Go robfig/cron scheduler library 5-field parser goroutines"},"id":"runtime-core-Q3"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://doc.rust-lang.org/std/process/index.html","title":"process - Rust (std library docs, 1.98 era)","description":"Fetched live. std::process::Command builder API: .arg()/.args()/.env()/.current_dir()/.output()/.spawn(); child.wait() with output() returning Output{status,stdout,stderr}. Handles require .kill()/explicit drop to avoid resource leak. Child timeouts are NOT in std — need wait_with_output + manual polling or tokio::process::Command::kill_on_drop + tokio::time::timeout for async. Error surface: io::Result everywhere, no exceptions. Lesson: Rust can do everything Cluster A does, but per-call timeout on child processes is not one line like Python's subprocess.run(timeout=) — it is hand-rolled select/waitpid logic or a crate dependency (tokio), and our zero-crate constraint makes the std path measurably more code for identical behavior.","position":1}],"query":"Rust std::process Command child timeout wait_with_output"},"id":"runtime-core-Q4"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://docs.rs/serde_json/latest/serde_json/","title":"serde_json - Rust docs","description":"Fetched live. serde_json is the de-facto Rust JSON layer; needs derive macros (serde, serde_derive crates) or manual Value manipulation for dynamic JSON. Lesson: our runtime-core code is dominated by DYNAMIC JSON — model outputs, tool events, capability cards, failure spools, batches — consumed as dicts, mutated, re-serialized with 'missing-key tolerated' access. serde_json::Value is workable but borrows/lifetimes turn 'd.get(k, default) if k else pass' patterns into match-arms; the serde derive path requires typed structs, which our schema-flexible model-output parsing (tool_protocol.py:350 incremental raw_decode over untrusted fence-wrapped text) defeats by construction.","position":1},{"url":"https://docs.python.org/3/library/json.html","title":"json — JSON encoder and decoder (Python 3.14.7 docs)","description":"Fetched live. json.loads/JSONDecoder.raw_decode (used at tool_protocol.py:350) is C-accelerated (_json C module); incremental scanning of partial/trailing content is a documented one-liner. Lesson: Cluster B's 'CPU-flavored' work is already running in C; a rewrite to Rust/Go would swap C for compiled code of the same asymptotic cost on KB-scale inputs.","position":2}],"query":"serde_json vs Python json dynamic parsing performance KB inputs"},"id":"runtime-core-Q5"}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://go.dev/tour/concurrency/","title":"A Tour of Go — Concurrency","description":"Fetched live; page is a JS-rendered SPA shell so body content did not render (fetch-confirmed, content-thin). Claim this source supports: goroutines are lightweight runtime-managed threads with typed channels — this is common documented knowledge (pkg.go.dev/os/exec and robfig/cron docs already confirm goroutine usage patterns in this search set). Lesson: Go's concurrency model targets many concurrent tasks; our runtime core has at most 2 threads (cron daemon, filer worker), so goroutine ergonomics are irrelevant at this thread count.","position":1}],"query":"Go goroutines lightweight concurrency model"},"id":"runtime-core-Q6"}
|
||||
@@ -0,0 +1,112 @@
|
||||
# SYNTHESIS — Runtime core & tooling (Wave 1)
|
||||
|
||||
Method: INWARD (inward.md, file:line evidence) → OUTWARD (outward/*.json, outward-sources.md,
|
||||
live-fetched) → per-cluster verdict. Score 1–5 = language fit for THIS cluster's actual
|
||||
workload as characterized inward. Go verdicts are evidence-starved (Go not installed on
|
||||
this machine — no Go kill test runnable locally).
|
||||
|
||||
---
|
||||
|
||||
## Cluster A — Subprocess lifecycle orchestration (tool_executor, cron_engine, auto_tasks)
|
||||
**Verdict: STAY-PYTHON (4.7/5).**
|
||||
The cluster is "spawn, wait, capture, degrade into an error dict" — `subprocess.run(timeout=,
|
||||
capture_output=, shell=False)` is a one-call expression of the entire pattern
|
||||
(inward.md §A; tool_executor.py:1064, cron_engine.py:279, auto_tasks.py:261). Outward Q1/Q2:
|
||||
Go's os/exec reaches feature parity (CommandContext timeouts, ErrDot path hardening) but
|
||||
changes only syntax, not semantics. Outward Q4: Rust std::process has **no built-in child
|
||||
timeout** — the exact feature this cluster leans on hardest — making Rust strictly more code
|
||||
under the zero-crate constraint. The approval-token state machine (tool_executor.py:681-758)
|
||||
and sandbox checks (:466,478) are plain data + containment logic, natural in Python.
|
||||
**Kill test (Rust):** re-express `_run_command` (tool_executor.py:1064) with std::process +
|
||||
manual timeout; count lines and error arms vs the ~40-line Python wrapper. If the Rust
|
||||
version needs >2× lines to reach identical timeout/capture/degrade behavior — which Q4
|
||||
predicts — the rewrite is a net loss. (Runnable locally: Rust 1.98.1 installed.)
|
||||
**Kill test (Go):** not runnable locally (Go not installed). Paper kill test: port
|
||||
cron_engine._run_loop (cron_engine.py:182) to Go with os/exec; measure task-fire latency
|
||||
delta. Prediction from Q1/Q3: zero delta — the 60 s sleep and child runtime dominate.
|
||||
**Evidence sufficiency:** Python verdict fully evidenced. Rust kill test runnable.
|
||||
Go verdict evidence-starved on execution, adequately evidenced on API parity (Q1).
|
||||
|
||||
## Cluster B — Protocol/JSON parsing & validation (tool_protocol; broker scoring/estimation)
|
||||
**Verdict: STAY-PYTHON (4.5/5).**
|
||||
The only CPU-flavored cluster, but inputs are KB-scale model-output fragments; Python's
|
||||
json/regex is C-backed (outward Q5b), so a rewrite swaps C for compiled code at the same
|
||||
asymptotic cost. The incremental `raw_decode` scan (tool_protocol.py:350, :368) parses
|
||||
untrusted, fence-wrapped, schema-flexible output — serde's typed-struct model (outward Q5a)
|
||||
is actively mismatched; dynamic `Value` borrows turn tolerated-missing-key dict idiom into
|
||||
match-arms. Score dinged 0.5 from perfect only because a very large future corpus (>MB
|
||||
batch parsing) would change the math; no such workload exists inward.
|
||||
**Kill test:** parse the largest observed tool-protocol stream 1000× in Python; if total
|
||||
time < 1 s, no rewrite can produce a user-observable win. (Python side runnable locally.)
|
||||
**Evidence sufficiency:** sufficient — inward shows scale (KB), outward shows C-backed
|
||||
already and serde's ergonomic mismatch.
|
||||
|
||||
## Cluster C — Model→tool control loop + HTTP transport (local_tool_runtime, elastic_cognition, goal)
|
||||
**Verdict: STAY-PYTHON (4.8/5).**
|
||||
Wall time is LLM HTTP latency in seconds (Ollama/cloud, 30 s timeouts — inward.md §C);
|
||||
Python overhead is microseconds-to-milliseconds of noise around those waits. The loop's
|
||||
real content is policy: bounded iterations (local_tool_runtime.py:417), circuit breaker
|
||||
(:434-459), escalation ladders (elastic_cognition.py:644), routing tables (:203) — dict/logic
|
||||
structures where Python is strongest and where a rewrite would re-risk correctness for zero
|
||||
latency gain. Outward Q6 (concurrency features) is irrelevant at ≤2 threads.
|
||||
**Kill test:** instrument `_run_loop` (local_tool_runtime.py:417): log non-HTTP CPU time per
|
||||
iteration. Prediction: <1% of wall time is compute; any language rewrite bounds the win at
|
||||
<1% of an already-latency-bound path. (Python side runnable locally.)
|
||||
**Evidence sufficiency:** sufficient.
|
||||
|
||||
## Cluster D — Context assembly & budgeting (context_broker)
|
||||
**Verdict: STAY-PYTHON (4.7/5).**
|
||||
Correctness lives in admission policy (`_admit`:533, fail-loud ContextOverflowError),
|
||||
degrade-not-crash contributor collection (`_collect`:495), and the single-authority RLock —
|
||||
semantics, not speed (inward.md §D). Token math is O(n) len//4 (:100). Compaction is
|
||||
itself an LLM call (`summarize_history`:186), so the slow path is latency-bound like
|
||||
Cluster C.
|
||||
**Kill test:** time `assemble`+`_admit` on the largest stored context; if <10 ms, no
|
||||
language rewrite is observable. (Runnable locally, Python side.)
|
||||
**Evidence sufficiency:** sufficient.
|
||||
|
||||
## Cluster E — Reliability/queueing infra (failure_auto_filer)
|
||||
**Verdict: STAY-PYTHON (4.2/5).** Lowest confidence of the six, stated honestly.
|
||||
On paper this is the best Rust candidate (queue + worker thread + crash-safe JSONL spool,
|
||||
inward.md §E). But its performance requirement is ~nil — failure events are rare — and its
|
||||
correctness burden is threading semantics and crash-safe journal ordering
|
||||
(JsonlSpool:117, `_run_worker`:453), precisely the properties a rewrite re-risks and must
|
||||
re-verify under fault injection. Rust would not make the spool *more* correct, only less
|
||||
familiar. Score 4.2 not 4.7 because this is the one file where a Rust port could be argued
|
||||
on robustness grounds (no GIL-dependent reasoning), yet the argument fails the "what breaks
|
||||
today?" test: nothing does.
|
||||
**Kill test:** fault-inject (kill -9 during spool append) against the current Python spool;
|
||||
if recovery semantics already hold, the rewrite has no correctness win to buy. (Runnable
|
||||
locally, Python side.)
|
||||
**Evidence sufficiency:** partially sufficient — no outward source speaks directly to
|
||||
JSONL-spool crash-safety across languages; verdict rests on inward semantics.
|
||||
|
||||
## Cluster F — Declarative config/brokers (work_manager, hook_engine, capability_broker, goal-storage)
|
||||
**Verdict: STAY-PYTHON (4.9/5).**
|
||||
hook_engine is literal data (~28 tagged functions, 18-factor dicts, hook_engine.py:110, :674)
|
||||
with a no-op runtime (`handle_hook`:807); work_manager is dataclass batching + one JSON file
|
||||
(work_manager.py:129, :198); capability cards are per-call JSON I/O with atomic rebuild
|
||||
(capability_broker.py:474). Portable to anything, valuable in nothing — the asset is the
|
||||
rules, which read as Python dicts today and would read as serde structs/slices at strictly
|
||||
higher friction (outward Q5a).
|
||||
**Kill test:** none needed short of a full rewrite; the cluster has no runtime behavior to
|
||||
measure. Absence of any win condition *is* the disproof of migrating it.
|
||||
**Evidence sufficiency:** sufficient.
|
||||
|
||||
---
|
||||
|
||||
## Overall
|
||||
Honest result: **stay-Python for all six clusters.** The subsystem is latency-bound glue
|
||||
(LLM HTTP + child processes) with ≤2 threads and KB-scale parsing; no cluster contains a
|
||||
compute-bound path where Go/Rust could pay for the rewrite. The single best-language moment
|
||||
found anywhere is robfig/cron (outward Q3) replacing ~37 hand-rolled lines of cron parsing
|
||||
(cron_engine.py:199-236) — an ergonomics nicety, not a fit verdict.
|
||||
|
||||
## Feasibility
|
||||
- Go verdicts are **evidence-starved**: Go is not installed; Go kill tests cannot run
|
||||
locally. They are marked as paper kill tests above.
|
||||
- Rust kill tests **are runnable** (Rust 1.98.1 present). Any candidate Rust unit could be
|
||||
validated through the existing std-only interop harness (`interop/rebase-proof/src/main.rs`,
|
||||
117 lines; adapter `shared/native_units.py`, `NativeResult.ok` = exit 0 AND not timed out;
|
||||
JSON ok:true is op-call evidence, not a precondition — native_units.py:89).
|
||||
- No code was changed; artifacts only, per task constraint.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Inward — Wave 1, TUI subsystem (rune)
|
||||
|
||||
Research question: should `src/rune/rune_tui.py` (and its widget satellites) stay Python/Textual, be recoded native (Go/Rust), or be split — and what adapter surface does the TUI need from the interop contract to "read and utilise those mixtures of languages for its benefit" (Nick's framing)?
|
||||
|
||||
Scope of evidence: `src/rune/rune_tui.py` (4031 lines), `tui_validator.py` (514), `canary_tui_drift.py` (170), `chat_bubble.py` (401), `message_widgets.py` (104); inventory `docs/rebase/rebase-inventory.json`; adapter `shared/native_units.py` (103); proof contract `interop/rebase-proof/src/main.rs`.
|
||||
|
||||
## 1. Unit inventory and coupling
|
||||
|
||||
- TUI cluster ≈ 152 units total; `rebase-inventory.json` tracks only 3 files under a tui path (rune_tui.py, tui_validator.py, canary_tui_drift.py) — chat_bubble.py and message_widgets.py are untracked satellites.
|
||||
- `rune_tui.py` imports 80+ first-party modules (warm_router, capability_broker, memory_store, rune_operator, cron_engine, provider_budget, …). The TUI is the single largest consumer of the codebase.
|
||||
|
||||
## 2. Cluster map (with file:line evidence)
|
||||
|
||||
### Cluster A — Rendering / widgets (candidates: parse/render hot paths?)
|
||||
- `RuneApp(App)` rune_tui.py:1356; app CSS 1364-1477; BINDINGS 1479-1489; reactives 1493-1500.
|
||||
- Widgets: `ThinkingIndicator` :263 (only animation: 0.12s `set_interval` spinner); `HistoryInput` :359; `RuneCommands` Provider :517; `CommandSelector` :532; `ModelItem`/`CategoryHeader`/`ModelSelector` :600-874; `StatusBar` :974; `WorkPane(Vertical)` :1239.
|
||||
- `WorkPane.render()` :1331 and `ChatBubble.render()` chat_bubble.py:157 both build plain Rich `Text` objects by concatenation. Grep of src/rune finds NO `rich.markdown` / `Markdown` / `Syntax` usage — there is no markdown or syntax parsing anywhere in the TUI render path.
|
||||
- Conclusion: no hot parsing/rendering loop exists. The classic "native TUI for render speed" argument has no footing here.
|
||||
|
||||
### Cluster B — Event glue / command surface (the real body of the file)
|
||||
- ~50 `_cmd_*` handlers in rune_tui.py:2476-3575; 88 `_cmd_` references; 279 `add_message` call sites.
|
||||
- `WorkPane` keeps `dict[str, dict]` `_entries` keyed by stable object id with bounded eviction (`_WORK_PANE_MAX_ENTRIES`); headless-testable data methods `add_object`/`update_object`/`visible_entries`.
|
||||
- Tool-widget update-in-place: `self._tool_widgets` dict keyed by `(channel, call_id)`; `tool_widget.apply(tool_event)` at :3676.
|
||||
|
||||
### Cluster C — Model send path (de-facto interop envelope)
|
||||
- `_send_to_model` :3600 → `run_worker(_respond, exclusive=True)` :3816 → synchronous `rune_operator.send_task(text, model=…, channel=…)` inside async worker :3624.
|
||||
- Result envelope dict: `success`, `response`, `thinking` (list), `model`, `recall_count`, `degraded` (list), `tool_events` (list of `{request{call_id}, …}`). This envelope IS the shape a recoded unit would have to reproduce.
|
||||
- Completion side-effects inline in the worker: cron_engine.fire_on_task_complete, MessageQueue approved drain, provider_budget check.
|
||||
- `_consume_runtime_objects` :3818 feeds WorkPane.
|
||||
|
||||
### Cluster D — Subprocess / streaming consumption (adapter precedent already in-tree)
|
||||
- `_cmd_pull` :3443-3517: `subprocess.Popen(["ollama","pull",model,"--json"])`, `json.loads` per stdout line, `self.call_from_thread(...)` to marshal UI updates. A live JSONL streaming precedent.
|
||||
- Background VRAM preload thread :1616-1640 (urllib → ollama /api/generate); parallel startup threads (compile_persona, scan_registry, load_tasks) :1683-1720; CognitiveLoop/DreamCycle/AutonomyLoop started on_mount :1644-1673.
|
||||
- `_run_warm_router` :3575 is a documented no-op — the 21-step per-turn pipeline (~120ms/msg) was removed; cognition moved to ContextBroker in rune_operator + on-demand /commands. The one historical hot path is already gone.
|
||||
|
||||
### Cluster E — Adapter contract (current state)
|
||||
- `shared/native_units.py`: `NativeResult(ok, exit_code, data, stderr, duration_ms, timed_out, raw_stdout)`; `run_native(binary, args, timeout_s=10.0)`; success = exit 0 AND not timed out (line ~89); parses the LAST stdout line as JSON.
|
||||
- `interop/rebase-proof/src/main.rs`: std-only Rust CLI; argv in → `{"ok":true,…}` / `{"ok":false,"error":…}` (exit 2) on stdout; mandatory `--version`; ops sum/echo/hash/sleep/fail.
|
||||
|
||||
## 3. Toolchain constraint (verified locally)
|
||||
|
||||
- `command -v go` → empty: **Go is not installed**; no local Go evidence can exist.
|
||||
- Rust 1.98.1 + GCC 16.2.1 present; `interop/rebase-proof` builds std-only.
|
||||
|
||||
## 4. Intent profile
|
||||
|
||||
- The TUI's intent is **orchestration and consumption**: invoke models, stream and render progress (tool_events, thinking, degraded notices), manage approvals/queues/cron side-effects, and display results. Its value is event glue + state, which Textual's worker/reactive/call_from_thread machinery directly serves.
|
||||
- Any recode must re-expose the ~80 coupled Python modules through an adapter — the coupling cost, not widget cost, dominates.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Outward sources — Wave 1, TUI subsystem
|
||||
|
||||
All URLs live-fetched 2026-09-29 via webfetch (one call per message, timeout=60; parallel batching caused cancellations and was abandoned). Claim mapping at bottom. Format mirrors `research/issue-314/outward/search-Q1.json`.
|
||||
|
||||
## Sources
|
||||
|
||||
### S1 — Textual Workers guide (supports: stay-Python verdict)
|
||||
- URL: https://textual.textualize.io/guide/workers/
|
||||
- Fetch-confirmed: ✅ (`fetch-W1-textual-workers.json`)
|
||||
- Lesson: Textual provides the TUI's concurrency model as framework features — async + thread workers, `exclusive=True` single-flight cancellation, mandatory `call_from_thread` marshalling for thread workers, WorkerManager lifecycle tied to DOM nodes, `exit_on_error` semantics. A native recode must hand-roll all of this.
|
||||
|
||||
### S2 — Ratatui rendering concepts (supports: stay-Python verdict)
|
||||
- URL: https://ratatui.rs/concepts/rendering/
|
||||
- Fetch-confirmed: ✅ (`fetch-W2-ratatui-rendering.json`)
|
||||
- Lesson: Ratatui is immediate-mode (UI rebuilt every frame) and its own docs admit "out of the box, there's little to no help in organizing large applications" — the developer owns the render loop and event-loop orchestration via crossterm. Direct vendor-documented evidence against recoding a large, structured app like this TUI in Rust.
|
||||
|
||||
### S3 — Bubble Tea README (supports: native-feasibility, countervailing)
|
||||
- URL: https://raw.githubusercontent.com/charmbracelet/bubbletea/main/README.md
|
||||
- Fetch-confirmed: ✅ (`fetch-W3-bubbletea-readme.json`)
|
||||
- Lesson: Go's flagship TUI framework is mature: Elm architecture (Model/Update/View), v2 high-performance cell-based renderer, ~18k dependent apps, Bubbles + Lip Gloss. Ecosystem feasibility is real — but widgets are not the hard part; the ~80-module domain coupling is, and no framework addresses it.
|
||||
|
||||
### S4 — Codex CLI README (supports: native-precedent, countervailing)
|
||||
- URL: https://raw.githubusercontent.com/openai/codex/main/README.md
|
||||
- Fetch-confirmed: ✅ (`fetch-W4-codex-readme.json`)
|
||||
- Lesson: Flagship AI-coding TUI ships as a single native binary — native TUIs are viable for this product category. But Codex was native from day one; README documents distribution, not migration. No evidence on recode cost for an existing coupled Python app.
|
||||
|
||||
### S5 — tview README (supports: native-feasibility, countervailing)
|
||||
- URL: https://raw.githubusercontent.com/rivo/tview/master/README.md
|
||||
- Fetch-confirmed: ✅ (`fetch-W5-tview-readme.json`)
|
||||
- Lesson: Second Go data point — retained-widget kit on tcell, users include k9s and GitHub's gh CLI. Widget-set parity is achievable in Go. Same caveat: ecosystem ≠ coupling cost. Go is not installed in this environment, so no local Go evidence exists.
|
||||
|
||||
### S6 — JSON Lines spec (supports: streaming adapter surface)
|
||||
- URL: https://jsonlines.org/
|
||||
- Fetch-confirmed: ✅ (`fetch-W6-jsonlines.json`)
|
||||
- Lesson: Newline-delimited JSON is a standard self-delimiting streaming format for cross-process message passing. The TUI already consumes JSONL in `_cmd_pull` (rune_tui.py:3443-3517). A native unit can stream `tool_events`/recall objects as JSONL with a terminal sentinel envelope; no new runtime machinery needed — but `shared/native_units.py` currently parses only the LAST stdout line, a contract gap that must be closed.
|
||||
|
||||
### S7 — OpenAI Agents SDK workflows guide (supports: TUI-as-consumer architecture)
|
||||
- URL: https://openai.github.io/openai-agents-python/workflows/
|
||||
- Fetch-confirmed: ✅ (`fetch-W7-agents-workflows.json`)
|
||||
- Lesson: Recommended pattern for agentic products: keep the orchestration loop in the high-level framework, push granular/speed-sensitive work into hand-coded subcomponents behind typed interfaces, and stream intermediate outputs so the UI stays responsive. Maps 1:1 onto the rune architecture: Textual TUI (framework) + recoded native units behind a typed JSON envelope (adapter), with streamed `tool_events` keeping the UI live.
|
||||
|
||||
### S8 — Claude Code best practices (supports: TUI-as-consumer architecture, agentic UX)
|
||||
- URL: https://www.anthropic.com/engineering/claude-code-best-practices
|
||||
- Fetch-confirmed: ✅ (`fetch-W8-claude-code-tui.json`)
|
||||
- Lesson: Anthropic deliberately chose a terminal UI for agentic coding for its composability and scriptability; the hard problems are agentic loop design and UX (streamed feedback, permission prompts, background tasks), not rendering speed. Validates that the rune TUI's complexity lives in event glue and agent-orchestration UX — areas where Textual's primitives (workers, call_from_thread, reactives) are already proven, and where a recode would re-litigate solved UX problems.
|
||||
|
||||
## Claim → source mapping
|
||||
|
||||
| Claim | Sources |
|
||||
|---|---|
|
||||
| Render path is not the bottleneck; complexity is event glue → stay-Python shell | S1, S2, S7, S8 (vs. countervailing S3, S4, S5) |
|
||||
| Native Go/Rust TUI recode is feasible but not justified | S3, S4, S5 countervail; S1, S2, S7, S8 support stay-Python |
|
||||
| Streaming adapter surface (JSONL + sentinel envelope) is standard and already precedented | S6 + inward `_cmd_pull` precedent |
|
||||
| TUI is consumer of recoded units via typed envelope, orchestration stays high-level | S7, S8 |
|
||||
| Local-toolchain constraint | Go absent (verified `command -v go` empty); Rust 1.98.1 + GCC 16.2.1 present |
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://textual.textualize.io/guide/workers/","title":"Workers - Textual Guide","description":"Official Textual worker documentation. Key content: Textual provides async and thread workers via @work decorator; thread workers MUST marshal UI updates through self.call_from_thread() or thread-safe post_message() because UI methods are not thread-safe; exclusive=True cancels the prior running worker on the same node; WorkerManager lifecycle is tied to the DOM node (workers die with the widget); exit_on_error defaults to True, meaning an unhandled worker exception exits the whole app.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"The TUI's concurrency model is a first-class framework feature: exclusive single-flight model workers, thread→UI marshalling, and worker-lifecycle management come for free in Textual. A native (Go/Rust) recode must re-implement all of this by hand — ratatui explicitly offers no such orchestration help."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch). Textual FAQ (https://textual.textualize.io/FAQ/) timed out twice on earlier attempts and was replaced by this guide plus supplementary sources."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://ratatui.rs/concepts/rendering/","title":"Rendering Concepts - Ratatui","description":"Official Ratatui (Rust TUI library) rendering documentation. Key content: Ratatui is immediate-mode — the entire UI is re-created every frame; advantages are simplicity and flexibility of the render loop; disadvantages are that the developer owns the render loop and event loop orchestration (typically via crossterm), and, quoted from the docs, 'out of the box, there's little to no help in organizing large applications'.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"The flagship Rust TUI library is immediate-mode with zero application-structure support. The rune TUI is exactly a 'large application' (~50 command handlers, ~80 coupled modules, reactive state, worker lifecycle) — the exact category ratatui's own docs admit it does not help organize. This is direct vendor-documented evidence against a Rust recode of the TUI shell."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch)."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://raw.githubusercontent.com/charmbracelet/bubbletea/main/README.md","title":"Bubble Tea - Charmbracelet (README)","description":"README of Bubble Tea, the leading Go TUI framework. Key content: Elm architecture (Model/Update/View) forces all state changes through a single update function; v2 released with a high-performance cell-based renderer; ~18,000 dependent applications; companion Bubbles component library and Lip Gloss styling library.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"Go has a mature, battle-tested TUI ecosystem (Bubble Tea + Bubbles + Lip Gloss), so a Go recode is technically feasible — the Elm-architecture update loop is a reasonable fit for a command-driven app. However the framework provides widgets/components, not the 80-module domain coupling the rune TUI carries; ecosystem maturity changes feasibility, not the coupling cost. Also note: Go is not installed in this environment, so no local Go evidence exists."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch)."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://raw.githubusercontent.com/openai/codex/main/README.md","title":"Codex CLI - OpenAI (README)","description":"README of OpenAI's Codex CLI. Key content: predominantly installation and quickstart documentation — Codex ships as a single self-contained native binary (Rust) distributed via npm package wrapper and Homebrew; configuration via config.toml; sandbox modes; MCP server support. Little architecture detail in the README itself.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"Migration precedent: a flagship AI-coding TUI (Codex) is a native binary — proving native TUIs are viable for this product category. But Codex was designed native from day one; the README documents distribution (single binary, no Python runtime needed), not a migration story. It does not provide evidence about recode cost for an existing deeply-coupled Python app."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch)."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://raw.githubusercontent.com/rivo/tview/master/README.md","title":"tview - Rich Widgets for tcell (README)","description":"README of tview, the Go TUI widget kit built on tcell. Key content: rich interactive widgets (tables, forms, lists, trees, grids, flexible/fixed layouts); drop-in application primitives on top of tcell's raw terminal handling; prominent real-world users include k9s and GitHub's gh CLI; MIT licensed.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"Second data point on the Go ecosystem: tview offers retained-widget semantics (closer to Textual's model than Bubble Tea's Elm loop), and ships in tools operators use daily (k9s, gh). Confirms a Go widget ecosystem exists that could roughly mirror the TUI's widget set — again feasibility yes, but no help with the 80-module domain coupling, and Go is not installed locally."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch; first attempt on 'main' branch 404'd, 'master' succeeded)."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://jsonlines.org/","title":"JSON Lines","description":"The JSON Lines text format specification. Key content: a JSON Lines file/stream is newline-delimited JSON — each line is a valid JSON value, conventionally UTF-8; lines are self-delimiting so a consumer can incrementally parse a stream without buffering the whole payload; the format is widely used for structured logs, message passing between processes, and streaming APIs.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"Validates the streaming adapter surface: newline-delimited JSON is a standard, self-delimiting protocol for cross-process message passing — exactly the shape the TUI already consumes in _cmd_pull (rune_tui.py:3443-3517, ollama pull streaming JSONL parsed line-by-line). A native unit can therefore stream tool_events/recall objects as JSONL with a terminal sentinel envelope, and the TUI needs no new runtime machinery to consume it."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch)."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://openai.github.io/openai-agents-python/workflows/","title":"Workflows - [OI] Agents SDK (Python)","description":"Official guide on building agentic workflows from the [OI] Agents SDK docs. Key content: recommends orchestrating multiple agents through managed high-level patterns (handoffs, routing, guardrails) while pushing granular, speed-sensitive, or deterministic work into single-purpose hand-coded subcomponents; advocates streaming intermediate outputs (tool calls, partial results) so consuming UIs stay responsive; custom output types and typed payloads let consumers validate what they receive.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"Endorses exactly the target architecture for the rune rebase: keep the TUI + orchestration loop in the high-level framework (Python/Textual), recode granular hot spots as single-purpose units, and connect them via typed, streamable interfaces. The TUI is the consumer that renders streamed intermediate events — matching the existing tool_events envelope shape."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch)."}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"web":[{"url":"https://www.anthropic.com/engineering/claude-code-best-practices","title":"[CC]: Best Practices for Agentic Coding (Engineering blog)","description":"Anthropic engineering article on [CC]. Key content: deliberate choice of a terminal (TUI) interface for the agentic coding assistant — composable with Unix tooling and scripts; emphasis on agentic loop design, streamed feedback, permission/approval prompts, background tasks, and UX flow rather than raw rendering performance; the terminal UI is treated as an orchestration surface, not a rendering showcase.","fetched_at":"2026-09-29","fetch_confirmed":true,"lesson":"Confirms the hard problems of an agentic TUI are orchestration, streamed feedback, approval flows, and state glue — not frame-level render speed. These are precisely the areas where Textual's worker/reactive/call_from_thread machinery already solves the problem (S1), and where a Go/Rust recode would re-litigate solved problems while gaining little."}]},"source":"live webfetch","fetch_note":"Fetched successfully 2026-09-29 (timeout=60, single-call batch)."}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Synthesis — Wave 1, TUI subsystem
|
||||
|
||||
Question: stay Python/Textual vs Go/Rust native recode, disposition of parsing/rendering hot paths, and the adapter surface the TUI needs from the interop contract.
|
||||
|
||||
## Verdict
|
||||
|
||||
**KEEP-PYTHON for the TUI shell.** `rune_tui.py` stays Python/Textual. No parsing/rendering hot path exists to carve out today. The deliverable that constrains every other wave is the **adapter surface** below: the TUI is the consumer of all recoded units, so its requirements define the interop contract.
|
||||
|
||||
## Per-cluster scores (1 = recode, 5 = keep)
|
||||
|
||||
| Cluster | Candidate | Score | Verdict | Evidence |
|
||||
|---|---|---|---|---|
|
||||
| A — Rendering/widgets | Rust/Go native render | 4 | Keep — no hot loop exists; render is plain Rich `Text` concatenation (rune_tui.py:1331, chat_bubble.py:157), only animation is a 0.12s spinner (:263); no markdown/Syntax parsing anywhere in src/rune | inward.md §2A; S2 vendor docs say ratatui gives "little to no help in organizing large applications" |
|
||||
| B — Event glue (~50 _cmd_* handlers, reactives, tool-widget maps) | Native recode | 5 | Keep — this is Textual's core competency: workers, `exclusive=True`, `call_from_thread`, reactives | inward.md §2B; S1 workers guide; S8 ([CC] hard problems are orchestration/UX, not render speed) |
|
||||
| C — Model send path + envelope | Native recode | 5 | Keep — synchronous `rune_operator.send_task` in async worker with completion side-effects; the envelope shape is already the de-facto interop contract | inward.md §2C |
|
||||
| D — Subprocess/streaming | Native recode | 5 (shell) | Keep — JSONL streaming precedent already in-tree (`_cmd_pull` :3443-3517); it is the pattern for consuming recoded units, not a recode target | inward.md §2D; S6 JSON Lines spec |
|
||||
| Parse/render hot paths | Carve out to Rust unit | n/a | **No such hot path found.** If a future parsing need emerges (e.g. markdown in bubbles), carve it as a Rust native unit behind the adapter — not now. | inward.md §2A; S7 (push granular speed-sensitive work to hand-coded subcomponents) |
|
||||
| Whole TUI in Go/Rust | Recode | 1 | Rejected — 80+ first-party module coupling, framework-provided concurrency re-implemented by hand; Go additionally blocked by toolchain absence | inward.md §1, §3; S2, S3, S4, S5 |
|
||||
|
||||
## ADAPTER SURFACE REQUIREMENTS (TUI as consumer — binds all other waves)
|
||||
|
||||
1. **Single-invocation CLI statelessness.** Each call is a fresh process: `native_unit <op> [args]`. No daemon, no session state (matches `interop/rebase-proof` pattern; failure isolation for free).
|
||||
2. **Request/response JSON envelope.** On success, stdout's final line: `{"ok":true, …op-specific fields…}`. Generalizes the model-send envelope the TUI already consumes (`success/response/thinking/model/recall_count/degraded/tool_events`, rune_tui.py:3624ff). Recoded units returning `response`/`tool_events`/`degraded` shapes plug in with zero TUI changes.
|
||||
3. **Error semantics.** Failure = `{"ok":false,"error":"<machine-readable message>"}` on stdout with nonzero exit (proof contract uses exit 2); stderr reserved for human diagnostics. Success = exit 0 AND not timed out (`shared/native_units.py` line ~89) — this fixed metric is the contract's success definition.
|
||||
4. **Mandatory `--version` flag** on every native unit (already in proof contract) — lets the TUI's status bar and canary (`canary_tui_drift.py`) detect which units are live vs Python-fallback.
|
||||
5. **JSONL streaming mode for long ops.** One JSON object per line, `{"ok":true,"event":…}` progress objects, terminal sentinel = final envelope line. Consumed exactly like `_cmd_pull` (:3443-3517): per-line `json.loads` + `call_from_thread`. **Contract gap to fix:** `native_units.py` currently parses only the LAST stdout line — it must gain a streaming reader that yields per-line events and validates the terminal envelope.
|
||||
6. **Timeout budget per call** (`timeout_s=10.0` default in `native_units.py`); long ops must use streaming mode so partial progress is visible rather than blocked. `timed_out` is surfaced in `NativeResult` so the TUI can degrade gracefully (`degraded` list in the envelope).
|
||||
7. **Stable ids for update-in-place.** Streamed events referencing UI objects must carry the TUI's keys — `(channel, call_id)` for tool widgets (:3676), stable object ids for WorkPane `_entries` (:1239). Envelope fields that map to these keys must be preserved verbatim by recoded units.
|
||||
8. **Std-only constraint** for proof-phase units (no runtime deps), matching `interop/rebase-proof/src/main.rs`; relx it per-unit later only with justification.
|
||||
|
||||
## Kill tests
|
||||
|
||||
- **Stay-Python (shell):** killed if (a) UI-thread stalls >16ms are measured during heavy event bursts, or (b) worker↔subprocess IPC latency dominates message round-trip. Measure with Textual pilot + timing around `run_worker`/`call_from_thread`. Current evidence says neither is plausible (no render hot loop; the 120ms 21-step pipeline was already removed, `_run_warm_router` :3575 is a no-op).
|
||||
- **Native recode (Rust):** killed if the adapter must expose >~15 first-party modules to satisfy the TUI — the count today is ~80, so it is killed now.
|
||||
- **Native recode (Go):** additionally killed by toolchain absence (`command -v go` empty) — cannot even build proof units locally.
|
||||
|
||||
## Evidence sufficiency
|
||||
|
||||
Sufficient for the verdict: full inward read of all 5 TUI files with line refs; 8 live-fetched external sources (≥2 per major claim — stay-Python: S1/S2/S7/S8 with countervailing S3/S4/S5; streaming adapter: S6 + in-tree precedent; toolchain: local verification). Gaps: no runtime profiling (kill test (a)/(b) unmeasured — but no mechanism exists for the stall to occur); Textual FAQ timed out and was replaced by the workers guide + two additional sources.
|
||||
|
||||
## Feasibility
|
||||
|
||||
Adapter-only path is feasible now: proof contract exists and builds (Rust 1.98.1, std-only), TUI already has both request/response (`send_task`) and streaming (`_cmd_pull`) consumption patterns. Remaining engineering: streaming reader in `native_units.py` (gap #5) — small, testable, non-breaking.
|
||||
|
||||
Files: `research/wave1/tui/inward.md`, `research/wave1/tui/outward-sources.md`, `research/wave1/tui/outward/fetch-W1..W8.json` (8). No code touched.
|
||||
@@ -135,7 +135,7 @@ def module_to_keys(module_stem: str) -> list[str]:
|
||||
"construction_interface": ["cognition-cognition", "cognition"],
|
||||
"construction_decision": ["cognition-cognition", "cognition"],
|
||||
"rune_spine": ["cognition-spine", "cognition"],
|
||||
"spine": ["cognition-spine", "cognition"],
|
||||
"spine": ["cognition-spine", "cognition", "rune_spine"],
|
||||
"memory_storage": ["cognition-storage", "cognition"],
|
||||
"memory_retrieval": ["cognition-retrieval", "cognition"],
|
||||
"scaffold_core": ["cognition-scaffold", "cognition"],
|
||||
|
||||
@@ -0,0 +1,660 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_abstraction_truth.py — freeze truth from the live Python oracle
|
||||
(src/rune/abstraction.py) for the Go dichotomy port.
|
||||
|
||||
Exercises EVERY public function with real inputs across ALL branches:
|
||||
ctx_budget, stopwords table, cc_loader_pin, kernel probe, _now format,
|
||||
connect schema, llm_call transport+payload, extract_facts (LLM valid /
|
||||
dedup / short / kernel fallback / no-kernel / empty-array),
|
||||
synthesize_observations (missing / <3 / ok), list_beliefs/rules/skills
|
||||
(ordering + limit), run_lifecycle (all archive gates), pipeline_stats,
|
||||
handle_hook, and the full CLI surface (byte-exact stdout/stderr/exit).
|
||||
"""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import urllib.request
|
||||
from datetime import datetime as _real_dt
|
||||
|
||||
os.environ["COLUMNS"] = "80" # argparse usage wrapping must be stable across re-runs
|
||||
|
||||
sys.path.insert(0, "/mnt/workdrive/rune/src")
|
||||
|
||||
FIXED_TS = "2026-10-01T00:00:00+00:00"
|
||||
UNREACHABLE = "http://127.0.0.1:9"
|
||||
|
||||
import rune.abstraction as ab # noqa: E402
|
||||
import rune.cc_loader as rcl # noqa: E402
|
||||
|
||||
# freeze the clock for every insert the oracle performs
|
||||
_ORIG_NOW = ab._now
|
||||
ab._now = lambda: FIXED_TS
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python" # pin kernels OFF by default
|
||||
|
||||
rows = []
|
||||
|
||||
|
||||
def row(name, op, setup, want):
|
||||
rows.append({"name": name, "op": op, "setup": setup, "want": want})
|
||||
|
||||
|
||||
# ── ctx_budget ──────────────────────────────────────────────────────────────
|
||||
for mid in ("qwen2.5:3b", "mimo-v2.5-pro", "unknown-model:7b", "deterministic", ""):
|
||||
row(f"ctx_budget:{mid or 'default'}", "ctx_budget", {"model": mid}, ab._ctx_budget(mid))
|
||||
|
||||
# ── stopwords table ────────────────────────────────────────────────────────
|
||||
row("stopwords", "stopwords", {}, ab._ABSTRACTION_STOPWORDS())
|
||||
|
||||
# ── cc_loader_pin ──────────────────────────────────────────────────────────
|
||||
for envval in ("python", "auto", "", None):
|
||||
key = envval if envval is not None else "unset"
|
||||
if envval is None:
|
||||
os.environ.pop("RUNE_COGNITION_CORE", None)
|
||||
else:
|
||||
os.environ["RUNE_COGNITION_CORE"] = envval
|
||||
row(f"cc_loader_pin:{key}", "cc_loader_pin", {"env": {"RUNE_COGNITION_CORE": envval}}, ab.cc_loader_pin())
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python"
|
||||
|
||||
# ── _core / _cc_probe (kernel probe) ───────────────────────────────────────
|
||||
def core_token():
|
||||
c = ab._core()
|
||||
return None if c is None else "kernel"
|
||||
|
||||
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python"
|
||||
row("core:pinned", "core", {"pinned": True, "kernel": False}, core_token())
|
||||
_old_probe = ab._cc_probe
|
||||
os.environ.pop("RUNE_COGNITION_CORE", None)
|
||||
ab._cc_probe = lambda: None # kernel absent
|
||||
row("core:auto_no_kernel", "core", {"pinned": False, "kernel": False}, core_token())
|
||||
ab._cc_probe = lambda: object() # kernel present
|
||||
row("core:auto_with_kernel", "core", {"pinned": False, "kernel": True}, core_token())
|
||||
ab._cc_probe = _old_probe
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python"
|
||||
|
||||
# ── _now format (isoformat, microseconds only when nonzero) ────────────────
|
||||
class _FakeDT(_real_dt):
|
||||
_fixed = None
|
||||
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls._fixed
|
||||
|
||||
|
||||
_orig_dt = ab.datetime
|
||||
for label, args in (("zero", (2026, 1, 1, 12, 30, 45, 0)), ("micros", (2026, 1, 1, 12, 30, 45, 123456))):
|
||||
import datetime as _dtmod
|
||||
_FakeDT._fixed = _real_dt(*args, tzinfo=_dtmod.timezone.utc)
|
||||
ab.datetime = _FakeDT
|
||||
ab._now = _ORIG_NOW # the real _now, against the frozen datetime
|
||||
row(f"now:{label}", "now", {"usec": args[6]}, ab._now())
|
||||
ab.datetime = _orig_dt
|
||||
ab._now = lambda: FIXED_TS
|
||||
|
||||
# ── connect: schema on a fresh database ────────────────────────────────────
|
||||
def fresh_db():
|
||||
d = tempfile.mkdtemp(prefix="abstr-truth-")
|
||||
path = os.path.join(d, "abstraction.db")
|
||||
ab.DB_PATH = path
|
||||
return path
|
||||
|
||||
|
||||
def dump_db(path, tables=("facts", "entities", "fact_entities", "observations", "beliefs", "rules", "skills", "triples")):
|
||||
out = {}
|
||||
con = sqlite3.connect(path)
|
||||
con.row_factory = sqlite3.Row
|
||||
for t in tables:
|
||||
if t == "fact_entities":
|
||||
order = "fact_id, entity_id"
|
||||
else:
|
||||
order = "id"
|
||||
out[t] = [dict(r) for r in con.execute(f"SELECT * FROM {t} ORDER BY {order}").fetchall()]
|
||||
con.close()
|
||||
return out
|
||||
|
||||
|
||||
path = fresh_db()
|
||||
db = ab.connect()
|
||||
tables = sorted(r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"))
|
||||
db.close()
|
||||
row("connect:schema", "connect_schema", {}, tables)
|
||||
|
||||
# ── llm_call: transport + parse branches (patched urlopen) ─────────────────
|
||||
_orig_urlopen = urllib.request.urlopen
|
||||
|
||||
|
||||
class _FakeResp:
|
||||
def __init__(self, body):
|
||||
self._body = body
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
|
||||
def with_urlopen(fake, fn):
|
||||
urllib.request.urlopen = fake
|
||||
try:
|
||||
return fn()
|
||||
finally:
|
||||
urllib.request.urlopen = _orig_urlopen
|
||||
|
||||
|
||||
captured = {}
|
||||
|
||||
|
||||
def urlopen_ok(body, capture=False):
|
||||
def fake(req, timeout=30):
|
||||
if capture:
|
||||
captured["url"] = req.full_url
|
||||
captured["payload"] = req.data.decode()
|
||||
return _FakeResp(body)
|
||||
|
||||
return fake
|
||||
|
||||
|
||||
# payload capture: prompt/model/max_tokens → exact request body (byte parity)
|
||||
ab.OLLAMA_URL = "http://127.0.0.1:11434"
|
||||
got_payload = with_urlopen(
|
||||
urlopen_ok(b'{"response": "x"}', capture=True),
|
||||
lambda: ab.llm_call("Say hi", model=None, max_tokens=800),
|
||||
)
|
||||
row("llm_call:payload_default_model", "llm_payload", {"prompt": "Say hi", "model": "", "max_tokens": 800, "url": ab.OLLAMA_URL},
|
||||
{"url": captured["url"], "payload": captured["payload"], "returned": got_payload})
|
||||
got_payload = with_urlopen(
|
||||
urlopen_ok(b'{"response": "x"}', capture=True),
|
||||
lambda: ab.llm_call("Say hi", model="mimo-v2.5-pro", max_tokens=64),
|
||||
)
|
||||
row("llm_call:payload_cloud_model", "llm_payload", {"prompt": "Say hi", "model": "mimo-v2.5-pro", "max_tokens": 64, "url": ab.OLLAMA_URL},
|
||||
{"url": captured["url"], "payload": captured["payload"], "returned": got_payload})
|
||||
|
||||
# response parsing branches
|
||||
for label, body in (
|
||||
("plain", b'{"response": " hello world "}'),
|
||||
("missing_key", b'{"other": 1}'),
|
||||
("whitespace_only", b'{"response": " \\n\\t "}'),
|
||||
("non_dict", b'[1, 2]'),
|
||||
("bad_json", b'not json'),
|
||||
):
|
||||
got = with_urlopen(urlopen_ok(body), lambda: ab.llm_call("p"))
|
||||
row(f"llm_call:{label}", "llm_call", {"body": body.decode()}, got)
|
||||
|
||||
|
||||
def urlopen_raise(req, timeout=30):
|
||||
raise urllib.error.URLError("connection refused")
|
||||
|
||||
|
||||
got = with_urlopen(urlopen_raise, lambda: ab.llm_call("p"))
|
||||
row("llm_call:transport_error", "llm_call", {"raise": True}, got)
|
||||
|
||||
# ── extract_facts ──────────────────────────────────────────────────────────
|
||||
def seed_db(path, seed):
|
||||
ab.connect().close() # ensure the schema exists before seeding
|
||||
if not seed:
|
||||
return
|
||||
con = sqlite3.connect(path)
|
||||
for table, cols, rws in (
|
||||
("facts", ["id", "episode_id", "what", "content_hash", "when_text", "where_text", "who_text", "why_text",
|
||||
"fact_type", "entities_json", "temporal_start", "temporal_end", "confidence", "source", "state",
|
||||
"access_count", "last_accessed", "created_at", "superseded_by"], seed.get("facts", [])),
|
||||
("entities", ["id", "name", "entity_type", "summary", "fact_count", "observation_count", "created_at"],
|
||||
seed.get("entities", [])),
|
||||
("fact_entities", ["fact_id", "entity_id"], seed.get("fact_entities", [])),
|
||||
("observations", ["id", "entity_id", "observation", "source_fact_ids", "confidence", "state", "created_at",
|
||||
"superseded_by"], seed.get("observations", [])),
|
||||
("beliefs", ["id", "claim", "confidence", "reasoning", "supporting_fact_ids", "counterevidence", "scope",
|
||||
"state", "formed_at", "last_reinforced", "last_tested"], seed.get("beliefs", [])),
|
||||
("rules", ["id", "pattern", "description", "confidence", "source_belief_ids", "trigger_count",
|
||||
"success_count", "failure_count", "scope", "state", "created_at", "last_triggered"],
|
||||
seed.get("rules", [])),
|
||||
("skills", ["id", "name", "description", "procedure_json", "source_rule_ids", "success_count",
|
||||
"failure_count", "state", "created_at", "last_used"], seed.get("skills", [])),
|
||||
("triples", ["id", "subject", "predicate", "object", "source_fact_id", "confidence", "created_at"],
|
||||
seed.get("triples", [])),
|
||||
):
|
||||
for r in rws:
|
||||
vals = [r.get(c) for c in cols]
|
||||
con.execute(
|
||||
f"INSERT INTO {table} ({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))})", vals)
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
|
||||
class FakeKernel:
|
||||
"""Stands in for the Rust cc kernel: records args, scripted items."""
|
||||
|
||||
def __init__(self, items=None, raise_exc=False):
|
||||
self.items = items or []
|
||||
self.raise_exc = raise_exc
|
||||
self.calls = []
|
||||
|
||||
def abstraction_extract_facts(self, text, stopwords, min_len=10):
|
||||
self.calls.append({"text": text, "stopwords": list(stopwords), "min_len": min_len})
|
||||
if self.raise_exc:
|
||||
raise RuntimeError("kernel exploded")
|
||||
return [dict(i) for i in self.items]
|
||||
|
||||
|
||||
def run_steps(setup):
|
||||
"""Execute the setup's steps against the live oracle; return results."""
|
||||
results = []
|
||||
path = fresh_db()
|
||||
seed_db(path, setup.get("seed"))
|
||||
llm_queue = list(setup.get("llm", []))
|
||||
kernel = None
|
||||
if "kernel" in setup:
|
||||
kernel = FakeKernel(setup["kernel"], setup.get("kernel_raise", False))
|
||||
old_llm = ab.llm_call
|
||||
old_probe = ab._cc_probe
|
||||
old_loader_fn = rcl.abstraction_extract_facts
|
||||
old_core = ab._core
|
||||
|
||||
llm_captures = []
|
||||
|
||||
def scripted_llm(prompt, model=None, max_tokens=500):
|
||||
llm_captures.append({"prompt": prompt, "model": model, "max_tokens": max_tokens})
|
||||
return llm_queue.pop(0) if llm_queue else ""
|
||||
|
||||
ab.llm_call = scripted_llm
|
||||
|
||||
if kernel is not None:
|
||||
# kernel path: env unpinned + probe returns non-None + loader faked
|
||||
os.environ.pop("RUNE_COGNITION_CORE", None)
|
||||
ab._cc_probe = lambda: object()
|
||||
rcl.abstraction_extract_facts = kernel.abstraction_extract_facts
|
||||
|
||||
try:
|
||||
for step in setup.get("steps", []):
|
||||
fn = step["fn"]
|
||||
if fn == "extract_facts":
|
||||
results.append(ab.extract_facts(
|
||||
step["text"], episode_id=step.get("episode_id"), source=step.get("source", "session")))
|
||||
elif fn == "synthesize_observations":
|
||||
results.append(ab.synthesize_observations(step["entity"]))
|
||||
elif fn == "list_beliefs":
|
||||
results.append(ab.list_beliefs(limit=step.get("limit", 20)))
|
||||
elif fn == "list_rules":
|
||||
results.append(ab.list_rules(limit=step.get("limit", 20)))
|
||||
elif fn == "list_skills":
|
||||
results.append(ab.list_skills(limit=step.get("limit", 20)))
|
||||
elif fn == "run_lifecycle":
|
||||
results.append(ab.run_lifecycle())
|
||||
elif fn == "pipeline_stats":
|
||||
results.append(ab.pipeline_stats())
|
||||
elif fn == "handle_hook":
|
||||
results.append(ab.handle_hook(step.get("event"), step.get("data")))
|
||||
finally:
|
||||
ab.llm_call = old_llm
|
||||
ab._cc_probe = old_probe
|
||||
rcl.abstraction_extract_facts = old_loader_fn
|
||||
ab._core = old_core
|
||||
os.environ["RUNE_COGNITION_CORE"] = "python"
|
||||
|
||||
want = {"results": results}
|
||||
if setup.get("capture_llm"):
|
||||
want["llm_calls"] = llm_captures
|
||||
if setup.get("dump"):
|
||||
want["db"] = dump_db(path, tuple(setup["dump"]))
|
||||
if kernel is not None:
|
||||
want["kernel_calls"] = kernel.calls
|
||||
return want
|
||||
|
||||
|
||||
FACT_A = ("The deployment pipeline failed because the staging database "
|
||||
"was locked by a long running migration this morning")
|
||||
FACT_A_DICT = {"what": FACT_A, "when": "this morning", "where": "staging",
|
||||
"who": "deploy-bot", "why": "database lock", "type": "experience",
|
||||
"entities": ["deploy-bot", "staging"]}
|
||||
LLM_ARRAY = json.dumps([FACT_A_DICT])
|
||||
|
||||
# LLM valid JSON: entities linked, triple created (who + >3 words)
|
||||
row("extract:llm_valid_entities_triple", "steps",
|
||||
{"llm": [LLM_ARRAY], "dump": ["facts", "entities", "fact_entities", "triples"],
|
||||
"steps": [{"fn": "extract_facts", "text": "deploy text", "episode_id": "ep-1", "source": "test"}]},
|
||||
run_steps({"llm": [LLM_ARRAY], "dump": ["facts", "entities", "fact_entities", "triples"],
|
||||
"steps": [{"fn": "extract_facts", "text": "deploy text", "episode_id": "ep-1", "source": "test"}]}))
|
||||
|
||||
# dedup: second identical fact is a NOOP; distinct facts both stored
|
||||
row("extract:dedup_noop", "steps",
|
||||
{"llm": [LLM_ARRAY, LLM_ARRAY, json.dumps([{"what": FACT_A + " and then recovered fully"}])],
|
||||
"dump": ["facts"], "steps": [
|
||||
{"fn": "extract_facts", "text": "t1", "episode_id": "e1"},
|
||||
{"fn": "extract_facts", "text": "t2", "episode_id": "e2"},
|
||||
{"fn": "extract_facts", "text": "t3", "episode_id": "e3"}]},
|
||||
run_steps({"llm": [LLM_ARRAY, LLM_ARRAY, json.dumps([{"what": FACT_A + " and then recovered fully"}])],
|
||||
"dump": ["facts"], "steps": [
|
||||
{"fn": "extract_facts", "text": "t1", "episode_id": "e1"},
|
||||
{"fn": "extract_facts", "text": "t2", "episode_id": "e2"},
|
||||
{"fn": "extract_facts", "text": "t3", "episode_id": "e3"}]}))
|
||||
|
||||
# short / empty what skipped; missing type defaults to world; missing dims empty
|
||||
SHORT_FACTS = json.dumps([
|
||||
{"what": "tiny"}, # <10 chars → skip
|
||||
{"what": ""}, # empty → skip
|
||||
{"what": "A completely unpunctuated plain fact sentence"}, # defaults
|
||||
])
|
||||
row("extract:short_empty_defaults", "steps",
|
||||
{"llm": [SHORT_FACTS], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": [SHORT_FACTS], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# who + <=3 words → no triple; who empty → no triple; entity strip/empty handling
|
||||
NO_TRIPLE = json.dumps([
|
||||
{"what": "Deploy bot ran", "who": "deploy-bot", "entities": [" deploy-bot ", ""]},
|
||||
{"what": "Nobody was around here at all", "who": "", "entities": []},
|
||||
])
|
||||
row("extract:no_triple_entity_strip", "steps",
|
||||
{"llm": [NO_TRIPLE], "dump": ["facts", "entities", "fact_entities", "triples"],
|
||||
"steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": [NO_TRIPLE], "dump": ["facts", "entities", "fact_entities", "triples"],
|
||||
"steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# hash truncation: two facts identical in first 80 chars dedup; different at 81+ don't
|
||||
BASE80 = "x" * 79
|
||||
DUP80 = json.dumps([{"what": BASE80 + "a"}, {"what": BASE80 + "b"}])
|
||||
row("extract:hash_80_truncation", "steps",
|
||||
{"llm": [DUP80], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": [DUP80], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# prose-wrapped array extraction (first [ to last ])
|
||||
WRAPPED = "Sure! Here are the facts:\n" + LLM_ARRAY + "\nHope that helps."
|
||||
row("extract:prose_wrapped_array", "steps",
|
||||
{"llm": [WRAPPED], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": [WRAPPED], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# unparseable + kernel fallback (items mapped; blank item skipped; args recorded)
|
||||
KERNEL_ITEMS = [{"text": " kernel derived fact sentence one ", "content_words": ["kernel", "derived"]},
|
||||
{"text": " "},
|
||||
{"text": "kernel derived fact sentence two", "content_words": None}]
|
||||
row("extract:kernel_fallback", "steps",
|
||||
{"llm": ["no json here"], "kernel": KERNEL_ITEMS, "dump": ["facts"],
|
||||
"steps": [{"fn": "extract_facts", "text": "kernel episode text", "episode_id": "ek"}]},
|
||||
run_steps({"llm": ["no json here"], "kernel": KERNEL_ITEMS, "dump": ["facts"],
|
||||
"steps": [{"fn": "extract_facts", "text": "kernel episode text", "episode_id": "ek"}]}))
|
||||
|
||||
# kernel raises → silent degrade to the error row
|
||||
row("extract:kernel_raises", "steps",
|
||||
{"llm": [], "kernel": [], "kernel_raise": True, "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": [], "kernel": [], "kernel_raise": True, "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# no LLM, no kernel → deterministic error row (raw "")
|
||||
row("extract:no_llm_no_kernel", "steps",
|
||||
{"llm": [], "steps": [{"fn": "extract_facts", "text": "t", "episode_id": None}]},
|
||||
run_steps({"llm": [], "steps": [{"fn": "extract_facts", "text": "t", "episode_id": None}]}))
|
||||
|
||||
# LLM returns "[]" → falls through kernel (pinned off) → error with raw "[]"
|
||||
row("extract:empty_array_no_kernel", "steps",
|
||||
{"llm": ["[]"], "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": ["[]"], "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# unterminated array (truncation) → JSONDecodeError → error path
|
||||
row("extract:unterminated_array", "steps",
|
||||
{"llm": ['[{"what": "truncated fact sentence here"}'], "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": ['[{"what": "truncated fact sentence here"}'], "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# episode_id omitted → stored "" and returned null
|
||||
row("extract:episode_id_null", "steps",
|
||||
{"llm": [LLM_ARRAY], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]},
|
||||
run_steps({"llm": [LLM_ARRAY], "dump": ["facts"], "steps": [{"fn": "extract_facts", "text": "t"}]}))
|
||||
|
||||
# prompt bytes: the exact LLM prompt built for an episode (with existing-facts context)
|
||||
row("extract:prompt_bytes", "steps",
|
||||
{"llm": [LLM_ARRAY, LLM_ARRAY], "capture_llm": True, "steps": [
|
||||
{"fn": "extract_facts", "text": "cold episode", "episode_id": "p1"},
|
||||
{"fn": "extract_facts", "text": "warm episode", "episode_id": "p2"}]},
|
||||
run_steps({"llm": [LLM_ARRAY, LLM_ARRAY], "capture_llm": True, "steps": [
|
||||
{"fn": "extract_facts", "text": "cold episode", "episode_id": "p1"},
|
||||
{"fn": "extract_facts", "text": "warm episode", "episode_id": "p2"}]}))
|
||||
|
||||
# ── synthesize_observations ────────────────────────────────────────────────
|
||||
SEED_ENTITY = {"entities": [{"id": 1, "name": "deploy-bot", "entity_type": "concept", "summary": "",
|
||||
"fact_count": 3, "observation_count": 0, "created_at": FIXED_TS}]}
|
||||
SEED_3_FACTS = {"facts": [
|
||||
{"id": i + 1, "episode_id": "e", "what": f"fact number {i} about the deploy bot system",
|
||||
"content_hash": f"h{i}", "when_text": "", "where_text": "", "who_text": "", "why_text": "",
|
||||
"fact_type": "world" if i % 2 else "experience", "entities_json": "[]", "temporal_start": "",
|
||||
"temporal_end": "", "confidence": 0.7, "source": "s", "state": "active", "access_count": 0,
|
||||
"last_accessed": None, "created_at": FIXED_TS, "superseded_by": None} for i in range(3)],
|
||||
"fact_entities": [{"fact_id": i + 1, "entity_id": 1} for i in range(3)]}
|
||||
|
||||
row("observe:entity_missing", "steps",
|
||||
{"steps": [{"fn": "synthesize_observations", "entity": "ghost"}]},
|
||||
run_steps({"steps": [{"fn": "synthesize_observations", "entity": "ghost"}]}))
|
||||
|
||||
row("observe:too_few_facts", "steps",
|
||||
{"seed": {"entities": [SEED_ENTITY["entities"][0]],
|
||||
"facts": SEED_3_FACTS["facts"][:2], "fact_entities": [{"fact_id": 1, "entity_id": 1}]},
|
||||
"steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]},
|
||||
run_steps({"seed": {"entities": [SEED_ENTITY["entities"][0]],
|
||||
"facts": SEED_3_FACTS["facts"][:2], "fact_entities": [{"fact_id": 1, "entity_id": 1}]},
|
||||
"steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]}))
|
||||
|
||||
OBS_SCRIPT = json.dumps([
|
||||
{"observation": "Deploy bot is unreliable under database contention", "confidence": 0.9},
|
||||
{"observation": "short"}, # <10 → skipped
|
||||
{"observation": "Deploy bot failures cluster around migrations"}, # default confidence
|
||||
])
|
||||
row("observe:ok", "steps",
|
||||
{**SEED_ENTITY, **SEED_3_FACTS, "llm": [OBS_SCRIPT], "dump": ["observations", "entities"],
|
||||
"steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]},
|
||||
run_steps({**SEED_ENTITY, **SEED_3_FACTS, "llm": [OBS_SCRIPT], "dump": ["observations", "entities"],
|
||||
"steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]}))
|
||||
|
||||
# observe prompt bytes
|
||||
row("observe:prompt_bytes", "steps",
|
||||
{**SEED_ENTITY, **SEED_3_FACTS, "fact_entities": SEED_3_FACTS["fact_entities"], "llm": [OBS_SCRIPT],
|
||||
"capture_llm": True, "steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]},
|
||||
run_steps({**SEED_ENTITY, **SEED_3_FACTS, "fact_entities": SEED_3_FACTS["fact_entities"], "llm": [OBS_SCRIPT],
|
||||
"capture_llm": True, "steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]}))
|
||||
|
||||
# ordering: facts created_at DESC controls source_fact_ids
|
||||
SEED_3_FACTS_DESC = {"facts": [
|
||||
{"id": 1, "episode_id": "e", "what": "oldest fact sentence about the bot", "content_hash": "h1",
|
||||
"when_text": "", "where_text": "", "who_text": "", "why_text": "", "fact_type": "world",
|
||||
"entities_json": "[]", "temporal_start": "", "temporal_end": "", "confidence": 0.7, "source": "s",
|
||||
"state": "active", "access_count": 0, "last_accessed": None, "created_at": "2026-01-01T00:00:00+00:00",
|
||||
"superseded_by": None},
|
||||
{"id": 2, "episode_id": "e", "what": "newest fact sentence about the bot", "content_hash": "h2",
|
||||
"when_text": "", "where_text": "", "who_text": "", "why_text": "", "fact_type": "world",
|
||||
"entities_json": "[]", "temporal_start": "", "temporal_end": "", "confidence": 0.7, "source": "s",
|
||||
"state": "active", "access_count": 0, "last_accessed": None, "created_at": "2026-09-01T00:00:00+00:00",
|
||||
"superseded_by": None},
|
||||
{"id": 3, "episode_id": "e", "what": "middle fact sentence about the bot", "content_hash": "h3",
|
||||
"when_text": "", "where_text": "", "who_text": "", "why_text": "", "fact_type": "world",
|
||||
"entities_json": "[]", "temporal_start": "", "temporal_end": "", "confidence": 0.7, "source": "s",
|
||||
"state": "active", "access_count": 0, "last_accessed": None, "created_at": "2026-05-01T00:00:00+00:00",
|
||||
"superseded_by": None},
|
||||
]}
|
||||
row("observe:source_ids_order", "steps",
|
||||
{**SEED_ENTITY, **SEED_3_FACTS_DESC, "llm": [json.dumps([{"observation": "Bot facts span the whole year range"}])],
|
||||
"dump": ["observations"], "steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]},
|
||||
run_steps({**SEED_ENTITY, **SEED_3_FACTS_DESC,
|
||||
"llm": [json.dumps([{"observation": "Bot facts span the whole year range"}])],
|
||||
"dump": ["observations"], "steps": [{"fn": "synthesize_observations", "entity": "deploy-bot"}]}))
|
||||
|
||||
# ── list_beliefs / list_rules / list_skills (ordering + limit + state) ─────
|
||||
SEED_LISTS = {
|
||||
"beliefs": [
|
||||
{"id": 1, "claim": "low confidence belief", "confidence": 0.3, "reasoning": "r",
|
||||
"supporting_fact_ids": "[]", "counterevidence": "[]", "scope": "", "state": "active",
|
||||
"formed_at": FIXED_TS, "last_reinforced": None, "last_tested": None},
|
||||
{"id": 2, "claim": "high confidence belief", "confidence": 0.9, "reasoning": "r",
|
||||
"supporting_fact_ids": "[]", "counterevidence": "[]", "scope": "", "state": "active",
|
||||
"formed_at": FIXED_TS, "last_reinforced": None, "last_tested": None},
|
||||
{"id": 3, "claim": "mid confidence belief", "confidence": 0.5, "reasoning": "r",
|
||||
"supporting_fact_ids": "[]", "counterevidence": "[]", "scope": "", "state": "archived",
|
||||
"formed_at": FIXED_TS, "last_reinforced": None, "last_tested": None},
|
||||
],
|
||||
"rules": [
|
||||
{"id": 1, "pattern": "rule a", "description": "d", "confidence": 0.5, "source_belief_ids": "[]",
|
||||
"trigger_count": 2, "success_count": 2, "failure_count": 0, "scope": "", "state": "active",
|
||||
"created_at": FIXED_TS, "last_triggered": None},
|
||||
{"id": 2, "pattern": "rule b", "description": "d", "confidence": 0.5, "source_belief_ids": "[]",
|
||||
"trigger_count": 5, "success_count": 4, "failure_count": 1, "scope": "", "state": "active",
|
||||
"created_at": FIXED_TS, "last_triggered": None},
|
||||
],
|
||||
"skills": [
|
||||
{"id": 1, "name": "skill-low", "description": "d", "procedure_json": "[]", "source_rule_ids": "[]",
|
||||
"success_count": 1, "failure_count": 0, "state": "active", "created_at": FIXED_TS, "last_used": None},
|
||||
{"id": 2, "name": "skill-high", "description": "d", "procedure_json": "[]", "source_rule_ids": "[]",
|
||||
"success_count": 9, "failure_count": 0, "state": "active", "created_at": FIXED_TS, "last_used": None},
|
||||
],
|
||||
}
|
||||
row("beliefs:order_and_state", "steps",
|
||||
{"seed": SEED_LISTS, "dump": [], "steps": [{"fn": "list_beliefs"}]},
|
||||
run_steps({"seed": SEED_LISTS, "steps": [{"fn": "list_beliefs"}]}))
|
||||
row("beliefs:limit", "steps",
|
||||
{"seed": SEED_LISTS, "steps": [{"fn": "list_beliefs", "limit": 1}]},
|
||||
run_steps({"seed": SEED_LISTS, "steps": [{"fn": "list_beliefs", "limit": 1}]}))
|
||||
row("rules:order_tiebreak_and_state", "steps",
|
||||
{"seed": SEED_LISTS, "steps": [{"fn": "list_rules"}]},
|
||||
run_steps({"seed": SEED_LISTS, "steps": [{"fn": "list_rules"}]}))
|
||||
row("skills:order_and_state", "steps",
|
||||
{"seed": SEED_LISTS, "steps": [{"fn": "list_skills"}]},
|
||||
run_steps({"seed": SEED_LISTS, "steps": [{"fn": "list_skills"}]}))
|
||||
row("lists:empty_db", "steps",
|
||||
{"steps": [{"fn": "list_beliefs"}, {"fn": "list_rules"}, {"fn": "list_skills"}]},
|
||||
run_steps({"steps": [{"fn": "list_beliefs"}, {"fn": "list_rules"}, {"fn": "list_skills"}]}))
|
||||
|
||||
# ── run_lifecycle (all archive gates) ──────────────────────────────────────
|
||||
LIFECYCLE_SEED = {"facts": [
|
||||
{"id": 1, "episode_id": "", "what": "old low confidence untouched fact sentence",
|
||||
"content_hash": "l1", "when_text": "", "where_text": "", "who_text": "", "why_text": "",
|
||||
"fact_type": "world", "entities_json": "[]", "temporal_start": "", "temporal_end": "",
|
||||
"confidence": 0.4, "source": "", "state": "active", "access_count": 0, "last_accessed": None,
|
||||
"created_at": "2020-01-01T00:00:00+00:00", "superseded_by": None}, # → archived
|
||||
{"id": 2, "episode_id": "", "what": "old but accessed fact sentence here",
|
||||
"content_hash": "l2", "when_text": "", "where_text": "", "who_text": "", "why_text": "",
|
||||
"fact_type": "world", "entities_json": "[]", "temporal_start": "", "temporal_end": "",
|
||||
"confidence": 0.4, "source": "", "state": "active", "access_count": 1, "last_accessed": None,
|
||||
"created_at": "2020-01-01T00:00:00+00:00", "superseded_by": None}, # stays
|
||||
{"id": 3, "episode_id": "", "what": "old but confident fact sentence",
|
||||
"content_hash": "l3", "when_text": "", "where_text": "", "who_text": "", "why_text": "",
|
||||
"fact_type": "world", "entities_json": "[]", "temporal_start": "", "temporal_end": "",
|
||||
"confidence": 0.8, "source": "", "state": "active", "access_count": 0, "last_accessed": None,
|
||||
"created_at": "2020-01-01T00:00:00+00:00", "superseded_by": None}, # stays
|
||||
{"id": 4, "episode_id": "", "what": "recent low confidence fact sentence",
|
||||
"content_hash": "l4", "when_text": "", "where_text": "", "who_text": "", "why_text": "",
|
||||
"fact_type": "world", "entities_json": "[]", "temporal_start": "", "temporal_end": "",
|
||||
"confidence": 0.4, "source": "", "state": "active", "access_count": 0, "last_accessed": None,
|
||||
"created_at": "2099-01-01T00:00:00+00:00", "superseded_by": None}, # stays
|
||||
{"id": 5, "episode_id": "", "what": "already archived fact sentence",
|
||||
"content_hash": "l5", "when_text": "", "where_text": "", "who_text": "", "why_text": "",
|
||||
"fact_type": "world", "entities_json": "[]", "temporal_start": "", "temporal_end": "",
|
||||
"confidence": 0.4, "source": "", "state": "archived", "access_count": 0, "last_accessed": None,
|
||||
"created_at": "2020-01-01T00:00:00+00:00", "superseded_by": None}, # untouched
|
||||
], "observations": [
|
||||
{"id": 1, "entity_id": 1, "observation": "superseded observation sentence", "source_fact_ids": "[]",
|
||||
"confidence": 0.7, "state": "active", "created_at": FIXED_TS, "superseded_by": 2}, # → archived
|
||||
{"id": 2, "entity_id": 1, "observation": "current observation sentence", "source_fact_ids": "[]",
|
||||
"confidence": 0.7, "state": "active", "created_at": FIXED_TS, "superseded_by": None}, # stays
|
||||
{"id": 3, "entity_id": 1, "observation": "archived observation sentence", "source_fact_ids": "[]",
|
||||
"confidence": 0.7, "state": "archived", "created_at": FIXED_TS, "superseded_by": 2}, # stays
|
||||
]}
|
||||
row("lifecycle:archive_gates", "steps",
|
||||
{"seed": LIFECYCLE_SEED, "dump": ["facts", "observations"],
|
||||
"steps": [{"fn": "run_lifecycle"}]},
|
||||
run_steps({"seed": LIFECYCLE_SEED, "dump": ["facts", "observations"], "steps": [{"fn": "run_lifecycle"}]}))
|
||||
|
||||
# ── pipeline_stats ─────────────────────────────────────────────────────────
|
||||
row("stats:mixed", "steps",
|
||||
{"seed": {**LIFECYCLE_SEED, **SEED_LISTS, **SEED_ENTITY, **SEED_3_FACTS,
|
||||
"fact_entities": SEED_3_FACTS["fact_entities"], "triples": [
|
||||
{"id": 1, "subject": "s", "predicate": "p", "object": "o", "source_fact_id": 1,
|
||||
"confidence": 0.6, "created_at": FIXED_TS}]},
|
||||
"steps": [{"fn": "run_lifecycle"}, {"fn": "pipeline_stats"}]},
|
||||
run_steps({"seed": {**LIFECYCLE_SEED, **SEED_LISTS, **SEED_ENTITY, **SEED_3_FACTS,
|
||||
"fact_entities": SEED_3_FACTS["fact_entities"], "triples": [
|
||||
{"id": 1, "subject": "s", "predicate": "p", "object": "o", "source_fact_id": 1,
|
||||
"confidence": 0.6, "created_at": FIXED_TS}]},
|
||||
"steps": [{"fn": "run_lifecycle"}, {"fn": "pipeline_stats"}]}))
|
||||
|
||||
row("stats:empty", "steps", {"steps": [{"fn": "pipeline_stats"}]},
|
||||
run_steps({"steps": [{"fn": "pipeline_stats"}]}))
|
||||
|
||||
# ── handle_hook ────────────────────────────────────────────────────────────
|
||||
row("hook:session_end_with_data", "steps",
|
||||
{"steps": [{"fn": "handle_hook", "event": "on_session_end", "data": {"k": 1}}]},
|
||||
run_steps({"steps": [{"fn": "handle_hook", "event": "on_session_end", "data": {"k": 1}}]}))
|
||||
row("hook:other_event_no_data", "steps",
|
||||
{"steps": [{"fn": "handle_hook", "event": "on_tick", "data": None}]},
|
||||
run_steps({"steps": [{"fn": "handle_hook", "event": "on_tick", "data": None}]}))
|
||||
|
||||
# ── CLI (byte-exact stdout/stderr/exit) ────────────────────────────────────
|
||||
def run_cli(setup):
|
||||
import contextlib as _cl
|
||||
path = fresh_db()
|
||||
seed_db(path, setup.get("seed"))
|
||||
llm_queue = list(setup.get("llm", []))
|
||||
old_llm, old_url = ab.llm_call, ab.OLLAMA_URL
|
||||
ab.llm_call = lambda prompt, model=None, max_tokens=500: llm_queue.pop(0) if llm_queue else ""
|
||||
ab.OLLAMA_URL = UNREACHABLE
|
||||
old_argv, old_out, old_err = sys.argv, sys.stdout, sys.stderr
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
code = 0
|
||||
sys.argv = ["abstraction.py"] + setup["argv"]
|
||||
sys.stdout, sys.stderr = out, err
|
||||
try:
|
||||
ab.main()
|
||||
except SystemExit as e:
|
||||
code = e.code if isinstance(e.code, int) else 1
|
||||
finally:
|
||||
sys.stdout, sys.stderr = old_out, old_err
|
||||
sys.argv = old_argv
|
||||
ab.llm_call, ab.OLLAMA_URL = old_llm, old_url
|
||||
return {"stdout": out.getvalue(), "stderr": err.getvalue(), "code": code}
|
||||
|
||||
|
||||
CLI_SETS = {
|
||||
"extract_nojson": ({"llm": [LLM_ARRAY]}, ["extract", "deploy text", "--episode-id", "cx1"]),
|
||||
"extract_json": ({"llm": [LLM_ARRAY]}, ["extract", "deploy text", "--json"]),
|
||||
"extract_usage": ({}, ["extract"]),
|
||||
"extract_fails": ({}, ["extract", "some episode text here"]),
|
||||
"observe_ok": ({"seed": {**SEED_ENTITY, **SEED_3_FACTS, "fact_entities": SEED_3_FACTS["fact_entities"]},
|
||||
"llm": [OBS_SCRIPT]}, ["observe", "deploy-bot", "--json"]),
|
||||
"observe_missing": ({}, ["observe", "ghost"]),
|
||||
"observe_usage": ({}, ["observe"]),
|
||||
"beliefs_text": ({"seed": SEED_LISTS}, ["beliefs"]),
|
||||
"beliefs_json": ({"seed": SEED_LISTS}, ["beliefs", "--json"]),
|
||||
"beliefs_empty": ({}, ["beliefs"]),
|
||||
"rules_text": ({"seed": SEED_LISTS}, ["rules"]),
|
||||
"skills_text": ({"seed": SEED_LISTS}, ["skills"]),
|
||||
"skills_json": ({"seed": SEED_LISTS}, ["skills", "--json"]),
|
||||
"lifecycle_text": ({"seed": LIFECYCLE_SEED}, ["lifecycle"]),
|
||||
"lifecycle_json": ({"seed": LIFECYCLE_SEED}, ["lifecycle", "--json"]),
|
||||
"stats_text": ({"seed": SEED_LISTS}, ["stats"]),
|
||||
"stats_json": ({"seed": SEED_LISTS}, ["stats", "--json"]),
|
||||
"stats_empty_text": ({}, ["stats"]),
|
||||
"hook": ({}, ["hook", "--event", "on_session_end", "--data", '{"session": "s1"}']),
|
||||
"hook_no_event": ({}, ["hook"]),
|
||||
"hook_data_null": ({}, ["hook", "--event", "on_tick"]),
|
||||
"evolve_noop": ({}, ["evolve", "mem-123"]),
|
||||
"bad_choice": ({}, ["nope"]),
|
||||
"no_cmd": ({}, []),
|
||||
"help": ({}, ["--help"]),
|
||||
}
|
||||
for label, (setup, argv) in CLI_SETS.items():
|
||||
s = dict(setup)
|
||||
s["argv"] = argv
|
||||
row(f"cli:{label}", "cli", s, run_cli(s))
|
||||
|
||||
# ── freeze ─────────────────────────────────────────────────────────────────
|
||||
doc = {
|
||||
"frozen_ts": "2026-10-11T13:45:00Z",
|
||||
"frozen_id": "abstraction-full-go-#2479",
|
||||
"generator": "scripts/gen_abstraction_truth.py equivalent (session temp)",
|
||||
"rows": rows,
|
||||
}
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else "/mnt/workdrive/rune/src/go/abstraction/truth.json"
|
||||
with open(out, "w") as f:
|
||||
json.dump(doc, f, indent=1)
|
||||
print(f"frozen {len(rows)} rows -> {out}")
|
||||
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_auto_tasks_truth.py — factory wave truth battery for auto_tasks (live oracle).
|
||||
|
||||
Exercises EVERY public function of src/rune/auto_tasks.py with real inputs,
|
||||
freezing the oracle's behavior (including its documented numbered/bullet
|
||||
adjacency consumption and the (\\d+)/\\1 benchmark backreference) so the Go
|
||||
port at src/go/auto_tasks replays the recorded CALL SEQUENCE byte-for-byte.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
FROZEN_TS = "2026-10-11T12:00:00+00:00"
|
||||
FROZEN_DATE = "2026-10-11"
|
||||
|
||||
|
||||
class FrozenDateTime(datetime):
|
||||
"""Frozen wall clock that advances 1µs per now() call: _now() stays at
|
||||
the frozen second (log entries), but created_at values become strictly
|
||||
increasing so ORDER BY created_at DESC is deterministic in BOTH engines
|
||||
(CPython sqlite3 and modernc sqlite)."""
|
||||
|
||||
_n = 0
|
||||
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
cls._n += 1
|
||||
return datetime(2026, 10, 11, 12, 0, 0, tzinfo=timezone.utc) + timedelta(microseconds=cls._n)
|
||||
|
||||
|
||||
def load_oracle(memory_dir):
|
||||
os.environ["RUNE_MEMORY_DIR"] = memory_dir
|
||||
if "rune.auto_tasks" in sys.modules:
|
||||
del sys.modules["rune.auto_tasks"]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
import rune.auto_tasks as at
|
||||
|
||||
at.datetime = FrozenDateTime
|
||||
return at
|
||||
|
||||
|
||||
def dump_rows(db):
|
||||
return [dict(r) for r in db.execute("SELECT * FROM tasks ORDER BY id").fetchall()]
|
||||
|
||||
|
||||
TS_RE = None
|
||||
|
||||
def _ts_re():
|
||||
global TS_RE
|
||||
if TS_RE is None:
|
||||
import re as _re
|
||||
TS_RE = _re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+00:00")
|
||||
return TS_RE
|
||||
|
||||
|
||||
def norm_ts(obj):
|
||||
"""Rewrite every ISO timestamp to its rank offset so the battery's own
|
||||
clock offsets compare equal to the oracle's (same call sequence → same
|
||||
relative order)."""
|
||||
import re as _re
|
||||
seen = {}
|
||||
|
||||
def rank(m):
|
||||
v = m.group(0)
|
||||
if v not in seen:
|
||||
seen[v] = f"<T{len(seen)}>"
|
||||
return seen[v]
|
||||
|
||||
def walk(o):
|
||||
if isinstance(o, str):
|
||||
return _ts_re().sub(rank, o)
|
||||
if isinstance(o, list):
|
||||
return [walk(x) for x in o]
|
||||
if isinstance(o, dict):
|
||||
return {k: walk(v) for k, v in o.items()}
|
||||
return o
|
||||
|
||||
return walk(obj)
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="at-truth-"))
|
||||
cases = []
|
||||
at = load_oracle(str(tmp / "mem"))
|
||||
|
||||
# ── extract_tasks: every pattern and gate ────────────────────────────
|
||||
def ext(name, message, channel="chat"):
|
||||
cases.append({
|
||||
"fn": "extract_tasks", "name": name,
|
||||
"input": {"message": message, "channel": channel},
|
||||
"expected": at.extract_tasks(message, channel),
|
||||
})
|
||||
|
||||
# numbered, blank-line separated (oracle matches BOTH)
|
||||
ext("numbered_spaced", "Plan:\n\n1. write the parser module first\n\n2. test the parser with fixtures\n\n3. ship the release notes\n")
|
||||
# numbered, dense adjacency (oracle consumes the newline: alternates dropped — PINNED)
|
||||
ext("numbered_dense", "1. write the parser module first\n2. test the parser with fixtures\n3. ship the release notes\n")
|
||||
# numbered with paren style and leading spaces
|
||||
ext("numbered_paren", " 1) audit the lint baseline today\n 2) verify the scope rules too\n")
|
||||
# single numbered item → >=2 gate blocks
|
||||
ext("numbered_single", "1. only one numbered item here\n")
|
||||
# numbered item too short (<=10)
|
||||
ext("numbered_short", "1. short one\n2. short two\n")
|
||||
# bullets, blank-line separated, all three markers
|
||||
ext("bullets_spaced", "- fix the login redirect loop\n\n* update the docs index page\n\n• audit the auth scopes list\n")
|
||||
# bullets, dense (adjacency consumption — PINNED)
|
||||
ext("bullets_dense", "- fix the login redirect loop\n* update the docs index page\n• audit the auth scopes list\n")
|
||||
# bullets single → gate
|
||||
ext("bullets_single", "- just one bullet item here\n")
|
||||
# action verbs: I'll / let me / we should + imperative verbs
|
||||
ext("action_verbs", "I'll fix the parser edge case. Let me check the config loader too. We should verify the whole build. Fix the login bug now. research the flaky test flakes.")
|
||||
# action verb lowercase/mixed case (IGNORECASE)
|
||||
ext("action_case", "FIX the broken parser path. BUILD the release artifact next.")
|
||||
# action verb too long (>=200)
|
||||
long_task = "fix " + "x" * 200
|
||||
ext("action_too_long", f"fix {long_task}\n")
|
||||
# action verb exactly at gates
|
||||
ext("action_boundaries", "fix " + "y" * 7 + "\nfix " + "z" * 196)
|
||||
# sequence words
|
||||
ext("sequence", "first extract the tasks from the message. then store them in the database. finally report the summary counts.")
|
||||
# sequence with commas and "after that"
|
||||
ext("sequence_after_that", "First, audit the scopes. after that verify the parity battery.")
|
||||
# single sequence mention → gate
|
||||
ext("sequence_single", "first do the extraction of tasks only.")
|
||||
# code block stripped: numbered list inside fences ignored
|
||||
ext("codeblock_fence", "```\n1. numbered item inside the fence block\n2. another numbered inside fence\n```\nAfter the fence we should verify the output shape.")
|
||||
# inline code stripped
|
||||
ext("codeblock_inline", "run `fix the parser command` then real text: we should verify the parser output now.")
|
||||
# dedup by description[:50].lower()
|
||||
ext("dedup", "1. write the parser module first, with care\n2. write the parser module first, WITH CARE\n3. write the parser module first, with CARE too\n")
|
||||
# unicode: char-count gates and dedup key on chars (not bytes)
|
||||
ext("unicode_gates", "- ééééééééééé unicode bullet item\n- aaaaaaaaaaééééééééééé another unicode one\n")
|
||||
# empty message
|
||||
ext("empty", "")
|
||||
|
||||
# ── checks: live subprocess runs recorded for shape + decision ───────
|
||||
compile_result = at.run_compile_check()
|
||||
bench_result = at.run_benchmark_check()
|
||||
sanity_result = at.run_sanity_check()
|
||||
# capture the raw stdout each check saw so Go replays the DECISION with
|
||||
# injected runner output (the subprocess itself is the IO seam)
|
||||
scripts_dir = str(REPO / "src" / "rune")
|
||||
compile_stdout = subprocess.run(
|
||||
[sys.executable, "-c",
|
||||
"import py_compile, glob, sys; "
|
||||
"failed = []; "
|
||||
"[failed.append(f) for f in glob.glob('*.py') "
|
||||
"if not py_compile.compile(f, doraise=True)]; "
|
||||
"print(f'FAIL:{len(failed)}' if failed else 'PASS')"],
|
||||
capture_output=True, text=True, timeout=30, cwd=scripts_dir).stdout
|
||||
sanity_stdout = subprocess.run(
|
||||
[sys.executable, "-c",
|
||||
"import sys; sys.path.insert(0, '.'); import memory_store, warm_router, microkernel; print('PASS')"],
|
||||
capture_output=True, text=True, timeout=10, cwd=scripts_dir).stdout
|
||||
cases.append({"fn": "run_compile_check", "name": "live",
|
||||
"input": {"stdout": compile_stdout, "stderr": ""},
|
||||
"expected": compile_result})
|
||||
|
||||
# decision cases: expected computed by the ORACLE itself with a stubbed
|
||||
# subprocess (the oracle's PASS-substring + backreference semantics are
|
||||
# subtle — "9/12 benchmarks passed" PASSES via the "PASS" in "PASSED").
|
||||
import subprocess as _sp
|
||||
|
||||
real_run = _sp.run
|
||||
|
||||
def stubbed_check(canned_stdout):
|
||||
def fake_run(*a, **k):
|
||||
class R:
|
||||
stdout = canned_stdout
|
||||
return R()
|
||||
_sp.run = fake_run
|
||||
try:
|
||||
return at.run_benchmark_check()
|
||||
finally:
|
||||
_sp.run = real_run
|
||||
|
||||
for name, canned in [
|
||||
("decision_equal_counts", "12/12 benchmarks passed\n"),
|
||||
("decision_ALL", "all scenarios ALL GREEN\n"),
|
||||
("decision_PASS_word", "9/12 benchmarks passed\n"),
|
||||
("decision_FAIL", "9/12 benchmarks done\n"),
|
||||
("decision_backref_mid", "progress 3/3 of 9/12 total\n"),
|
||||
]:
|
||||
cases.append({"fn": "run_benchmark_check", "name": name,
|
||||
"input": {"stdout": canned},
|
||||
"expected": stubbed_check(canned)})
|
||||
cases.append({"fn": "run_sanity_check", "name": "live",
|
||||
"input": {"stdout": sanity_stdout, "stderr": ""},
|
||||
"expected": sanity_result})
|
||||
|
||||
# ── run_auto_checks dispatch (checks patched to sentinels) ───────────
|
||||
calls = []
|
||||
|
||||
def fake_compile():
|
||||
calls.append("compile")
|
||||
return {"check": "compile", "passed": True, "details": "sentinel-compile"}
|
||||
|
||||
def fake_bench():
|
||||
calls.append("benchmark")
|
||||
return {"check": "benchmark", "passed": False, "details": "sentinel-bench"}
|
||||
|
||||
def fake_sanity():
|
||||
calls.append("sanity")
|
||||
return {"check": "sanity", "passed": True, "details": "sentinel-sanity"}
|
||||
|
||||
at.run_compile_check = fake_compile
|
||||
at.run_benchmark_check = fake_bench
|
||||
at.run_sanity_check = fake_sanity
|
||||
for name, desc in [
|
||||
("code_and_system", "Refactor the memory plugin code and rebuild the system cache"),
|
||||
("code_only", "fix the parser script"),
|
||||
("system_only", "microkernel neuron cache tuning"),
|
||||
("neutral", "write the weekly summary report"),
|
||||
]:
|
||||
calls.clear()
|
||||
result = at.run_auto_checks(desc)
|
||||
cases.append({"fn": "run_auto_checks", "name": name,
|
||||
"input": {"description": desc},
|
||||
"expected": {"result": result, "calls": list(calls)}})
|
||||
# None-filtering: compile check returns None → filtered
|
||||
at.run_compile_check = lambda: None
|
||||
calls.clear()
|
||||
result = at.run_auto_checks("code review for the fix")
|
||||
cases.append({"fn": "run_auto_checks", "name": "none_filtered",
|
||||
"input": {"description": "code review for the fix"},
|
||||
"expected": {"result": result, "calls": list(calls)}})
|
||||
|
||||
# ── store_tasks + complete_task + list_tasks + completion files ──────
|
||||
# NOTE: the oracle's store_tasks calls _now() ONCE per invocation (shared
|
||||
# ts for the whole batch) while complete_task calls it once more; the
|
||||
# battery replays the same call sequence, so the same relative offsets
|
||||
# apply — the comparison normalizes absolute timestamps below.
|
||||
at = load_oracle(str(tmp / "mem2"))
|
||||
tasks = [
|
||||
{"description": "write the parser module first, with care", "priority": "normal", "pattern": "numbered_list"},
|
||||
{"description": "test the parser with the full fixture corpus", "priority": "high", "pattern": "bullet_list"},
|
||||
{"description": "ééééééééééé unicode store task", "priority": "normal", "pattern": "sequence"},
|
||||
]
|
||||
source = "1. write the parser module first, with care\n- test the parser with the full fixture corpus"
|
||||
stored = at.store_tasks(tasks, channel="tests", source_message=source)
|
||||
# the battery replays the same call sequence against its own clock, so
|
||||
# per-call snapshots record only the rows THIS call added (by id range);
|
||||
# full-table order is engine sort order, not ported logic.
|
||||
max_before = 0
|
||||
db_rows = [r for r in dump_rows(at.connect()) if r["id"] > max_before]
|
||||
completion_path = at.get_completion_file()
|
||||
completion_after_store = Path(completion_path).read_text()
|
||||
# clock sync: the battery replays store_tasks in its own session, so the
|
||||
# completion tail is compared relative to the battery's own file length;
|
||||
# record the tail length only as a self-consistency marker.
|
||||
store_file_len = len(completion_after_store)
|
||||
cases.append({"fn": "store_tasks", "name": "three_tasks_truncated_source",
|
||||
"input": {"tasks": tasks, "channel": "tests", "source_message": source},
|
||||
"expected": {"stored": norm_ts(stored), "db_rows": norm_ts(db_rows),
|
||||
"completion_file": "completion_" + FROZEN_DATE + ".md",
|
||||
"completion_entry": completion_after_store}})
|
||||
# source_message longer than 500 → truncated
|
||||
long_source = "S" * 700
|
||||
max_before = max(r["id"] for r in dump_rows(at.connect()))
|
||||
stored2 = at.store_tasks([{"description": "store the long source message test case"}],
|
||||
channel="chat", source_message=long_source)
|
||||
db_rows2 = [r for r in dump_rows(at.connect()) if r["id"] > max_before]
|
||||
cases.append({"fn": "store_tasks", "name": "source_over_500_truncated",
|
||||
"input": {"tasks": [{"description": "store the long source message test case"}],
|
||||
"channel": "chat", "source_message": long_source},
|
||||
"expected": {"stored": norm_ts(stored2), "db_rows": norm_ts(db_rows2)}})
|
||||
|
||||
# complete_task with patched check sentinels.
|
||||
# NOTE (frozen-truth contract): the battery replays the whole store→
|
||||
# complete sequence against ONE database, so the oracle's db_rows here
|
||||
# are the FULL table (all 4 rows), not just the completed row.
|
||||
at.run_compile_check = fake_compile
|
||||
at.run_benchmark_check = fake_bench
|
||||
at.run_sanity_check = fake_sanity
|
||||
result = at.complete_task(stored[0]["id"], notes="parser module landed, battery green")
|
||||
db_rows3 = [r for r in dump_rows(at.connect()) if r["task_id"] == stored[0]["id"]]
|
||||
log_rows = [dict(r) for r in at.connect().execute(
|
||||
"SELECT * FROM completion_log ORDER BY id").fetchall()]
|
||||
completion_after_complete = Path(completion_path).read_text()
|
||||
cases.append({"fn": "complete_task", "name": "with_checks",
|
||||
"input": {"task_id": stored[0]["id"], "notes": "parser module landed, battery green"},
|
||||
"expected": {"result": norm_ts(result), "db_rows": norm_ts(db_rows3), "log_rows": norm_ts(log_rows),
|
||||
"completion_entry_tail": completion_after_complete[
|
||||
len(completion_after_store):],
|
||||
"tail_len": len(completion_after_complete) - len(completion_after_store),
|
||||
"tail_completed_only": completion_after_complete[
|
||||
completion_after_complete.index("] AT1: completed") - 24:]}})
|
||||
# complete unknown id → description "", no row change, still logs
|
||||
result2 = at.complete_task("AT999", notes="nothing here")
|
||||
db_rows4 = [r for r in dump_rows(at.connect()) if r["task_id"] == "AT999"]
|
||||
cases.append({"fn": "complete_task", "name": "unknown_id",
|
||||
"input": {"task_id": "AT999", "notes": "nothing here"},
|
||||
"expected": {"result": norm_ts(result2), "db_rows": norm_ts(db_rows4)}})
|
||||
# list_tasks: active + completed + limit
|
||||
active = at.list_tasks(status="active", limit=20)
|
||||
completed = at.list_tasks(status="completed", limit=1)
|
||||
done_all = at.list_tasks(status="done", limit=20)
|
||||
cases.append({"fn": "list_tasks", "name": "filters",
|
||||
"input": {},
|
||||
"expected": {"active": norm_ts(active), "completed": norm_ts(completed), "done": norm_ts(done_all)}})
|
||||
|
||||
# log_completion unit: entry bytes with checks incl. FAIL + truncation
|
||||
completion_dir2 = tmp / "mem3" / "completions"
|
||||
os.environ["RUNE_MEMORY_DIR"] = str(tmp / "mem3")
|
||||
at2 = load_oracle(str(tmp / "mem3"))
|
||||
at2.log_completion("AT7", "completed", details="did the thing" * 30,
|
||||
channel="ops",
|
||||
checks=[{"check": "compile", "passed": True, "details": "P" * 150},
|
||||
{"check": "benchmark", "passed": False, "error": "E" * 150}])
|
||||
entry = (tmp / "mem3" / "completions" / ("completion_" + FROZEN_DATE + ".md")).read_text()
|
||||
cases.append({"fn": "log_completion", "name": "entry_bytes",
|
||||
"input": {"task_id": "AT7", "action": "completed", "details": "did the thing" * 30,
|
||||
"channel": "ops",
|
||||
"checks": [{"check": "compile", "passed": True, "details": "P" * 150},
|
||||
{"check": "benchmark", "passed": False, "error": "E" * 150}]},
|
||||
"expected": {"entry": entry}})
|
||||
|
||||
# ── handle_hook ──────────────────────────────────────────────────────
|
||||
at3 = load_oracle(str(tmp / "mem4"))
|
||||
hooky = "we should fix the release pipeline scripts today. we should verify the changelog entries too."
|
||||
cases.append({"fn": "handle_hook", "name": "on_message_stores",
|
||||
"input": {"event": "on_message",
|
||||
"data": {"message": hooky, "channel": "ops"}},
|
||||
"expected": at3.handle_hook("on_message", {"message": hooky, "channel": "ops"})})
|
||||
cases.append({"fn": "handle_hook", "name": "short_message",
|
||||
"input": {"event": "on_message", "data": {"message": "hi there"}},
|
||||
"expected": at3.handle_hook("on_message", {"message": "hi there"})})
|
||||
cases.append({"fn": "handle_hook", "name": "text_key_fallback",
|
||||
"input": {"event": "on_message",
|
||||
"data": {"text": "then update the index page links. finally rebuild the search index."}},
|
||||
"expected": at3.handle_hook("on_message", {
|
||||
"text": "then update the index page links. finally rebuild the search index."})})
|
||||
cases.append({"fn": "handle_hook", "name": "str_data_fallback",
|
||||
"input": {"event": "on_message", "data": {"foo": "bar baz qux quux corge"}},
|
||||
"expected": at3.handle_hook("on_message", {"foo": "bar baz qux quux corge"})})
|
||||
cases.append({"fn": "handle_hook", "name": "other_event",
|
||||
"input": {"event": "on_tick", "data": {"message": hooky}},
|
||||
"expected": at3.handle_hook("on_tick", {"message": hooky})})
|
||||
cases.append({"fn": "handle_hook", "name": "no_data",
|
||||
"input": {"event": "on_message", "data": None},
|
||||
"expected": at3.handle_hook("on_message", None)})
|
||||
|
||||
# ── CLI (subprocess, frozen-memory env; timestamp-free outputs only) ─
|
||||
cli_mem = tmp / "cli"
|
||||
env = {**os.environ, "RUNE_MEMORY_DIR": str(cli_mem), "PYTHONPATH": str(REPO / "src")}
|
||||
|
||||
def cli(name, argv, mem=cli_mem):
|
||||
env_i = {**env, "RUNE_MEMORY_DIR": str(mem)}
|
||||
p = subprocess.run([sys.executable, "-m", "rune.auto_tasks", *argv],
|
||||
capture_output=True, text=True, env=env_i, cwd=str(REPO))
|
||||
cases.append({"fn": "cli", "name": name, "input": {"argv": argv},
|
||||
"expected": {"stdout": p.stdout, "returncode": p.returncode}})
|
||||
|
||||
cli("extract_json", ["extract",
|
||||
"we should fix the release pipeline scripts today. we should verify the changelog entries too.",
|
||||
"--json"])
|
||||
cli("extract_plain", ["extract",
|
||||
"we should fix the release pipeline scripts today. we should verify the changelog entries too."])
|
||||
cli("extract_empty", ["extract", "nothing actionable in this text at all"])
|
||||
cli("extract_usage", ["extract", ""])
|
||||
cli("hook", ["hook", "--event", "on_message",
|
||||
"--data", json.dumps({"message": "let me check the release notes today"})])
|
||||
cli("hook_default_event", ["hook"])
|
||||
cli("complete", ["complete", "AT1", "--notes", "done via cli", "--json"], mem=cli_mem)
|
||||
cli("complete_usage", ["complete", ""])
|
||||
cli("list_json", ["list", "--json"], mem=cli_mem)
|
||||
|
||||
# ── PLUGIN ───────────────────────────────────────────────────────────
|
||||
cases.append({"fn": "plugin", "name": "dict", "input": {},
|
||||
"expected": at.PLUGIN})
|
||||
|
||||
out = {"oracle": "rune.auto_tasks (live)", "frozen_ts": FROZEN_TS,
|
||||
"frozen_date": FROZEN_DATE, "cases": cases}
|
||||
dest = REPO / "src/go/auto_tasks/truth_auto_tasks.json"
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases → {dest}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze construction_intent oracle truth for the Rust port battery
|
||||
(dichotomy wave #2479 tranche, batch: construction_intent).
|
||||
|
||||
Exercises EVERY branch of resolve_construction_intent (negation, request
|
||||
modals, interrogative structure, bare approval, approval idiom,
|
||||
affirmation+demonstrative, imperative+demonstrative, option referents,
|
||||
recency tie-break, stale proposals, reference revival, relative-that,
|
||||
demand fallthrough, creation-verb standalone, consequence + constraint
|
||||
capture) plus the is_question / imperative_verb signal helpers.
|
||||
|
||||
Output: /tmp/ci_truth.json — replayed VERBATIM by
|
||||
src/rust/construction_intent/src/branch_battery.rs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_CI_CORE"] = "python" # freeze the ORACLE, never the kernel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import construction_intent as cint # noqa: E402
|
||||
|
||||
|
||||
def ctx(turn=0, turns=None, props=None, project=None, task=None):
|
||||
c = cint.ConversationContext()
|
||||
c.turn = turn
|
||||
c.recent_turns = [dict(t) for t in (turns or [])]
|
||||
c.pending_proposals = [cint.Proposal(**p) for p in (props or [])]
|
||||
c.active_project = project
|
||||
c.active_task_id = task
|
||||
return c
|
||||
|
||||
|
||||
def snapshot(c: cint.ConversationContext) -> dict:
|
||||
return {
|
||||
"turn": c.turn,
|
||||
"recent_turns": [dict(t) for t in c.recent_turns],
|
||||
"pending_proposals": [
|
||||
{"proposal_id": p.proposal_id, "summary": p.summary,
|
||||
"created_turn": p.created_turn, "superseded": p.superseded}
|
||||
for p in c.pending_proposals],
|
||||
"active_project": c.active_project,
|
||||
"active_task_id": c.active_task_id,
|
||||
}
|
||||
|
||||
|
||||
def run(name, text, c, origin=None):
|
||||
before = snapshot(c)
|
||||
intent = cint.resolve_construction_intent(text, c, origin_context_id=origin)
|
||||
d = intent.to_dict()
|
||||
d.pop("intent_id") # uuid-random by design; the facade mints it
|
||||
return {
|
||||
"name": name,
|
||||
"input": {"text": text, "origin_context_id": origin,
|
||||
"context": before},
|
||||
"expected": d,
|
||||
"post_context": snapshot(c),
|
||||
}
|
||||
|
||||
|
||||
P1 = {"proposal_id": "prop-aaa11111", "summary": "add retry queue with backoff",
|
||||
"created_turn": 3, "superseded": False}
|
||||
P2 = {"proposal_id": "prop-bbb22222", "summary": "ship the metrics dashboard",
|
||||
"created_turn": 4, "superseded": False}
|
||||
P3 = {"proposal_id": "prop-ccc33333", "summary": "replace the auth subsystem",
|
||||
"created_turn": 5, "superseded": False}
|
||||
POLD = {"proposal_id": "prop-olddd01", "summary": "add retry queue with backoff",
|
||||
"created_turn": 10, "superseded": False}
|
||||
|
||||
cases = []
|
||||
|
||||
# ── 0. negation frame ────────────────────────────────────────────────────
|
||||
cases.append(run("negation-blocks", "don't build that yet",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("negation-stop-never", "stop — never migrate the schema",
|
||||
ctx(project="rune",
|
||||
turns=[{"role": "user", "text": "never touch the WAL",
|
||||
"turn": 1}])))
|
||||
|
||||
# ── 1a. request modals ───────────────────────────────────────────────────
|
||||
cases.append(run("modal-authorizes-proposal", "can you fix that?",
|
||||
ctx(turn=6, props=[dict(P1)])))
|
||||
cases.append(run("modal-target-project", "could you add the export flag?",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("modal-no-target", "would you wire the broker?", ctx()))
|
||||
cases.append(run("modal-info-opener-wins", "how would you fix that?",
|
||||
ctx(turn=6, props=[dict(P1)])))
|
||||
cases.append(run("modal-please-remove", "please remove the old cache",
|
||||
ctx(project="rune")))
|
||||
|
||||
# ── 1. interrogative structure ───────────────────────────────────────────
|
||||
cases.append(run("question-plain", "what time is it?", ctx(project="rune")))
|
||||
cases.append(run("question-analysis", "do you think the API needs improvement?",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("question-how-build", "How would you build this?",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("question-constraints-not-captured", "what time is it?",
|
||||
ctx(project="rune", turns=[
|
||||
{"role": "user", "text": "never store raw tokens", "turn": 2},
|
||||
{"role": "user", "text": "must not break the CLI", "turn": 3},
|
||||
])))
|
||||
|
||||
# ── 2. authorization paths ───────────────────────────────────────────────
|
||||
cases.append(run("bare-approval", "go ahead",
|
||||
ctx(turn=6, props=[dict(P1)], project="rune")))
|
||||
cases.append(run("approval-idiom-ship-it", "yes, ship it",
|
||||
ctx(turn=6, props=[dict(P2)])))
|
||||
cases.append(run("affirmation-demonstrative", "yes, that one",
|
||||
ctx(turn=6, props=[dict(P2)])))
|
||||
cases.append(run("imperative-demonstrative", "build that",
|
||||
ctx(turn=6, props=[dict(P1)])))
|
||||
cases.append(run("bare-approval-lgtm", "lgtm", ctx(turn=6, props=[dict(P3)])))
|
||||
cases.append(run("bare-approval-approved", "approved",
|
||||
ctx(turn=6, props=[dict(P1)])))
|
||||
cases.append(run("do-that-period", "Do that.", ctx()))
|
||||
cases.append(run("approval-no-referent", "ok", ctx(project=None)))
|
||||
cases.append(run("approval-stale-only", "go ahead",
|
||||
ctx(turn=14, props=[dict(POLD)])))
|
||||
cases.append(run("approval-stale-imperative", "go ahead and build the parser",
|
||||
ctx(turn=14, props=[dict(POLD)])))
|
||||
cases.append(run("stale-revived-by-reference", "go ahead with the backoff queue",
|
||||
ctx(turn=14, props=[dict(POLD)])))
|
||||
cases.append(run("ambiguous-two-proposals", "go ahead",
|
||||
ctx(turn=6, props=[dict(P1), dict(P2)])))
|
||||
cases.append(run("recency-decisive", "go ahead",
|
||||
ctx(turn=8, props=[dict(P1), dict(P2, created_turn=7)])))
|
||||
cases.append(run("option-second", "the second one, ship it",
|
||||
ctx(turn=6, props=[dict(P1), dict(P2)])))
|
||||
cases.append(run("option-latter", "the latter works for me",
|
||||
ctx(turn=6, props=[dict(P1), dict(P2), dict(P3)])))
|
||||
cases.append(run("option-former", "the former is fine",
|
||||
ctx(turn=6, props=[dict(P1), dict(P2)])))
|
||||
cases.append(run("option-out-of-range", "the third option",
|
||||
ctx(turn=6, props=[dict(P1), dict(P2)])))
|
||||
cases.append(run("option-other", "the other approach is better, go ahead",
|
||||
ctx(turn=6, props=[dict(P1), dict(P2)])))
|
||||
cases.append(run("relative-that-no-referent", "the proposal that you suggested",
|
||||
ctx(turn=6, props=[dict(P1)])))
|
||||
cases.append(run("relative-that-imperative-target",
|
||||
"build the thing that you suggested", ctx(project="rune")))
|
||||
cases.append(run("idiom-approval-target", "yes, build that",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("creation-standalone-idiom", "yes, write me a new config parser",
|
||||
ctx()))
|
||||
cases.append(run("ambiguous-probe-summaries", "go ahead",
|
||||
ctx(turn=6, props=[dict(P1), dict(P3)])))
|
||||
|
||||
# ── 3. explicit imperative demands ───────────────────────────────────────
|
||||
cases.append(run("imperative-target", "fix the flaky lease test",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("imperative-no-target", "fix the flaky lease test", ctx()))
|
||||
cases.append(run("imperative-creation-article", "build me a program", ctx()))
|
||||
cases.append(run("imperative-creation-no-target", "refactor the scheduler module",
|
||||
ctx()))
|
||||
cases.append(run("imperative-idiom-strip", "go ahead and then build the indexer",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("imperative-please-extend", "please extend the report schema",
|
||||
ctx(project="rune")))
|
||||
|
||||
# ── 4. default discussion ────────────────────────────────────────────────
|
||||
cases.append(run("default-discussion", "the weather is nice", ctx(project="rune")))
|
||||
|
||||
# ── consequence levels ───────────────────────────────────────────────────
|
||||
cases.append(run("consequence-high", "migrate the auth subsystem to the new provider",
|
||||
ctx(project="rune")))
|
||||
cases.append(run("consequence-medium", "port the module", ctx(project="rune")))
|
||||
cases.append(run("consequence-low", "fix the typo in the README",
|
||||
ctx(project="rune")))
|
||||
|
||||
# ── constraints capture ──────────────────────────────────────────────────
|
||||
cases.append(run("constraints-captured", "add the export flag",
|
||||
ctx(project="rune", turns=[
|
||||
{"role": "user", "text": "no hardcoded endpoints", "turn": 2},
|
||||
{"role": "assistant", "text": "understood", "turn": 3},
|
||||
{"role": "user", "text": "and never store raw tokens", "turn": 4},
|
||||
{"role": "user", "text": "must not break the CLI", "turn": 5},
|
||||
{"role": "user", "text": "window filler A", "turn": 6},
|
||||
{"role": "user", "text": "window filler B", "turn": 7},
|
||||
{"role": "user", "text": "dropped: no hardcoded paths", "turn": 8},
|
||||
])))
|
||||
|
||||
# ── stale gap boundary ───────────────────────────────────────────────────
|
||||
cases.append(run("stale-gap-exactly-two", "go ahead",
|
||||
ctx(turn=6, props=[dict(P1, created_turn=4)])))
|
||||
cases.append(run("stale-gap-three-revived", "go ahead with the retry queue",
|
||||
ctx(turn=6, props=[dict(P1, created_turn=3)])))
|
||||
|
||||
# ── superseded proposals are invisible ───────────────────────────────────
|
||||
cases.append(run("superseded-invisible", "go ahead",
|
||||
ctx(turn=6, props=[dict(P1, superseded=True)])))
|
||||
|
||||
# ── origin_context_id round-trip ─────────────────────────────────────────
|
||||
cases.append(run("origin-roundtrip", "fix the flaky lease test",
|
||||
ctx(project="rune"), origin="ctx-12345"))
|
||||
|
||||
# ── signal helpers ───────────────────────────────────────────────────────
|
||||
is_question_cases = [
|
||||
["How would you build this?", True],
|
||||
["what time is it?", True],
|
||||
["Build me a program", False],
|
||||
["The weather is nice", False],
|
||||
["replace the old widget", False],
|
||||
["shipping it today?", True],
|
||||
["remove it?", False],
|
||||
["do we ship tonight?", True],
|
||||
["did the deploy finish?", True],
|
||||
["go ahead?", True],
|
||||
]
|
||||
imperative_cases = [
|
||||
["Build me a program", "build"],
|
||||
["please implement X now", "implement"],
|
||||
["go ahead and fix the bug", "fix"],
|
||||
["go ahead and then build the indexer", "build"],
|
||||
[" Build something", "build"],
|
||||
["How would you build this?", None],
|
||||
["The weather is nice", None],
|
||||
["PORT the module", "port"],
|
||||
]
|
||||
|
||||
truth = {
|
||||
"frozen": "construction_intent resolve_construction_intent + signal helpers",
|
||||
"oracle": "src/rune/construction_intent.py",
|
||||
"note": "intent_id popped (uuid-random; facade mints it). Contexts are "
|
||||
"PRE-mutation; the facade advances next_turn/record_turn before "
|
||||
"delegation, so the Rust kernel receives the POST-mutation state.",
|
||||
"resolve": cases,
|
||||
"is_question": is_question_cases,
|
||||
"imperative_verb": imperative_cases,
|
||||
}
|
||||
|
||||
out = Path("/tmp/ci_truth.json")
|
||||
out.write_text(json.dumps(truth, indent=1) + "\n")
|
||||
print(f"froze {len(cases)} resolve cases + {len(is_question_cases)} is_question "
|
||||
f"+ {len(imperative_cases)} imperative_verb -> {out}")
|
||||
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_curator_wave_truth.py — dichotomy wave #2479 (curator FULL-Go batch).
|
||||
|
||||
Freezes truth for the deterministic curator surface not yet covered by the
|
||||
earlier batch truth files (batch22/lexicon/batch2-4): _is_topic_fact,
|
||||
semantic_candidate_key (embed seam stubbed), graph_write decision core
|
||||
(subprocess seam stubbed), author_skill doc-building (SKILLS_DIR → tmp),
|
||||
apply_op surgery (store seams stubbed), handle_hook, skill_path.
|
||||
|
||||
Output: /tmp/truth.json — replayed verbatim by the Go battery
|
||||
(src/go/curator-core/truth_curator_wave_test.go). Never hand-transcribe:
|
||||
every `want` comes from the live oracle run below.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import curator # noqa: E402 (the live oracle)
|
||||
|
||||
cases = []
|
||||
|
||||
|
||||
def add(fn, name, input_, want):
|
||||
cases.append({"fn": fn, "name": name, "input": input_, "want": copy.deepcopy(want)})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- is_topic_fact
|
||||
add("is_topic_fact", "short fact", {"fact": "likes coffee"}, curator._is_topic_fact("likes coffee"))
|
||||
add("is_topic_fact", "empty fact", {"fact": ""}, curator._is_topic_fact(""))
|
||||
add("is_topic_fact", "no domain word", {"fact": "the user prefers coconut milk"},
|
||||
curator._is_topic_fact("the user prefers coconut milk"))
|
||||
add("is_topic_fact", "workflow", {"fact": "our workflow for shipping releases"},
|
||||
curator._is_topic_fact("our workflow for shipping releases"))
|
||||
add("is_topic_fact", "how to", {"fact": "how to build a birdhouse with hand tools"},
|
||||
curator._is_topic_fact("how to build a birdhouse with hand tools"))
|
||||
add("is_topic_fact", "pattern", {"fact": "the pattern of repeat failures in builds"},
|
||||
curator._is_topic_fact("the pattern of repeat failures in builds"))
|
||||
add("is_topic_fact", "project", {"fact": "the project migration is blocked on review"},
|
||||
curator._is_topic_fact("the project migration is blocked on review"))
|
||||
add("is_topic_fact", "strategy", {"fact": "the strategy for cache warming"},
|
||||
curator._is_topic_fact("the strategy for cache warming"))
|
||||
add("is_topic_fact", "architecture", {"fact": "architecture of the message queue"},
|
||||
curator._is_topic_fact("architecture of the message queue"))
|
||||
|
||||
# ------------------------------------------------------ semantic_candidate_key
|
||||
# Embed seam stubbed to a deterministic vector map (no ollama in the loop).
|
||||
VEC = {
|
||||
"fact alpha about deploy runners": [1.0, 0.0, 0.0],
|
||||
"fact beta about gitea runners": [0.0, 1.0, 0.0], # orthogonal
|
||||
"curator fact about nothing": [0.0, 0.0, 1.0],
|
||||
}
|
||||
curator._embed = lambda texts: {t: VEC[t] for t in texts if t in VEC}
|
||||
|
||||
|
||||
def _ck(text):
|
||||
return curator.candidate_key(text)
|
||||
|
||||
|
||||
cands = {
|
||||
_ck("fact alpha about deploy runners"): {"text": "fact alpha about deploy runners", "count": 1},
|
||||
_ck("fact beta about gitea runners"): {"text": "fact beta about gitea runners", "count": 1},
|
||||
}
|
||||
add("semantic_candidate_key", "exact normalized hit",
|
||||
{"fact": "fact alpha about deploy runners"},
|
||||
curator.semantic_candidate_key("fact alpha about deploy runners", dict(cands)))
|
||||
add("semantic_candidate_key", "rephrased semantic match",
|
||||
{"fact": "fact alpha about deploy runners "},
|
||||
curator.semantic_candidate_key("fact alpha about deploy runners ", dict(cands)))
|
||||
add("semantic_candidate_key", "orthogonal vectors no match",
|
||||
{"fact": "curator fact about nothing"},
|
||||
curator.semantic_candidate_key("curator fact about nothing", dict(cands)))
|
||||
add("semantic_candidate_key", "no embedding available",
|
||||
{"fact": "unseen fact text entirely"},
|
||||
curator.semantic_candidate_key("unseen fact text entirely", dict(cands)))
|
||||
|
||||
# ------------------------------------------------------------------ graph_write
|
||||
# Subprocess seam stubbed: capture the argv the oracle hands to graph_memory.py
|
||||
# (argv[3:6] = subj/pred/obj after the "add" verb) and return the exact
|
||||
# stdout contract graph_write greps for ('"added": True').
|
||||
_real_run = subprocess.run
|
||||
captured = []
|
||||
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
captured.append(list(argv))
|
||||
return subprocess.CompletedProcess(argv, 0, stdout='{"added": True}\n', stderr="")
|
||||
|
||||
|
||||
subprocess.run = fake_run
|
||||
try:
|
||||
for name, fact, strength in [
|
||||
("strength below 3 no write", "the user prefers coconut milk", 2),
|
||||
("simple prefer triple", "the user prefers coconut milk", 3),
|
||||
("object trimmed at preposition", "the user teaches guitar at the studio", 3),
|
||||
("unmapped verb no triple", "the user likes coffee", 3),
|
||||
("short subject rejected", "ai builds stuff", 3),
|
||||
("works at canonical verb", "the user works at acme", 3),
|
||||
("object truncated at punctuation", "the user manages the example project. Also more", 3),
|
||||
]:
|
||||
captured.clear()
|
||||
ok = curator.graph_write(fact, "ev", "src", strength)
|
||||
triple = captured[0][3:6] if captured else None
|
||||
add("graph_write", name, {"fact": fact, "strength": strength}, {"ok": ok, "triple": triple})
|
||||
finally:
|
||||
subprocess.run = _real_run
|
||||
|
||||
# ------------------------------------------------------------------ author_skill
|
||||
tmp = tempfile.mkdtemp(prefix="curator-wave-")
|
||||
try:
|
||||
curator.SKILLS_DIR = tmp
|
||||
jl = []
|
||||
written = curator.author_skill(
|
||||
{"name": "Deploy Checklist", "steps": ["pull latest", "run tests", "tag release"],
|
||||
"description": "", "strength": 3, "target": "skill"}, jl)
|
||||
slug_body = open(os.path.join(tmp, "deploy-checklist", "SKILL.md")).read()
|
||||
add("author_skill", "fresh procedure write", {"name": "Deploy Checklist", "strength": 3},
|
||||
{"written": written, "body": slug_body, "journal": jl})
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "Deploy Checklist!!", "steps": ["a"], "strength": 1}, jl)
|
||||
rel = os.path.relpath(curator.skill_path("Deploy Checklist!!"), tmp)
|
||||
add("author_skill", "slug sanitization", {"name": "Deploy Checklist!!"},
|
||||
{"written": written, "rel": rel, "journal": jl})
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "No Steps", "steps": [], "strength": 2}, jl)
|
||||
add("author_skill", "missing steps refused", {"name": "No Steps"}, {"written": written, "journal": jl})
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "", "steps": ["x"], "strength": 2}, jl)
|
||||
add("author_skill", "missing name refused", {"name": ""}, {"written": written, "journal": jl})
|
||||
# existing file without an Evidence section -> Evidence block appended (timestamped)
|
||||
os.makedirs(os.path.join(tmp, "recurring-procedure"), exist_ok=True)
|
||||
with open(os.path.join(tmp, "recurring-procedure", "SKILL.md"), "w") as f:
|
||||
f.write("---\nname: recurring-procedure\ndescription: d\n---\n\n# Recurring Procedure\n")
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "Recurring Procedure", "steps": ["one"], "strength": 2}, jl)
|
||||
body = open(os.path.join(tmp, "recurring-procedure", "SKILL.md")).read()
|
||||
add("author_skill", "existing skill gains evidence section",
|
||||
{"name": "Recurring Procedure", "strength": 2},
|
||||
{"written": written, "has_evidence": "## Evidence" in body, "journal": jl})
|
||||
|
||||
add("skill_path", "path joins slug dirs", {"name": "Deploy X"},
|
||||
os.path.relpath(curator.skill_path("Deploy X"), tmp))
|
||||
|
||||
add("handle_hook", "default handler", {"event": "on_memory_add"},
|
||||
curator.handle_hook("on_memory_add", {"k": "v"}))
|
||||
|
||||
# -------------------------------------------------------------------- apply_op
|
||||
# Store seams stubbed: read_block/write_block/store_has/touch_entry/
|
||||
# graph_write. Every case resets the recorders so each `want` holds only
|
||||
# its own delta.
|
||||
body_by_label = {}
|
||||
store_has = {}
|
||||
state = {}
|
||||
|
||||
def reset():
|
||||
state["writes"] = []
|
||||
state["touches"] = []
|
||||
state["graph"] = []
|
||||
state["rel"] = None
|
||||
|
||||
def fake_read_block(label):
|
||||
return {"label": label}, body_by_label.get(label, "")
|
||||
|
||||
def fake_write_block(label, fm, body):
|
||||
state["writes"].append([label, body])
|
||||
return curator.block_path(label)
|
||||
|
||||
curator.read_block = fake_read_block
|
||||
curator.write_block = fake_write_block
|
||||
curator.store_has = lambda text, topic=None: store_has.get((text.strip(), topic), False)
|
||||
curator.touch_entry = lambda idx, label, text: state["touches"].append([label, text])
|
||||
curator.graph_write = lambda fact, ev="", src="curator", strength=3: state["graph"].append([fact, strength]) or True
|
||||
|
||||
def run_apply(name, op, want_extra=None):
|
||||
reset()
|
||||
idx, jl = {}, []
|
||||
# DELETE exercises the index-meta pop: seed the meta the oracle would
|
||||
# have created when the entry was added.
|
||||
if op["op"] == "DELETE" and op.get("before") == "ship on friday":
|
||||
idx.setdefault(op["target"], {})[curator.entry_key(op["before"])] = {"importance": 0.7}
|
||||
got = curator.apply_op(op, idx, jl)
|
||||
want = {"applied": got, "journal": jl, "writes": state["writes"]}
|
||||
if state["touches"]:
|
||||
want["touches"] = state["touches"]
|
||||
if state["graph"]:
|
||||
want["graph"] = state["graph"]
|
||||
if idx:
|
||||
want["index"] = idx
|
||||
if want_extra:
|
||||
want.update(want_extra)
|
||||
add("apply_op", name, {"op": op}, want)
|
||||
|
||||
run_apply("propose journaled only",
|
||||
{"op": "PROPOSE", "target": "human", "after": "weak fact", "evidence": "e",
|
||||
"source": "s", "strength": 1})
|
||||
|
||||
body_by_label["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
run_apply("add duplicate in body",
|
||||
{"op": "ADD", "target": "human", "after": "prefers tea", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
store_has[("the user bakes bread", "human")] = True
|
||||
run_apply("add duplicate in store",
|
||||
{"op": "ADD", "target": "human", "after": "the user bakes bread", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
store_has.clear()
|
||||
|
||||
body_by_label["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
run_apply("add fresh appends and graphs",
|
||||
{"op": "ADD", "target": "human", "after": "the user teaches guitar", "evidence": "ev",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("constitution never auto-applied",
|
||||
{"op": "ADD", "target": "constitution", "after": "rule", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
body_by_label["project"] = "- deploy at noon\n- ship on friday"
|
||||
run_apply("update supersede transition",
|
||||
{"op": "UPDATE", "target": "project", "before": "deploy at noon", "after": "deploy at dawn",
|
||||
"evidence": "e", "source": "s", "strength": 3})
|
||||
|
||||
run_apply("update without before skipped",
|
||||
{"op": "UPDATE", "target": "project", "after": "deploy at dawn", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("delete removes line and meta",
|
||||
{"op": "DELETE", "target": "project", "before": "ship on friday", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("delete without match skipped",
|
||||
{"op": "DELETE", "target": "project", "before": "no such line", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("unknown label journaled",
|
||||
{"op": "ADD", "target": "archive", "after": "cold fact", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("skill with steps authored",
|
||||
{"op": "ADD", "target": "skill", "after": "", "name": "Skill Thing", "steps": ["do it"],
|
||||
"evidence": "e", "source": "s", "strength": 2})
|
||||
finally:
|
||||
subprocess.run = _real_run
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
out = os.environ.get("TRUTH_OUT", "/tmp/truth.json")
|
||||
with open(out, "w") as f:
|
||||
json.dump(cases, f, indent=1, sort_keys=True)
|
||||
print(f"frozen {len(cases)} cases -> {out}")
|
||||
@@ -0,0 +1,880 @@
|
||||
// Package main — the FULL Go port of src/rune/abstraction.py (#2479
|
||||
// dichotomy tranche, GO side verdict, 17 fns). Name-preserving 1:1 port;
|
||||
// deterministic logic identical; all IO (sqlite, LLM transport, clock,
|
||||
// env, Rust kernel probe) behind injectable seams.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const localModel = "qwen2.5:3b"
|
||||
|
||||
// ── seams ───────────────────────────────────────────────────────────────────
|
||||
|
||||
var (
|
||||
// seamClock — Python datetime.now(timezone.utc).
|
||||
seamClock = time.Now
|
||||
// seamEnv — os.environ lookups.
|
||||
seamEnv = os.Getenv
|
||||
// seamDBPath — memory_env.memory_dir()/abstraction.db.
|
||||
seamDBPath = func() string {
|
||||
root := seamEnv("RUNE_MEMORY_DIR")
|
||||
if root == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = ""
|
||||
}
|
||||
root = filepath.Join(home, "memory")
|
||||
}
|
||||
return filepath.Join(root, "abstraction.db")
|
||||
}
|
||||
// seamLLMPost — the urllib transport behind llm_call.
|
||||
seamLLMPost = defaultLLMPost
|
||||
// seamKernelExtract — cc_abstraction_extract_facts (the Rust kernel).
|
||||
// Nil = kernel unavailable, exactly like cc_loader.available() == False.
|
||||
// Production wires this to the shared library (see src/go/cognition).
|
||||
seamKernelExtract KernelExtractor
|
||||
// seamLlmCall — the llm_call seam itself (extract/observe go through it).
|
||||
seamLlmCall LLMClient = transportLLM{}
|
||||
)
|
||||
|
||||
// KernelExtractor — the cc_abstraction_extract_facts seam (kwargs-style
|
||||
// call site in extract_facts) as an interface: production binds the Rust
|
||||
// kernel adapter, tests bind scripts.
|
||||
type KernelExtractor interface {
|
||||
ExtractFacts(text string, stopwords []string, minLen int) ([]map[string]any, error)
|
||||
}
|
||||
|
||||
// LLMClient — the llm_call seam as an interface: transportLLM in
|
||||
// production, scripted clients in the battery.
|
||||
type LLMClient interface {
|
||||
Call(prompt, model string, maxTokens int) string
|
||||
}
|
||||
|
||||
// transportLLM — production LLMClient: payload build + HTTP transport +
|
||||
// response parse, any failure degrading to "".
|
||||
type transportLLM struct{}
|
||||
|
||||
// Call implements LLMClient (llm_call body).
|
||||
func (transportLLM) Call(prompt, model string, maxTokens int) string {
|
||||
if model == "" {
|
||||
model = localModel
|
||||
}
|
||||
options := NewOrderedMap(
|
||||
KV{"temperature", 0.3},
|
||||
KV{"num_predict", maxTokens},
|
||||
KV{"num_ctx", CtxBudget(model)},
|
||||
)
|
||||
payload := NewOrderedMap(
|
||||
KV{"model", model},
|
||||
KV{"prompt", prompt},
|
||||
KV{"stream", false},
|
||||
KV{"options", options},
|
||||
)
|
||||
body, err := seamLLMPost(ollamaURL+"/api/generate", []byte(encodePythonJSON(payload, 0)))
|
||||
if err != nil {
|
||||
log.Printf("LLM call failed: %v", err)
|
||||
return ""
|
||||
}
|
||||
decoded, err := decodeJSON(body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
om, ok := decoded.(*OrderedMap)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
resp, _ := om.Get("response")
|
||||
s, _ := resp.(string)
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// ollamaURL — OLLAMA_URL env at init (module global in the oracle).
|
||||
var ollamaURL = envDefault("OLLAMA_URL", "http://localhost:11434")
|
||||
|
||||
// defaultLLMPost — the production urllib-equivalent transport.
|
||||
func defaultLLMPost(url string, payload []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(string(payload)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := make([]byte, 0, 4096)
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(tmp)
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func envDefault(key, def string) string {
|
||||
if v := seamEnv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ── _ABSTRACTION_STOPWORDS / _ctx_budget ───────────────────────────────────
|
||||
|
||||
var abstractionStopwordsFrozen = []string{
|
||||
"a", "after", "an", "and", "are", "as", "at", "be", "been", "being",
|
||||
"before", "but", "by", "can", "could", "did", "do", "does", "during",
|
||||
"for", "from", "had", "has", "have", "how", "in", "into", "is", "it",
|
||||
"its", "may", "might", "no", "not", "of", "on", "or", "shall", "should",
|
||||
"that", "the", "these", "this", "those", "through", "to", "was", "were",
|
||||
"what", "when", "where", "which", "who", "why", "will", "with", "would",
|
||||
}
|
||||
|
||||
// AbstractionStopwords — _ABSTRACTION_STOPWORDS(): the fact-extraction
|
||||
// stopword table as a sorted list (gather seam W3.4).
|
||||
func AbstractionStopwords() []string {
|
||||
out := make([]string, len(abstractionStopwordsFrozen))
|
||||
copy(out, abstractionStopwordsFrozen)
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// defaultProfiles — frozen snapshot of model_registry.DEFAULT_PROFILES
|
||||
// (id -> context_window, local): the only profile fields resolve_context_budget
|
||||
// consults for _ctx_budget(model_id). Pinned by the truth battery.
|
||||
var defaultProfiles = map[string]struct {
|
||||
window int64
|
||||
local bool
|
||||
}{
|
||||
"deterministic": {0, true},
|
||||
"qwen2.5:3b": {32768, true},
|
||||
"qwen3:14b": {32768, true},
|
||||
"qwen3.6:27b": {32768, true},
|
||||
"mimo-v2.5-pro": {128000, false},
|
||||
}
|
||||
|
||||
// outputReserve — model_registry.OUTPUT_RESERVE (task_kind defaults to
|
||||
// "default" in _ctx_budget).
|
||||
var outputReserve = map[string]int64{
|
||||
"default": 2048,
|
||||
"verify": 512,
|
||||
"summarize": 1024,
|
||||
"embed": 0,
|
||||
}
|
||||
|
||||
const toolResultReserve = 1024
|
||||
|
||||
// CtxBudget — _ctx_budget(model_id): resolve_context_budget(model_id,
|
||||
// task_kind="default")["num_ctx"] against the frozen default registry.
|
||||
func CtxBudget(modelID string) int64 {
|
||||
window := int64(8192)
|
||||
local := true
|
||||
if p, ok := defaultProfiles[modelID]; ok {
|
||||
window = p.window
|
||||
local = p.local
|
||||
}
|
||||
if window == 0 {
|
||||
window = 8192
|
||||
}
|
||||
reserve := outputReserve["default"]
|
||||
toolReserve := int64(0)
|
||||
if !local {
|
||||
toolReserve = toolResultReserve
|
||||
}
|
||||
numCtx := window - reserve - toolReserve
|
||||
if numCtx < 1024 {
|
||||
numCtx = 1024
|
||||
}
|
||||
return numCtx
|
||||
}
|
||||
|
||||
// ── cc_loader_pin / _core / _cc_probe / _now ────────────────────────────────
|
||||
|
||||
// CcLoaderPin — cc_loader_pin(): RUNE_COGNITION_CORE=python → kernels OFF.
|
||||
func CcLoaderPin() bool {
|
||||
return seamEnv("RUNE_COGNITION_CORE") == "python"
|
||||
}
|
||||
|
||||
// kernelToken — the truthy cc_loader module stand-in when the kernel is
|
||||
// available (Python returns the imported module; only truthiness is read).
|
||||
type kernelToken struct{}
|
||||
|
||||
// CcProbe — _cc_probe(): the kernel loader when available, else None.
|
||||
func CcProbe() any {
|
||||
if seamKernelExtract != nil {
|
||||
return kernelToken{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Core — _core(): the Rust cognition core when available (strangler:
|
||||
// Python fallback load-bearing). RUNE_COGNITION_CORE=python pins the fallback.
|
||||
func Core() any {
|
||||
if CcLoaderPin() {
|
||||
return nil
|
||||
}
|
||||
return CcProbe()
|
||||
}
|
||||
|
||||
// Now — _now(): datetime.now(timezone.utc).isoformat() — microseconds only
|
||||
// when nonzero, "+00:00" suffix.
|
||||
func Now() string {
|
||||
t := seamClock().UTC()
|
||||
base := t.Format("2006-01-02T15:04:05")
|
||||
usec := t.Nanosecond() / 1000
|
||||
if usec != 0 {
|
||||
base += fmt.Sprintf(".%06d", usec)
|
||||
}
|
||||
return base + "+00:00"
|
||||
}
|
||||
|
||||
// ── connect ────────────────────────────────────────────────────────────────
|
||||
|
||||
const schemaDDL = `
|
||||
CREATE TABLE IF NOT EXISTS facts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
episode_id TEXT,
|
||||
what TEXT NOT NULL,
|
||||
content_hash TEXT DEFAULT '',
|
||||
when_text TEXT DEFAULT '',
|
||||
where_text TEXT DEFAULT '',
|
||||
who_text TEXT DEFAULT '',
|
||||
why_text TEXT DEFAULT '',
|
||||
fact_type TEXT DEFAULT 'world',
|
||||
entities_json TEXT DEFAULT '[]',
|
||||
temporal_start TEXT DEFAULT '',
|
||||
temporal_end TEXT DEFAULT '',
|
||||
confidence REAL DEFAULT 0.7,
|
||||
source TEXT DEFAULT '',
|
||||
state TEXT DEFAULT 'active',
|
||||
access_count INTEGER DEFAULT 0,
|
||||
last_accessed TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
superseded_by INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
entity_type TEXT DEFAULT 'concept',
|
||||
summary TEXT DEFAULT '',
|
||||
fact_count INTEGER DEFAULT 0,
|
||||
observation_count INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(name, entity_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_entities (
|
||||
fact_id INTEGER REFERENCES facts(id),
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
PRIMARY KEY (fact_id, entity_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
observation TEXT NOT NULL,
|
||||
source_fact_ids TEXT DEFAULT '[]',
|
||||
confidence REAL DEFAULT 0.7,
|
||||
state TEXT DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
superseded_by INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS beliefs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claim TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 0.5,
|
||||
reasoning TEXT DEFAULT '',
|
||||
supporting_fact_ids TEXT DEFAULT '[]',
|
||||
counterevidence TEXT DEFAULT '[]',
|
||||
scope TEXT DEFAULT '',
|
||||
state TEXT DEFAULT 'active',
|
||||
formed_at TEXT NOT NULL,
|
||||
last_reinforced TEXT,
|
||||
last_tested TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pattern TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
confidence REAL DEFAULT 0.5,
|
||||
source_belief_ids TEXT DEFAULT '[]',
|
||||
trigger_count INTEGER DEFAULT 1,
|
||||
success_count INTEGER DEFAULT 0,
|
||||
failure_count INTEGER DEFAULT 0,
|
||||
scope TEXT DEFAULT '',
|
||||
state TEXT DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
last_triggered TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skills (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
procedure_json TEXT DEFAULT '[]',
|
||||
source_rule_ids TEXT DEFAULT '[]',
|
||||
success_count INTEGER DEFAULT 0,
|
||||
failure_count INTEGER DEFAULT 0,
|
||||
state TEXT DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
last_used TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS triples (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
subject TEXT NOT NULL,
|
||||
predicate TEXT NOT NULL,
|
||||
object TEXT NOT NULL,
|
||||
source_fact_id INTEGER REFERENCES facts(id),
|
||||
confidence REAL DEFAULT 0.7,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_facts_state ON facts(state);
|
||||
CREATE INDEX IF NOT EXISTS idx_facts_type ON facts(fact_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_beliefs_state ON beliefs(state);
|
||||
CREATE INDEX IF NOT EXISTS idx_rules_state ON rules(state);
|
||||
CREATE INDEX IF NOT EXISTS idx_triples_subj ON triples(subject);
|
||||
CREATE INDEX IF NOT EXISTS idx_triples_obj ON triples(object);
|
||||
`
|
||||
|
||||
// Connect — connect(): open/create the abstraction database.
|
||||
func Connect() (*sql.DB, error) {
|
||||
path := seamDBPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("connect: mkdir %s: %w", filepath.Dir(path), err)
|
||||
}
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect: open %s: %w", path, err)
|
||||
}
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("connect: pragma wal: %w", err)
|
||||
}
|
||||
for _, stmt := range splitStatements(schemaDDL) {
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("connect: schema: %w", err)
|
||||
}
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// splitStatements — executescript equivalent: split on ';' (no ';' appears
|
||||
// inside the DDL strings), drop empties.
|
||||
func splitStatements(script string) []string {
|
||||
var out []string
|
||||
for _, s := range strings.Split(script, ";") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── llm_call ───────────────────────────────────────────────────────────────
|
||||
|
||||
// LlmCall — llm_call(prompt, model=None, max_tokens=500): call local Ollama
|
||||
// for routine cognition; any failure degrades to "".
|
||||
func LlmCall(prompt string, model string, maxTokens int) string {
|
||||
return seamLlmCall.Call(prompt, model, maxTokens)
|
||||
}
|
||||
|
||||
// ── extract_facts ──────────────────────────────────────────────────────────
|
||||
|
||||
// pyLen — Python len() on str (codepoints, not bytes).
|
||||
func pyLen(s string) int { return len([]rune(s)) }
|
||||
|
||||
// pySlice — Python s[:n] (codepoints).
|
||||
func pySlice(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if n > len(r) {
|
||||
n = len(r)
|
||||
}
|
||||
return string(r[:n])
|
||||
}
|
||||
|
||||
// parseJSONArray — Python-identical: first '[' to LAST ']', json-parse the
|
||||
// slice; no '[' or end <= start or unparseable → empty.
|
||||
func parseJSONArray(response string) []any {
|
||||
start := strings.Index(response, "[")
|
||||
end := strings.LastIndex(response, "]") + 1
|
||||
if start < 0 || end <= start {
|
||||
return []any{}
|
||||
}
|
||||
decoded, err := decodeJSON([]byte(response[start:end]))
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
arr, ok := decoded.([]any)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// ExtractFacts — extract_facts(episode_text, episode_id=None,
|
||||
// source="session"): Hindsight-style what/when/where/who/why extraction with
|
||||
// Mem0-style ADD/NOOP classification against existing facts.
|
||||
func ExtractFacts(episodeText string, episodeID *string, source string) *OrderedMap {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
log.Printf("connect failed: %v", err)
|
||||
return NewOrderedMap(KV{"extracted", 0}, KV{"error", "LLM did not return valid JSON"}, KV{"raw", ""})
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Existing facts for context (Mem0 pattern).
|
||||
var existing []string
|
||||
rows, err := db.Query("SELECT id, what, who_text FROM facts WHERE state='active' ORDER BY created_at DESC LIMIT 10")
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var what, whoText sql.NullString
|
||||
_ = rows.Scan(&id, &what, &whoText)
|
||||
existing = append(existing, fmt.Sprintf("#%d: %s (who: %s)", id, what.String, whoText.String))
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
existingContext := strings.Join(existing, "\n")
|
||||
if existingContext == "" {
|
||||
existingContext = "No existing facts."
|
||||
}
|
||||
|
||||
prompt := "Extract structured facts from this text. For each fact, provide:\n" +
|
||||
"- what: what happened or what was stated\n" +
|
||||
"- when: when (if mentioned)\n" +
|
||||
"- where: where (if mentioned)\n" +
|
||||
"- who: who is involved\n" +
|
||||
"- why: why (if mentioned)\n" +
|
||||
"- type: \"world\" (objective fact), \"experience\" (something that happened), or \"opinion\" (subjective belief)\n" +
|
||||
"- entities: list of people/places/concepts mentioned\n" +
|
||||
"\nExisting facts for context:\n" +
|
||||
existingContext + "\n" +
|
||||
"\nText:\n" +
|
||||
pySlice(episodeText, 2000) + "\n" +
|
||||
"\nRespond as JSON array:\n" +
|
||||
"[{\"what\": \"...\", \"when\": \"...\", \"where\": \"...\", \"who\": \"...\", \"why\": \"...\", \"type\": \"world|experience|opinion\", \"entities\": [\"name1\", \"name2\"]}]\n" +
|
||||
"\nExtract 1-5 key facts. Be precise, not verbose."
|
||||
|
||||
response := LlmCall(prompt, "", 800)
|
||||
|
||||
// Typed decode of the LLM array (compile-time field access replaces
|
||||
// keyed lookups; parity semantics pinned by the truth battery).
|
||||
var facts []Fact
|
||||
facts = factsFromLLMArray(response)
|
||||
|
||||
// LLM unavailable / unparseable → DETERMINISTIC KERNEL FALLBACK
|
||||
// (W3.4: cc_abstraction_extract_facts — sentence split, stopwords
|
||||
// filtered, min-length gate).
|
||||
if len(facts) == 0 {
|
||||
if core := Core(); core != nil && seamKernelExtract != nil {
|
||||
items, kerr := seamKernelExtract.ExtractFacts(episodeText, AbstractionStopwords(), 10)
|
||||
if kerr == nil {
|
||||
for _, item := range items {
|
||||
rawText, _ := item["text"]
|
||||
text, _ := rawText.(string)
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
facts = append(facts, Fact{
|
||||
What: text,
|
||||
FactType: "experience",
|
||||
Kernel: true,
|
||||
ContentWords: item["content_words"],
|
||||
})
|
||||
}
|
||||
}
|
||||
// kernel failure degrades to empty, intentional silence
|
||||
}
|
||||
}
|
||||
|
||||
if len(facts) == 0 {
|
||||
return NewOrderedMap(
|
||||
KV{"extracted", 0},
|
||||
KV{"error", "LLM did not return valid JSON"},
|
||||
KV{"raw", pySlice(response, 200)},
|
||||
)
|
||||
}
|
||||
|
||||
ts := Now()
|
||||
extracted := int64(0)
|
||||
for _, fact := range facts {
|
||||
what := strings.TrimSpace(fact.What)
|
||||
if what == "" || pyLen(what) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
sum := sha256.Sum256([]byte(strings.ToLower(pySlice(what, 80))))
|
||||
contentHash := hex.EncodeToString(sum[:])[:16]
|
||||
|
||||
var dupID int64
|
||||
dupErr := db.QueryRow(
|
||||
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1", contentHash).Scan(&dupID)
|
||||
if dupErr == nil {
|
||||
continue // NOOP — already exists
|
||||
}
|
||||
|
||||
episodeVal := ""
|
||||
if episodeID != nil {
|
||||
episodeVal = *episodeID
|
||||
}
|
||||
entitiesRaw := make([]any, len(fact.Entities))
|
||||
for i, e := range fact.Entities {
|
||||
entitiesRaw[i] = e
|
||||
}
|
||||
entitiesJSON := encodePythonJSON(entitiesRaw, 0)
|
||||
|
||||
res, ierr := db.Exec(
|
||||
"INSERT INTO facts (episode_id, what, content_hash, when_text, where_text, who_text, why_text, "+
|
||||
"fact_type, entities_json, confidence, source, state, created_at) "+
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)",
|
||||
episodeVal, what, contentHash, fact.When, fact.Where, fact.Who, fact.Why,
|
||||
fact.FactType, entitiesJSON, 0.7, source, ts)
|
||||
if ierr != nil {
|
||||
log.Printf("insert fact failed: %v", ierr)
|
||||
continue
|
||||
}
|
||||
factID, _ := res.LastInsertId()
|
||||
extracted++
|
||||
|
||||
// Entity linking (Mem0 pattern)
|
||||
for _, entityName := range fact.Entities {
|
||||
entityName = strings.TrimSpace(entityName)
|
||||
if entityName == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(
|
||||
"INSERT OR IGNORE INTO entities (name, entity_type, created_at) VALUES (?, 'concept', ?)",
|
||||
entityName, ts); err != nil {
|
||||
continue
|
||||
}
|
||||
var entID int64
|
||||
if err := db.QueryRow("SELECT id FROM entities WHERE name=?", entityName).Scan(&entID); err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = db.Exec("INSERT OR IGNORE INTO fact_entities (fact_id, entity_id) VALUES (?, ?)", factID, entID)
|
||||
_, _ = db.Exec("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?", entID)
|
||||
}
|
||||
|
||||
// Triple extraction (HippoRAG 2 pattern)
|
||||
whoTrimmed := strings.TrimSpace(fact.Who)
|
||||
if whoTrimmed != "" && what != "" {
|
||||
words := strings.Fields(what)
|
||||
if len(words) > 3 {
|
||||
predicate := strings.Join(words[:3], " ")
|
||||
obj := strings.Join(words[3:], " ")
|
||||
_, _ = db.Exec(
|
||||
"INSERT INTO triples (subject, predicate, object, source_fact_id, confidence, created_at) "+
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
whoTrimmed, predicate, obj, factID, 0.6, ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(KV{"extracted", extracted}, KV{"episode_id", pyStrOrNone(episodeID)})
|
||||
}
|
||||
|
||||
// pyStrOrNone — episode_id or None for the result dict.
|
||||
func pyStrOrNone(p *string) any {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
// ── synthesize_observations ────────────────────────────────────────────────
|
||||
|
||||
// SynthesizeObservations — synthesize_observations(entity_name): Hindsight
|
||||
// pattern — when N facts accumulate, generate observations.
|
||||
func SynthesizeObservations(entityName string) *OrderedMap {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return NewOrderedMap(KV{"error", fmt.Sprintf("Entity '%s' not found", entityName)})
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var entityID int64
|
||||
if err := db.QueryRow("SELECT id FROM entities WHERE name=?", entityName).Scan(&entityID); err != nil {
|
||||
return NewOrderedMap(KV{"error", fmt.Sprintf("Entity '%s' not found", entityName)})
|
||||
}
|
||||
|
||||
type factRow struct {
|
||||
id int64
|
||||
what string
|
||||
factType string
|
||||
confidence float64
|
||||
}
|
||||
var facts []factRow
|
||||
rows, err := db.Query(`
|
||||
SELECT f.id, f.what, f.fact_type, f.confidence
|
||||
FROM facts f
|
||||
JOIN fact_entities fe ON f.id = fe.fact_id
|
||||
WHERE fe.entity_id = ? AND f.state = 'active'
|
||||
ORDER BY f.created_at DESC`, entityID)
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var fr factRow
|
||||
var what, factType sql.NullString
|
||||
_ = rows.Scan(&fr.id, &what, &factType, &fr.confidence)
|
||||
fr.what = what.String
|
||||
fr.factType = factType.String
|
||||
facts = append(facts, fr)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
if len(facts) < 3 {
|
||||
return NewOrderedMap(
|
||||
KV{"entity", entityName},
|
||||
KV{"facts", len(facts)},
|
||||
KV{"observations", 0},
|
||||
KV{"reason", "Need at least 3 facts to synthesize"},
|
||||
)
|
||||
}
|
||||
|
||||
var factLines []string
|
||||
limit := len(facts)
|
||||
if limit > 20 {
|
||||
limit = 20
|
||||
}
|
||||
for _, f := range facts[:limit] {
|
||||
factLines = append(factLines, fmt.Sprintf("- [%s] %s", f.factType, f.what))
|
||||
}
|
||||
factsText := strings.Join(factLines, "\n")
|
||||
|
||||
prompt := "Based on these facts about " + entityName + ", generate 3-5 key observations.\n" +
|
||||
"Observations should be objective summaries that synthesize multiple facts.\n" +
|
||||
"\nFacts:\n" +
|
||||
factsText + "\n" +
|
||||
"\nRespond as JSON array:\n" +
|
||||
"[{\"observation\": \"...\", \"confidence\": 0.0-1.0}]"
|
||||
|
||||
response := LlmCall(prompt, "", 500)
|
||||
|
||||
var observations []any
|
||||
if start := strings.Index(response, "["); start >= 0 {
|
||||
end := strings.LastIndex(response, "]") + 1
|
||||
segment := ""
|
||||
if end > start {
|
||||
segment = response[start:end]
|
||||
}
|
||||
if decoded, err := decodeJSON([]byte(segment)); err == nil {
|
||||
observations, _ = decoded.([]any)
|
||||
}
|
||||
}
|
||||
|
||||
ts := Now()
|
||||
var created int64
|
||||
sourceIDs := make([]any, len(facts))
|
||||
for i, f := range facts {
|
||||
sourceIDs[i] = f.id
|
||||
}
|
||||
for _, o := range observations {
|
||||
om, ok := o.(*OrderedMap)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
textRaw, _ := om.Get("observation")
|
||||
text, _ := textRaw.(string)
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" || pyLen(text) < 10 {
|
||||
continue
|
||||
}
|
||||
conf := 0.7
|
||||
if cRaw, ok := om.Get("confidence"); ok {
|
||||
if c, cok := cRaw.(float64); cok {
|
||||
conf = c
|
||||
}
|
||||
}
|
||||
if _, err := db.Exec(
|
||||
"INSERT INTO observations (entity_id, observation, source_fact_ids, confidence, state, created_at) "+
|
||||
"VALUES (?, ?, ?, ?, 'active', ?)",
|
||||
entityID, text, encodePythonJSON(sourceIDs, 0), conf, ts); err != nil {
|
||||
continue
|
||||
}
|
||||
created++
|
||||
}
|
||||
|
||||
_, _ = db.Exec("UPDATE entities SET observation_count = ? WHERE id=?", created, entityID)
|
||||
return NewOrderedMap(
|
||||
KV{"entity", entityName},
|
||||
KV{"facts", len(facts)},
|
||||
KV{"observations", created},
|
||||
)
|
||||
}
|
||||
|
||||
// ── list_beliefs / list_rules / list_skills ────────────────────────────────
|
||||
|
||||
func listActive(db *sql.DB, table, order string, limit int) []any {
|
||||
out := []any{}
|
||||
rows, err := db.Query(
|
||||
"SELECT * FROM "+table+" WHERE state='active' ORDER BY "+order+" DESC LIMIT ?", limit)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
defer rows.Close()
|
||||
cols, _ := rows.Columns()
|
||||
vals := make([]any, len(cols))
|
||||
ptrs := make([]any, len(cols))
|
||||
for rows.Next() {
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
continue
|
||||
}
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for i, c := range cols {
|
||||
om.Set(c, scanValue(vals[i]))
|
||||
}
|
||||
out = append(out, om)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// scanValue — database/sql returns []byte for TEXT; normalize to string /
|
||||
// float64 / int64 / nil.
|
||||
func scanValue(v any) any {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case []byte:
|
||||
return string(t)
|
||||
case int64:
|
||||
return t
|
||||
case float64:
|
||||
return t
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
// ListBeliefs — list_beliefs(limit=20).
|
||||
func ListBeliefs(limit int) []any {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
defer db.Close()
|
||||
return listActive(db, "beliefs", "confidence", limit)
|
||||
}
|
||||
|
||||
// ListRules — list_rules(limit=20).
|
||||
func ListRules(limit int) []any {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
defer db.Close()
|
||||
return listActive(db, "rules", "confidence", limit)
|
||||
}
|
||||
|
||||
// ListSkills — list_skills(limit=20).
|
||||
func ListSkills(limit int) []any {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
defer db.Close()
|
||||
return listActive(db, "skills", "success_count", limit)
|
||||
}
|
||||
|
||||
// ── run_lifecycle ──────────────────────────────────────────────────────────
|
||||
|
||||
func queryChanges(db *sql.DB) int64 {
|
||||
var n int64
|
||||
_ = db.QueryRow("SELECT changes()").Scan(&n)
|
||||
return n
|
||||
}
|
||||
|
||||
// RunLifecycle — run_lifecycle(): MemOS-pattern lifecycle state transitions.
|
||||
func RunLifecycle() *OrderedMap {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return NewOrderedMap(KV{"archived", 0})
|
||||
}
|
||||
defer db.Close()
|
||||
var archived int64
|
||||
|
||||
_, _ = db.Exec(`
|
||||
UPDATE facts SET state='archived'
|
||||
WHERE state='active'
|
||||
AND access_count = 0
|
||||
AND created_at < datetime('now', '-30 days')
|
||||
AND confidence < 0.5`)
|
||||
archived += queryChanges(db)
|
||||
|
||||
_, _ = db.Exec(`
|
||||
UPDATE observations SET state='archived'
|
||||
WHERE superseded_by IS NOT NULL AND state='active'`)
|
||||
archived += queryChanges(db)
|
||||
|
||||
return NewOrderedMap(KV{"archived", archived})
|
||||
}
|
||||
|
||||
// ── pipeline_stats ─────────────────────────────────────────────────────────
|
||||
|
||||
// PipelineStats — pipeline_stats().
|
||||
func PipelineStats() *OrderedMap {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return &OrderedMap{Vals: map[string]any{}}
|
||||
}
|
||||
defer db.Close()
|
||||
stats := NewOrderedMap()
|
||||
tablesWithState := map[string]bool{
|
||||
"facts": true, "observations": true, "beliefs": true, "rules": true, "skills": true,
|
||||
}
|
||||
for _, table := range []string{"facts", "entities", "observations", "beliefs", "rules", "skills", "triples"} {
|
||||
var count int64
|
||||
_ = db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count)
|
||||
active := count
|
||||
if tablesWithState[table] {
|
||||
_ = db.QueryRow("SELECT COUNT(*) FROM " + table + " WHERE state='active'").Scan(&active)
|
||||
}
|
||||
stats.Set(table, NewOrderedMap(KV{"total", count}, KV{"active", active}))
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// ── handle_hook ────────────────────────────────────────────────────────────
|
||||
|
||||
// HandleHook — handle_hook(event, data=None): hook handler.
|
||||
func HandleHook(event string, data any) *OrderedMap {
|
||||
if event == "on_session_end" && data != nil {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"}, KV{"event", event}, KV{"handler", "abstraction"})
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"}, KV{"event", event}, KV{"handler", "abstraction"})
|
||||
}
|
||||
|
||||
// pyInt — int rendering for f-string style interpolation.
|
||||
func pyInt(v int64) string { return strconv.FormatInt(v, 10) }
|
||||
@@ -0,0 +1,271 @@
|
||||
// CLI port of abstraction.py main() — argparse surface byte-parity
|
||||
// (usage, help, error strings frozen from the live oracle in truth.json).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const cliUsage = `usage: abstraction.py [-h] [--episode-id EPISODE_ID] [--source SOURCE]
|
||||
[--event EVENT] [--data DATA] [--json]
|
||||
{extract,observe,beliefs,rules,skills,evolve,lifecycle,stats,hook}
|
||||
[arg]
|
||||
`
|
||||
|
||||
const cliHelp = `usage: abstraction.py [-h] [--episode-id EPISODE_ID] [--source SOURCE]
|
||||
[--event EVENT] [--data DATA] [--json]
|
||||
{extract,observe,beliefs,rules,skills,evolve,lifecycle,stats,hook}
|
||||
[arg]
|
||||
|
||||
Abstraction pipeline
|
||||
|
||||
positional arguments:
|
||||
{extract,observe,beliefs,rules,skills,evolve,lifecycle,stats,hook}
|
||||
arg
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--episode-id EPISODE_ID
|
||||
--source SOURCE
|
||||
--event EVENT
|
||||
--data DATA
|
||||
--json
|
||||
`
|
||||
|
||||
var cliChoices = []string{"extract", "observe", "beliefs", "rules", "skills", "evolve", "lifecycle", "stats", "hook"}
|
||||
|
||||
// RunCLI — main(): parse argv, dispatch, print. Returns the exit code.
|
||||
func RunCLI(args []string, stdout, stderr io.Writer) int {
|
||||
var episodeID *string
|
||||
source := "session"
|
||||
var eventFlag *string
|
||||
var dataFlag *string
|
||||
jsonFlag := false
|
||||
var positionals []string
|
||||
helpSeen := false
|
||||
var unknown []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
a := args[i]
|
||||
takeValue := func() (string, bool) {
|
||||
if eq := strings.Index(a, "="); eq >= 0 {
|
||||
return a[eq+1:], true
|
||||
}
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
return args[i], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
switch {
|
||||
case a == "-h" || a == "--help":
|
||||
helpSeen = true
|
||||
case a == "--json":
|
||||
jsonFlag = true
|
||||
case a == "--episode-id" || strings.HasPrefix(a, "--episode-id=") ||
|
||||
a == "--source" || strings.HasPrefix(a, "--source=") ||
|
||||
a == "--event" || strings.HasPrefix(a, "--event=") ||
|
||||
a == "--data" || strings.HasPrefix(a, "--data="):
|
||||
val, ok := takeValue()
|
||||
if !ok {
|
||||
fmt.Fprintf(stderr, "%sabstraction.py: error: argument %s: expected one argument\n", cliUsage, a)
|
||||
return 2
|
||||
}
|
||||
name := a
|
||||
if eq := strings.Index(name, "="); eq >= 0 {
|
||||
name = name[:eq]
|
||||
}
|
||||
switch name {
|
||||
case "--episode-id":
|
||||
episodeID = &val
|
||||
case "--source":
|
||||
source = val
|
||||
case "--event":
|
||||
eventFlag = &val
|
||||
case "--data":
|
||||
dataFlag = &val
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(a, "-") && a != "-" {
|
||||
unknown = append(unknown, a)
|
||||
} else {
|
||||
positionals = append(positionals, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if helpSeen {
|
||||
fmt.Fprint(stdout, cliHelp)
|
||||
return 0
|
||||
}
|
||||
if len(unknown) > 0 {
|
||||
fmt.Fprintf(stderr, "%sabstraction.py: error: unrecognized arguments: %s\n",
|
||||
cliUsage, strings.Join(unknown, " "))
|
||||
return 2
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
fmt.Fprintf(stderr, "%sabstraction.py: error: the following arguments are required: cmd\n", cliUsage)
|
||||
return 2
|
||||
}
|
||||
cmd := positionals[0]
|
||||
valid := false
|
||||
for _, c := range cliChoices {
|
||||
if cmd == c {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
fmt.Fprintf(stderr, "%sabstraction.py: error: argument cmd: invalid choice: '%s' (choose from '%s')\n",
|
||||
cliUsage, cmd, strings.Join(cliChoices, "', '"))
|
||||
return 2
|
||||
}
|
||||
arg := ""
|
||||
if len(positionals) > 1 {
|
||||
arg = positionals[1]
|
||||
}
|
||||
if len(positionals) > 2 {
|
||||
fmt.Fprintf(stderr, "%sabstraction.py: error: unrecognized arguments: %s\n",
|
||||
cliUsage, strings.Join(positionals[2:], " "))
|
||||
return 2
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "extract":
|
||||
if arg == "" {
|
||||
fmt.Fprintln(stdout, "usage: abstraction.py extract '<episode text>'")
|
||||
return 0
|
||||
}
|
||||
result := ExtractFacts(arg, episodeID, source)
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(result, 2))
|
||||
} else {
|
||||
extracted := int64(0)
|
||||
if v, ok := result.Get("extracted"); ok {
|
||||
if n, ok := v.(int64); ok {
|
||||
extracted = n
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stdout, "Extracted %d facts\n", extracted)
|
||||
}
|
||||
|
||||
case "observe":
|
||||
if arg == "" {
|
||||
fmt.Fprintln(stdout, "usage: abstraction.py observe '<entity name>'")
|
||||
return 0
|
||||
}
|
||||
result := SynthesizeObservations(arg)
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(result, 2))
|
||||
} else {
|
||||
entity := "None"
|
||||
if v, ok := result.Get("entity"); ok && v != nil {
|
||||
entity, _ = v.(string)
|
||||
}
|
||||
observations, facts := int64(0), int64(0)
|
||||
if v, ok := result.Get("observations"); ok {
|
||||
if n, ok := v.(int64); ok {
|
||||
observations = n
|
||||
}
|
||||
}
|
||||
if v, ok := result.Get("facts"); ok {
|
||||
if n, ok := v.(int64); ok {
|
||||
facts = n
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stdout, "Entity: %s — %d observations from %d facts\n",
|
||||
entity, observations, facts)
|
||||
}
|
||||
|
||||
case "beliefs":
|
||||
items := ListBeliefs(20)
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(items, 2))
|
||||
} else {
|
||||
for _, item := range items {
|
||||
om := item.(*OrderedMap)
|
||||
conf, _ := om.Get2("confidence").(float64)
|
||||
claim, _ := om.Get2("claim").(string)
|
||||
fmt.Fprintf(stdout, " [%.2f] %s\n", conf, pySlice(claim, 80))
|
||||
}
|
||||
}
|
||||
|
||||
case "rules":
|
||||
items := ListRules(20)
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(items, 2))
|
||||
} else {
|
||||
for _, item := range items {
|
||||
om := item.(*OrderedMap)
|
||||
conf, _ := om.Get2("confidence").(float64)
|
||||
pattern, _ := om.Get2("pattern").(string)
|
||||
fmt.Fprintf(stdout, " [%.2f] %s\n", conf, pySlice(pattern, 80))
|
||||
}
|
||||
}
|
||||
|
||||
case "skills":
|
||||
items := ListSkills(20)
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(items, 2))
|
||||
} else {
|
||||
for _, item := range items {
|
||||
om := item.(*OrderedMap)
|
||||
success, _ := om.Get2("success_count").(int64)
|
||||
failure, _ := om.Get2("failure_count").(int64)
|
||||
name, _ := om.Get2("name").(string)
|
||||
desc, _ := om.Get2("description").(string)
|
||||
fmt.Fprintf(stdout, " [%dW/%dL] %s: %s\n", success, failure, name, pySlice(desc, 60))
|
||||
}
|
||||
}
|
||||
|
||||
case "lifecycle":
|
||||
result := RunLifecycle()
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(result, 2))
|
||||
} else {
|
||||
archived := int64(0)
|
||||
if v, ok := result.Get("archived"); ok {
|
||||
if n, ok := v.(int64); ok {
|
||||
archived = n
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stdout, "Archived: %d\n", archived)
|
||||
}
|
||||
|
||||
case "stats":
|
||||
s := PipelineStats()
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(s, 2))
|
||||
} else {
|
||||
for _, kv := range s.Entries() {
|
||||
counts := kv.Value.(*OrderedMap)
|
||||
total, _ := counts.Get2("total").(int64)
|
||||
active, _ := counts.Get2("active").(int64)
|
||||
fmt.Fprintf(stdout, " %-15s %5d total, %5d active\n", kv.Key, total, active)
|
||||
}
|
||||
}
|
||||
|
||||
case "hook":
|
||||
event := "unknown"
|
||||
if eventFlag != nil {
|
||||
event = *eventFlag
|
||||
}
|
||||
var data any = map[string]any{}
|
||||
if dataFlag != nil {
|
||||
decoded, err := decodeJSON([]byte(*dataFlag))
|
||||
if err != nil {
|
||||
// oracle: json.loads raises — unreachable in truth data
|
||||
return 1
|
||||
}
|
||||
data = decoded
|
||||
}
|
||||
result := HandleHook(event, data)
|
||||
fmt.Fprintln(stdout, encodePythonJSON(result, 0))
|
||||
|
||||
case "evolve":
|
||||
// the oracle's main() declares the choice but has no branch — no-op
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Language-advantage improvements (factory protocol v2 phase B):
|
||||
// compile-time type safety at the LLM boundary (typed Fact replaces
|
||||
// map/OrderedMap duck typing), interfaces at the kwargs-seams, and a
|
||||
// truncation-aware array parser. Behavior is parity-neutral — the frozen
|
||||
// truth battery pins the oracle semantics.
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Fact — the typed what/when/where/who/why fact schema (Hindsight-style)
|
||||
// the extractor works on. Replaces keyed lookups on *OrderedMap with
|
||||
// compile-time field checks.
|
||||
type Fact struct {
|
||||
What string
|
||||
When string
|
||||
Where string
|
||||
Who string
|
||||
Why string
|
||||
FactType string
|
||||
Entities []string
|
||||
Kernel bool
|
||||
ContentWords any // []string from the kernel, nil when absent
|
||||
}
|
||||
|
||||
// factFromMap — decode one LLM/kernel fact dict into a Fact. Missing
|
||||
// fields take the oracle defaults ("", type "world").
|
||||
func factFromMap(om *OrderedMap) Fact {
|
||||
f := Fact{FactType: "world"}
|
||||
if v, ok := om.Get("what"); ok {
|
||||
f.What, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("when"); ok {
|
||||
f.When, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("where"); ok {
|
||||
f.Where, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("who"); ok {
|
||||
f.Who, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("why"); ok {
|
||||
f.Why, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("type"); ok {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
f.FactType = s
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("entities"); ok {
|
||||
if arr, ok := v.([]any); ok {
|
||||
for _, e := range arr {
|
||||
if s, ok := e.(string); ok {
|
||||
f.Entities = append(f.Entities, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("kernel"); ok {
|
||||
f.Kernel, _ = v.(bool)
|
||||
}
|
||||
if v, ok := om.Get("content_words"); ok {
|
||||
f.ContentWords = v
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// factsFromLLMArray — parseJSONArray + typed decode.
|
||||
func factsFromLLMArray(response string) []Fact {
|
||||
var out []Fact
|
||||
for _, item := range parseJSONArray(response) {
|
||||
if om, ok := item.(*OrderedMap); ok {
|
||||
out = append(out, factFromMap(om))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseArrayStatus — ParseJSONArrayTruncated result: the parsed facts plus
|
||||
// a truncation flag. IMPROVEMENT over the oracle's bare []: "no facts"
|
||||
// (start < 0, or a well-formed empty array) is distinguishable from
|
||||
// "facts lost to truncation" ('[' found but the array never closed), so a
|
||||
// caller can retry instead of silently dropping.
|
||||
type ParseArrayStatus struct {
|
||||
Facts []any
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
// ParseJSONArrayTruncated — parseJSONArray with truncation detection.
|
||||
func ParseJSONArrayTruncated(response string) ParseArrayStatus {
|
||||
start := strings.Index(response, "[")
|
||||
lastBracket := strings.LastIndex(response, "]")
|
||||
end := lastBracket + 1
|
||||
if start < 0 {
|
||||
return ParseArrayStatus{Facts: []any{}}
|
||||
}
|
||||
if end <= start {
|
||||
// '[' found but no ']' — truncation
|
||||
return ParseArrayStatus{Facts: []any{}, Truncated: true}
|
||||
}
|
||||
facts := parseJSONArray(response)
|
||||
if len(facts) == 0 && lastBracket < start {
|
||||
// the only ']' precedes the '[' — the array never closed
|
||||
return ParseArrayStatus{Facts: []any{}, Truncated: true}
|
||||
}
|
||||
return ParseArrayStatus{Facts: facts}
|
||||
}
|
||||
@@ -1,3 +1,26 @@
|
||||
module rune/abstraction
|
||||
|
||||
go 1.22
|
||||
|
||||
require modernc.org/sqlite v1.28.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/mod v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.29.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
|
||||
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
@@ -0,0 +1,58 @@
|
||||
// Improvement pins (factory protocol v2 phase B): each language-advantage
|
||||
// improvement gets its own test. Behavior parity with the oracle remains
|
||||
// governed by TestTruthParity; these pin the ADDITIONS.
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJSONArrayTruncatedImprovement(t *testing.T) {
|
||||
// parity: identical results to the oracle semantics for all inputs
|
||||
// the truth battery already covers
|
||||
cases := []struct {
|
||||
in string
|
||||
facts int
|
||||
trunc bool
|
||||
}{
|
||||
{`[{"what": "x"}]`, 1, false},
|
||||
{"no json here", 0, false},
|
||||
{"[]", 0, false},
|
||||
{`[{"what": "x"}`, 0, true}, // unterminated → flagged
|
||||
{"garbage [not json] tail", 0, false},
|
||||
{"prose [4]: [{\"a\": 1}]", 1, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := ParseJSONArrayTruncated(c.in)
|
||||
if len(got.Facts) != c.facts || got.Truncated != c.trunc {
|
||||
t.Errorf("ParseJSONArrayTruncated(%q) = {%d facts, trunc %v}, want {%d, %v}",
|
||||
c.in, len(got.Facts), got.Truncated, c.facts, c.trunc)
|
||||
}
|
||||
}
|
||||
// parity identity: plain parseJSONArray agrees on the facts
|
||||
for _, c := range cases {
|
||||
if !reflect.DeepEqual(parseJSONArray(c.in), ParseJSONArrayTruncated(c.in).Facts) {
|
||||
t.Errorf("truncation-aware parser diverged from parity parser on %q", c.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactTypedDecode(t *testing.T) {
|
||||
om := NewOrderedMap(
|
||||
KV{"what", "a fact with the required length"},
|
||||
KV{"type", "experience"},
|
||||
KV{"entities", []any{"bot", ""}},
|
||||
)
|
||||
f := factFromMap(om)
|
||||
if f.What != "a fact with the required length" || f.FactType != "experience" ||
|
||||
len(f.Entities) != 2 || f.Entities[0] != "bot" || f.Kernel {
|
||||
t.Fatalf("unexpected decode: %+v", f)
|
||||
}
|
||||
// defaults mirror the oracle's fact.get(...): missing type -> "world",
|
||||
// missing dims -> ""
|
||||
empty := factFromMap(NewOrderedMap(KV{"what", "just the what field here"}))
|
||||
if empty.FactType != "world" || empty.When != "" || empty.Entities != nil {
|
||||
t.Fatalf("unexpected defaults: %+v", empty)
|
||||
}
|
||||
}
|
||||
+25
-81
@@ -1,97 +1,41 @@
|
||||
// Package main — abstraction: the GO port of src/rune/abstraction.py's
|
||||
// deterministic JSON-array extractor from LLM responses (Oracle-Battery
|
||||
// Parity Pattern, #2479 GO batch 28).
|
||||
//
|
||||
// IMPROVEMENT (defect fixed): the oracle's extractor returns [] for a
|
||||
// response with prose before the array BUT ALSO silently [] when the JSON
|
||||
// is unterminated — with no way to distinguish "no facts" from "facts
|
||||
// lost to truncation". The Go port returns the parsed array AND a
|
||||
// truncation flag, so the caller can retry instead of silently dropping.
|
||||
// Entry point: CLI (abstraction.py main() port) + truth replay subcommand
|
||||
// for the HARD CI parity gate.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExtractResult — the improved shape: facts + a loss flag.
|
||||
type ExtractResult struct {
|
||||
Facts []map[string]any `json:"facts"`
|
||||
Truncated bool `json:"truncated"` // IMPROVEMENT: true when [ found but no ]
|
||||
}
|
||||
|
||||
// parseJSONArray — Python-identical: first '[' to LAST ']', json-parse the
|
||||
// slice; unparseable or unterminated -> empty.
|
||||
func parseJSONArray(response string) ExtractResult {
|
||||
start := strings.Index(response, "[")
|
||||
end := strings.LastIndex(response, "]") + 1
|
||||
if start < 0 {
|
||||
return ExtractResult{Facts: []map[string]any{}}
|
||||
}
|
||||
if end <= start {
|
||||
// IMPROVEMENT: '[' found but no ']' — truncation, flagged
|
||||
return ExtractResult{Facts: []map[string]any{}, Truncated: true}
|
||||
}
|
||||
slice := response[start:end]
|
||||
var facts []map[string]any
|
||||
if err := json.Unmarshal([]byte(slice), &facts); err != nil {
|
||||
// IMPROVEMENT: distinguish truncation (last ']' precedes the last
|
||||
// '[' — the array never closed) from bad JSON inside a closed array
|
||||
return ExtractResult{Facts: []map[string]any{}, Truncated: strings.LastIndex(response, "]") < start}
|
||||
}
|
||||
if facts == nil {
|
||||
facts = []map[string]any{}
|
||||
}
|
||||
return ExtractResult{Facts: facts}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: abstraction replay <truth.json>")
|
||||
os.Exit(2)
|
||||
}
|
||||
raw, err := os.ReadFile(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var cases []map[string]any
|
||||
if err := json.Unmarshal(raw, &cases); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fails := 0
|
||||
for i, c := range cases {
|
||||
resp, _ := c["resp"].(string)
|
||||
want := c["want"].([]any)
|
||||
got := parseJSONArray(resp)
|
||||
if len(got.Facts) != len(want) {
|
||||
fails++
|
||||
fmt.Printf("case %d: len %d want %d (%q)\n", i, len(got.Facts), len(want), resp)
|
||||
continue
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
for j, w := range want {
|
||||
wm := w.(map[string]any)
|
||||
for k, v := range wm {
|
||||
if got.Facts[j][k] != v {
|
||||
fails++
|
||||
fmt.Printf("case %d [%d].%s: got %v want %v\n", i, j, k, got.Facts[j][k], v)
|
||||
}
|
||||
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())
|
||||
return
|
||||
}
|
||||
// IMPROVEMENT pin: unterminated JSON raises the truncation flag
|
||||
t := parseJSONArray(`[{"what": "x"}`)
|
||||
if !t.Truncated {
|
||||
fails++
|
||||
fmt.Println("IMPROVEMENT FAILED: truncation not flagged")
|
||||
os.Exit(RunCLI(os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
func truthRowCount() int {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
fmt.Println("IMPROVEMENT PINNED: truncation flag distinguishes 'no facts' from 'facts lost to truncation' — the caller can retry instead of silently dropping")
|
||||
if fails > 0 {
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", fails)
|
||||
os.Exit(1)
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases (1 improvement) vs the Python oracle\n", len(cases))
|
||||
return len(doc.Rows)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent and compact, ensure_ascii) so DB JSON columns, LLM
|
||||
// payloads and CLI output are byte-parity with the oracle.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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).
|
||||
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, decoding objects as *OrderedMap
|
||||
// (order-preserving) — dict insertion order is load-bearing in the oracle.
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
default:
|
||||
return tok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// decodeJSON decodes a full JSON document with ordered maps.
|
||||
func decodeJSON(data []byte) (any, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(string(data)))
|
||||
return decodeJSONValue(dec)
|
||||
}
|
||||
|
||||
// encodePythonJSON renders v exactly like Python json.dumps(v, indent=2)
|
||||
// (ensure_ascii=True): the top level is depth 0, children indent one level.
|
||||
// indent > 0 selects the indented form; indent <= 0 the compact default
|
||||
// (', ', ': ') separators.
|
||||
func encodePythonJSON(v any, indent int) string {
|
||||
var sb strings.Builder
|
||||
encodeValue(&sb, v, 0, indent > 0)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeValue(sb *strings.Builder, v any, depth int, indented bool) {
|
||||
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 []string:
|
||||
arr := make([]any, len(t))
|
||||
for i, s := range t {
|
||||
arr[i] = s
|
||||
}
|
||||
encodeValue(sb, arr, depth, indented)
|
||||
case []any:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
if !indented {
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeValue(sb, item, depth, indented)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, depth+1)
|
||||
encodeValue(sb, item, depth+1, indented)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, depth)
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
if t.Len() == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
if !indented {
|
||||
sb.WriteString("{")
|
||||
for i, kv := range t.Entries() {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, depth, indented)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
writeIndent(sb, depth+1)
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, depth+1, indented)
|
||||
if i < len(entries)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, depth)
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeValue(sb, OrderedMapFromMap(t), depth, indented)
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func writeIndent(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr for the values in play
|
||||
// (confidences, small counts): shortest round-trip form.
|
||||
func pythonFloat(f float64) string {
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// canonicalJSON — sorted-key compact rendering used by the truth replay to
|
||||
// compare structured values (Python sorted(dict) is codepoint order, which
|
||||
// for the ASCII keys in play equals Go byte order).
|
||||
func canonicalJSON(v any) string {
|
||||
return encodePythonJSON(sortKeys(v), 0)
|
||||
}
|
||||
|
||||
func sortKeys(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
keys := append([]string(nil), t.Keys...)
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
om.Set(k, sortKeys(t.Vals[k]))
|
||||
}
|
||||
return om
|
||||
case map[string]any:
|
||||
return sortKeys(OrderedMapFromMap(t))
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = sortKeys(item)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated from
|
||||
// the live Python oracle, src/rune/abstraction.py) and re-executes every row
|
||||
// through this Go port, comparing results. String rows (CLI stdout/stderr,
|
||||
// LLM payloads) compare byte-for-byte inside the canonical encoding;
|
||||
// structured rows compare canonically (sorted-key compact JSON).
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
FrozenID string `json:"frozen_id"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case (kept raw; ordered decode preserves want bytes).
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup any `json:"setup"`
|
||||
Want any `json:"want"`
|
||||
}
|
||||
|
||||
// stepDef — one step in a "steps" row (defaults mirror the oracle's kwargs).
|
||||
type stepDef struct {
|
||||
Fn string `json:"fn"`
|
||||
Text string `json:"text"`
|
||||
EpisodeID *string `json:"episode_id"`
|
||||
Source *string `json:"source"`
|
||||
Entity string `json:"entity"`
|
||||
Limit *int `json:"limit"`
|
||||
Event string `json:"event"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// setupDef — decoded setup for steps/cli rows.
|
||||
type setupDef struct {
|
||||
Seed map[string][]any `json:"seed"`
|
||||
LLM []string `json:"llm"`
|
||||
Kernel []any `json:"kernel"`
|
||||
KernelRaise bool `json:"kernel_raise"`
|
||||
Steps []stepDef `json:"steps"`
|
||||
Dump []string `json:"dump"`
|
||||
CaptureLLM bool `json:"capture_llm"`
|
||||
Env map[string]string `json:"env"`
|
||||
Argv []string `json:"argv"`
|
||||
}
|
||||
|
||||
// seedDatabase — create the schema, then insert explicit seed rows.
|
||||
func seedDatabase(dbPath string, seed map[string][]any) error {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Close()
|
||||
if len(seed) == 0 {
|
||||
return nil
|
||||
}
|
||||
con, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer con.Close()
|
||||
for _, table := range []string{"facts", "entities", "fact_entities", "observations", "beliefs", "rules", "skills", "triples"} {
|
||||
rows, ok := seed[table]
|
||||
if !ok || len(rows) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, r := range rows {
|
||||
var rowMap *OrderedMap
|
||||
switch m := r.(type) {
|
||||
case *OrderedMap:
|
||||
rowMap = m
|
||||
case map[string]any:
|
||||
rowMap = OrderedMapFromMap(m)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
var cols, ph []string
|
||||
var vals []any
|
||||
for _, kv := range rowMap.Entries() {
|
||||
cols = append(cols, kv.Key)
|
||||
ph = append(ph, "?")
|
||||
vals = append(vals, pyToSQL(kv.Value))
|
||||
}
|
||||
stmt := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||
table, strings.Join(cols, ", "), strings.Join(ph, ", "))
|
||||
if _, err := con.Exec(stmt, vals...); err != nil {
|
||||
return fmt.Errorf("seed %s: %w", table, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pyToSQL — decoded JSON values to driver values (nil, string, float64, int64).
|
||||
func pyToSQL(v any) any {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case bool:
|
||||
if t {
|
||||
return int64(1)
|
||||
}
|
||||
return int64(0)
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
// dumpDatabase — read tables back in id order (fact_entities by pair).
|
||||
// Language advantage (phase B): the per-table reads are independent —
|
||||
// goroutine fan-out replaces the serial loop; row order per table and the
|
||||
// table order in the result are unchanged.
|
||||
func dumpDatabase(dbPath string, tables []string) *OrderedMap {
|
||||
out := &OrderedMap{Vals: map[string]any{}}
|
||||
con, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
defer con.Close()
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
for _, t := range tables {
|
||||
wg.Add(1)
|
||||
go func(table string) {
|
||||
defer wg.Done()
|
||||
arr := dumpTable(con, table)
|
||||
mu.Lock()
|
||||
out.Set(table, arr)
|
||||
mu.Unlock()
|
||||
}(t)
|
||||
}
|
||||
wg.Wait()
|
||||
return out
|
||||
}
|
||||
|
||||
// dumpTable — one table's rows in id order (fact_entities by pair).
|
||||
func dumpTable(con *sql.DB, t string) []any {
|
||||
order := "id"
|
||||
if t == "fact_entities" {
|
||||
order = "fact_id, entity_id"
|
||||
}
|
||||
rows, err := con.Query(fmt.Sprintf("SELECT * FROM %s ORDER BY %s", t, order))
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
defer rows.Close()
|
||||
cols, _ := rows.Columns()
|
||||
arr := []any{}
|
||||
vals := make([]any, len(cols))
|
||||
ptrs := make([]any, len(cols))
|
||||
for rows.Next() {
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
continue
|
||||
}
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for i, c := range cols {
|
||||
om.Set(c, scanValue(vals[i]))
|
||||
}
|
||||
arr = append(arr, om)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// seamState — scoped seam overrides for one row.
|
||||
type seamState struct{}
|
||||
|
||||
// withSeams — override env/dbPath/clock seams for the duration of apply.
|
||||
func withSeams(env map[string]string, dbPath, clockStr string, apply func(*seamState)) {
|
||||
if env == nil {
|
||||
env = map[string]string{}
|
||||
}
|
||||
prevEnv := seamEnv
|
||||
seamEnv = func(k string) string { return env[k] }
|
||||
prevDBPath := seamDBPath
|
||||
seamDBPath = func() string { return dbPath }
|
||||
prevClock := seamClock
|
||||
if clockStr != "" {
|
||||
t, err := time.Parse(time.RFC3339Nano, clockStr)
|
||||
if err == nil {
|
||||
seamClock = func() time.Time { return t }
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
seamEnv = prevEnv
|
||||
seamDBPath = prevDBPath
|
||||
seamClock = prevClock
|
||||
}()
|
||||
apply(&seamState{})
|
||||
}
|
||||
|
||||
// execRow executes one truth row, returning the Go-side result value.
|
||||
func execRow(setup any, op string) (any, error) {
|
||||
switch op {
|
||||
case "ctx_budget":
|
||||
s := setup.(*OrderedMap)
|
||||
mid, _ := s.Get2("model").(string)
|
||||
return CtxBudget(mid), nil
|
||||
case "stopwords":
|
||||
return AbstractionStopwords(), nil
|
||||
case "cc_loader_pin":
|
||||
s := setup.(*OrderedMap)
|
||||
env, _ := s.Get2("env").(*OrderedMap)
|
||||
overlay := map[string]string{}
|
||||
if env != nil {
|
||||
for _, kv := range env.Entries() {
|
||||
if kv.Value == nil {
|
||||
continue // unset
|
||||
}
|
||||
overlay[kv.Key], _ = kv.Value.(string)
|
||||
}
|
||||
}
|
||||
var got bool
|
||||
withSeams(overlay, "/tmp/unused.db", "", func(*seamState) {
|
||||
got = CcLoaderPin()
|
||||
})
|
||||
return got, nil
|
||||
case "core":
|
||||
s := setup.(*OrderedMap)
|
||||
pinned, _ := s.Get2("pinned").(bool)
|
||||
kernel, _ := s.Get2("kernel").(bool)
|
||||
overlay := map[string]string{}
|
||||
if pinned {
|
||||
overlay["RUNE_COGNITION_CORE"] = "python"
|
||||
}
|
||||
var got any
|
||||
withSeams(overlay, "/tmp/unused.db", "", func(*seamState) {
|
||||
if kernel {
|
||||
prevKernel := seamKernelExtract
|
||||
seamKernelExtract = stubKernel{}
|
||||
defer func() { seamKernelExtract = prevKernel }()
|
||||
}
|
||||
if c := Core(); c != nil {
|
||||
got = "kernel"
|
||||
}
|
||||
})
|
||||
return got, nil
|
||||
case "now":
|
||||
s := setup.(*OrderedMap)
|
||||
usec, _ := s.Get2("usec").(float64)
|
||||
t := time.Date(2026, 1, 1, 12, 30, 45, int(usec)*1000, time.UTC)
|
||||
var got string
|
||||
withSeams(nil, "/tmp/unused.db", "", func(*seamState) {
|
||||
prev := seamClock
|
||||
seamClock = func() time.Time { return t }
|
||||
got = Now()
|
||||
seamClock = prev
|
||||
})
|
||||
return got, nil
|
||||
case "connect_schema":
|
||||
dir, err := os.MkdirTemp("", "abstr-replay-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
dbPath := filepath.Join(dir, "abstraction.db")
|
||||
var got []any
|
||||
withSeams(nil, dbPath, "", func(*seamState) {
|
||||
db, err := Connect()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var names []string
|
||||
for rows.Next() {
|
||||
var n string
|
||||
_ = rows.Scan(&n)
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
got = []any{}
|
||||
for _, n := range names {
|
||||
got = append(got, n)
|
||||
}
|
||||
})
|
||||
return got, nil
|
||||
case "llm_call":
|
||||
s := setup.(*OrderedMap)
|
||||
raise := truthy(s.Get2("raise"))
|
||||
body, _ := s.Get2("body").(string)
|
||||
var got string
|
||||
withSeams(nil, "/tmp/unused.db", "", func(*seamState) {
|
||||
prevPost := seamLLMPost
|
||||
prevCall := seamLlmCall
|
||||
seamLLMPost = func(string, []byte) ([]byte, error) {
|
||||
if raise {
|
||||
return nil, fmt.Errorf("connection refused")
|
||||
}
|
||||
return []byte(body), nil
|
||||
}
|
||||
seamLlmCall = transportLLM{}
|
||||
got = LlmCall("p", "", 500)
|
||||
seamLLMPost = prevPost
|
||||
seamLlmCall = prevCall
|
||||
})
|
||||
return got, nil
|
||||
case "llm_payload":
|
||||
s := setup.(*OrderedMap)
|
||||
prompt, _ := s.Get2("prompt").(string)
|
||||
model, _ := s.Get2("model").(string)
|
||||
maxTokens := 500
|
||||
if mt, ok := s.Get2("max_tokens").(float64); ok {
|
||||
maxTokens = int(mt)
|
||||
}
|
||||
wantURL, _ := s.Get2("url").(string)
|
||||
var gotURL, gotPayload, returned string
|
||||
withSeams(nil, "/tmp/unused.db", "", func(*seamState) {
|
||||
prevPost := seamLLMPost
|
||||
prevCall := seamLlmCall
|
||||
prevURL := ollamaURL
|
||||
seamLLMPost = func(u string, payload []byte) ([]byte, error) {
|
||||
gotURL = u
|
||||
gotPayload = string(payload)
|
||||
return []byte(`{"response": "x"}`), nil
|
||||
}
|
||||
seamLlmCall = transportLLM{}
|
||||
ollamaURL = wantURL
|
||||
returned = LlmCall(prompt, model, maxTokens)
|
||||
seamLLMPost = prevPost
|
||||
seamLlmCall = prevCall
|
||||
ollamaURL = prevURL
|
||||
})
|
||||
return NewOrderedMap(
|
||||
KV{"url", gotURL}, KV{"payload", gotPayload}, KV{"returned", returned}), nil
|
||||
case "steps":
|
||||
return execSteps(setup.(*setupDef))
|
||||
case "cli":
|
||||
return execCLI(setup.(*setupDef))
|
||||
}
|
||||
return nil, fmt.Errorf("unknown op %q", op)
|
||||
}
|
||||
|
||||
func truthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
default:
|
||||
return t != nil
|
||||
}
|
||||
}
|
||||
|
||||
// stubKernel — kernel-present stand-in for the "core" probe rows.
|
||||
type stubKernel struct{}
|
||||
|
||||
// ExtractFacts implements KernelExtractor (no-op).
|
||||
func (stubKernel) ExtractFacts(string, []string, int) ([]map[string]any, error) { return nil, nil }
|
||||
|
||||
// scriptedLLM — replay LLMClient: records calls, plays back frozen
|
||||
// responses in order.
|
||||
type scriptedLLM struct {
|
||||
s *setupDef
|
||||
captures *[]any
|
||||
}
|
||||
|
||||
// Call implements LLMClient.
|
||||
func (sc *scriptedLLM) Call(prompt, model string, maxTokens int) string {
|
||||
*sc.captures = append(*sc.captures, NewOrderedMap(
|
||||
KV{"prompt", prompt}, KV{"model", nil}, KV{"max_tokens", int64(maxTokens)}))
|
||||
if len(sc.s.LLM) == 0 {
|
||||
return ""
|
||||
}
|
||||
resp := sc.s.LLM[0]
|
||||
sc.s.LLM = sc.s.LLM[1:]
|
||||
return resp
|
||||
}
|
||||
|
||||
// scriptedKernel — replay KernelExtractor: records calls, returns the
|
||||
// frozen kernel items (or the scripted failure).
|
||||
type scriptedKernel struct {
|
||||
s *setupDef
|
||||
calls *[]any
|
||||
}
|
||||
|
||||
// ExtractFacts implements KernelExtractor.
|
||||
func (sk *scriptedKernel) ExtractFacts(text string, stopwords []string, minLen int) ([]map[string]any, error) {
|
||||
*sk.calls = append(*sk.calls, NewOrderedMap(
|
||||
KV{"text", text}, KV{"stopwords", stopwords}, KV{"min_len", int64(minLen)}))
|
||||
if sk.s.KernelRaise {
|
||||
return nil, fmt.Errorf("kernel exploded")
|
||||
}
|
||||
out := make([]map[string]any, 0, len(sk.s.Kernel))
|
||||
for _, item := range sk.s.Kernel {
|
||||
switch m := item.(type) {
|
||||
case *OrderedMap:
|
||||
cm := map[string]any{}
|
||||
for _, kv := range m.Entries() {
|
||||
cm[kv.Key] = kv.Value
|
||||
}
|
||||
out = append(out, cm)
|
||||
case map[string]any:
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func execSteps(s *setupDef) (any, error) {
|
||||
dir, err := os.MkdirTemp("", "abstr-replay-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
dbPath := filepath.Join(dir, "abstraction.db")
|
||||
|
||||
overlay := map[string]string{}
|
||||
for k, v := range s.Env {
|
||||
overlay[k] = v
|
||||
}
|
||||
if len(s.Kernel) > 0 || s.KernelRaise {
|
||||
delete(overlay, "RUNE_COGNITION_CORE") // kernel path: env unpinned
|
||||
} else {
|
||||
overlay["RUNE_COGNITION_CORE"] = "python"
|
||||
}
|
||||
|
||||
var results []any
|
||||
var llmCaptures []any
|
||||
var kernelCalls []any
|
||||
|
||||
prevEnv := seamEnv
|
||||
seamEnv = func(k string) string { return overlay[k] }
|
||||
prevDBPath := seamDBPath
|
||||
seamDBPath = func() string { return dbPath }
|
||||
prevClock := seamClock
|
||||
fixed := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
seamClock = func() time.Time { return fixed }
|
||||
prevLlm := seamLlmCall
|
||||
sc := &scriptedLLM{s: s, captures: &llmCaptures}
|
||||
seamLlmCall = sc
|
||||
prevKernel := seamKernelExtract
|
||||
if len(s.Kernel) > 0 || s.KernelRaise {
|
||||
seamKernelExtract = &scriptedKernel{s: s, calls: &kernelCalls}
|
||||
}
|
||||
defer func() {
|
||||
seamEnv = prevEnv
|
||||
seamDBPath = prevDBPath
|
||||
seamClock = prevClock
|
||||
seamLlmCall = prevLlm
|
||||
seamKernelExtract = prevKernel
|
||||
}()
|
||||
|
||||
if err := seedDatabase(dbPath, s.Seed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, step := range s.Steps {
|
||||
switch step.Fn {
|
||||
case "extract_facts":
|
||||
source := "session"
|
||||
if step.Source != nil {
|
||||
source = *step.Source
|
||||
}
|
||||
results = append(results, ExtractFacts(step.Text, step.EpisodeID, source))
|
||||
case "synthesize_observations":
|
||||
results = append(results, SynthesizeObservations(step.Entity))
|
||||
case "list_beliefs":
|
||||
results = append(results, ListBeliefs(pyLimit(step.Limit)))
|
||||
case "list_rules":
|
||||
results = append(results, ListRules(pyLimit(step.Limit)))
|
||||
case "list_skills":
|
||||
results = append(results, ListSkills(pyLimit(step.Limit)))
|
||||
case "run_lifecycle":
|
||||
results = append(results, RunLifecycle())
|
||||
case "pipeline_stats":
|
||||
results = append(results, PipelineStats())
|
||||
case "handle_hook":
|
||||
results = append(results, HandleHook(step.Event, step.Data))
|
||||
}
|
||||
}
|
||||
|
||||
out := NewOrderedMap(KV{"results", results})
|
||||
if s.CaptureLLM {
|
||||
out.Set("llm_calls", llmCaptures)
|
||||
}
|
||||
if len(s.Dump) > 0 {
|
||||
out.Set("db", dumpDatabase(dbPath, s.Dump))
|
||||
}
|
||||
if len(s.Kernel) > 0 || s.KernelRaise {
|
||||
out.Set("kernel_calls", kernelCalls)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func pyLimit(p *int) int {
|
||||
if p == nil {
|
||||
return 20
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func execCLI(s *setupDef) (any, error) {
|
||||
dir, err := os.MkdirTemp("", "abstr-replay-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
dbPath := filepath.Join(dir, "abstraction.db")
|
||||
|
||||
overlay := map[string]string{"RUNE_COGNITION_CORE": "python"}
|
||||
for k, v := range s.Env {
|
||||
overlay[k] = v
|
||||
}
|
||||
|
||||
prevEnv := seamEnv
|
||||
seamEnv = func(k string) string { return overlay[k] }
|
||||
prevDBPath := seamDBPath
|
||||
seamDBPath = func() string { return dbPath }
|
||||
prevClock := seamClock
|
||||
fixed := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
seamClock = func() time.Time { return fixed }
|
||||
prevLlm := seamLlmCall
|
||||
cliLLM := &scriptedLLM{s: s, captures: &[]any{}}
|
||||
seamLlmCall = cliLLM
|
||||
defer func() {
|
||||
seamEnv = prevEnv
|
||||
seamDBPath = prevDBPath
|
||||
seamClock = prevClock
|
||||
seamLlmCall = prevLlm
|
||||
}()
|
||||
|
||||
if err := seedDatabase(dbPath, s.Seed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var outBuf, errBuf strings.Builder
|
||||
code := RunCLI(s.Argv, &outBuf, &errBuf)
|
||||
return NewOrderedMap(
|
||||
KV{"stdout", outBuf.String()},
|
||||
KV{"stderr", errBuf.String()},
|
||||
KV{"code", int64(code)},
|
||||
), nil
|
||||
}
|
||||
|
||||
// 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 any
|
||||
decoded, derr := decodeJSON(raw)
|
||||
if derr != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", derr)}
|
||||
}
|
||||
doc = decoded
|
||||
docMap, ok := doc.(*OrderedMap)
|
||||
if !ok {
|
||||
return []string{"parse truth: not an object"}
|
||||
}
|
||||
rowsRaw, _ := docMap.Get("rows")
|
||||
rows, ok := rowsRaw.([]any)
|
||||
if !ok {
|
||||
return []string{"parse truth: no rows"}
|
||||
}
|
||||
var fails []string
|
||||
for i, r := range rows {
|
||||
rowMap, ok := r.(*OrderedMap)
|
||||
if !ok {
|
||||
fails = append(fails, fmt.Sprintf("row %d: not an object", i))
|
||||
continue
|
||||
}
|
||||
name, _ := rowMap.Get2("name").(string)
|
||||
op, _ := rowMap.Get2("op").(string)
|
||||
setup, err := decodeSetup(rowMap.Get2("setup"), op)
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("%s: setup: %v", name, err))
|
||||
continue
|
||||
}
|
||||
got, err := execRow(setup, op)
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("%s: error: %v", name, err))
|
||||
continue
|
||||
}
|
||||
gotStr := canonicalJSON(got)
|
||||
wantStr := canonicalJSON(rowMap.Get2("want"))
|
||||
if gotStr != wantStr {
|
||||
fails = append(fails, fmt.Sprintf("%s: divergence\n got: %s\n want: %s", name, clip(gotStr), clip(wantStr)))
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
func clip(s string) string {
|
||||
if len(s) > 2000 {
|
||||
return s[:2000] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// decodeSetup — steps/cli setups decode into setupDef; others stay raw.
|
||||
func decodeSetup(setup any, op string) (any, error) {
|
||||
if op == "steps" || op == "cli" {
|
||||
raw := []byte(encodePythonJSON(setup, 0))
|
||||
var sd setupDef
|
||||
if err := json.Unmarshal(raw, &sd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sd, nil
|
||||
}
|
||||
return setup, nil
|
||||
}
|
||||
+4278
-41
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
// Frozen-truth battery: replays every row generated from the live Python
|
||||
// oracle (src/rune/abstraction.py) through this Go port. 100% match required.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruthParity(t *testing.T) {
|
||||
fails := RunTruth("truth.json")
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
t.Error(f)
|
||||
}
|
||||
return
|
||||
}
|
||||
rows := truthRowCount()
|
||||
if rows <= 0 {
|
||||
t.Fatalf("truth.json unreadable or empty")
|
||||
}
|
||||
t.Logf("PARITY OK: %d truth rows exact-equal vs the Python oracle", rows)
|
||||
}
|
||||
|
||||
func TestTruthRowCount(t *testing.T) {
|
||||
rows := truthRowCount()
|
||||
if rows < 77 {
|
||||
t.Fatalf("expected >= 77 truth rows, got %d", rows)
|
||||
}
|
||||
fmt.Printf("truth rows: %d\n", rows)
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
// Package main — auto_tasks: the FULL Go port of src/rune/auto_tasks.py
|
||||
// (issue #2479 dichotomy tranche, factory-wave batch).
|
||||
//
|
||||
// PARITY: deterministic logic is 1:1 with the oracle — including the
|
||||
// oracle's numbered/bullet newline-consumption adjacency behavior and the
|
||||
// (\d+)/\1 benchmark backreference — pinned by the frozen truth battery
|
||||
// (truth_auto_tasks.json, generated live). IO (sqlite, subprocess checks,
|
||||
// completion files, clock, env paths) sits behind injectable seams: Env
|
||||
// carries the paths and clock, CheckRunner abstracts subprocesses.
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Task — one extracted task candidate (description/priority/pattern).
|
||||
type Task struct {
|
||||
Description string
|
||||
Priority string
|
||||
Pattern string
|
||||
}
|
||||
|
||||
// CheckRunner — the subprocess seam behind the auto checks: argv, timeout,
|
||||
// cwd → stdout (error only on start failure/timeout; a non-zero exit is
|
||||
// NOT an error, the oracle never uses check=True).
|
||||
type CheckRunner func(argv []string, timeout time.Duration, cwd string) (string, error)
|
||||
|
||||
// Env — the injectable seams of the module (paths, clock, check runner,
|
||||
// the three check funcs for dispatch-level stubbing).
|
||||
type Env struct {
|
||||
MemoryDir string
|
||||
ScriptsDir string
|
||||
clock func() time.Time
|
||||
Runner CheckRunner
|
||||
compileCheck,
|
||||
benchmarkCheck,
|
||||
sanityCheck func() *OrderedMap
|
||||
}
|
||||
|
||||
// newEnv resolves paths exactly like src/rune/memory_env.py: RUNE_HOME
|
||||
// (default /mnt/workdrive/rune), RUNE_MEMORY_DIR (default <home>/memory),
|
||||
// RUNE_SCRIPTS_DIR (default <home>/src/rune).
|
||||
func newEnv() *Env {
|
||||
home := envOr("RUNE_HOME", "/mnt/workdrive/rune")
|
||||
e := &Env{
|
||||
MemoryDir: envOr("RUNE_MEMORY_DIR", filepath.Join(home, "memory")),
|
||||
ScriptsDir: envOr("RUNE_SCRIPTS_DIR", filepath.Join(home, "src", "rune")),
|
||||
}
|
||||
e.clock = time.Now
|
||||
e.Runner = realRunner
|
||||
e.compileCheck = e.runCompileCheck
|
||||
e.benchmarkCheck = e.runBenchmarkCheck
|
||||
e.sanityCheck = e.runSanityCheck
|
||||
return e
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// nowISO — the oracle's _now(): datetime.now(timezone.utc).isoformat().
|
||||
func (e *Env) nowISO() string {
|
||||
t := e.clock().UTC()
|
||||
base := t.Format("2006-01-02T15:04:05")
|
||||
if micro := t.Nanosecond() / 1000; micro != 0 {
|
||||
base += fmt.Sprintf(".%06d", micro)
|
||||
}
|
||||
return base + "+00:00"
|
||||
}
|
||||
|
||||
func (e *Env) dbPath() string { return filepath.Join(e.MemoryDir, "auto_tasks.db") }
|
||||
func (e *Env) completionDir() string { return filepath.Join(e.MemoryDir, "completions") }
|
||||
|
||||
// connect — open/create the auto-tasks database (schema verbatim).
|
||||
func connect(path string) (*sql.DB, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, stmt := range []string{
|
||||
"PRAGMA journal_mode=WAL",
|
||||
`CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT UNIQUE NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
source_message TEXT DEFAULT '',
|
||||
source_channel TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'active',
|
||||
priority TEXT DEFAULT 'normal',
|
||||
research_links TEXT DEFAULT '[]',
|
||||
completion_notes TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
completed_at TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS completion_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
details TEXT DEFAULT '',
|
||||
timestamp TEXT NOT NULL
|
||||
)`,
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_task_id ON tasks(task_id)",
|
||||
} {
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// ── extract_tasks ────────────────────────────────────────────────────────
|
||||
|
||||
var (
|
||||
codeBlockRe = regexp.MustCompile("```[\\s\\S]*?```")
|
||||
inlineCodeRe = regexp.MustCompile("`[^`]+`")
|
||||
// Python \d is the Unicode Nd category → \p{Nd}.
|
||||
numberedRe = regexp.MustCompile(`(?:^|\n)\s*(\p{Nd}+)[.)]\s*(.+?)(?:\n|$)`)
|
||||
bulletRe = regexp.MustCompile(`(?:^|\n)\s*[-*•]\s*(.+?)(?:\n|$)`)
|
||||
actionRes = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?im)(?:^|\n)\s*(?:I(?:'ll| will| need to)|let me|we should|we need to)\s+(.+?)(?:\.|$|\n)`),
|
||||
regexp.MustCompile(`(?im)(?:^|\n)\s*(?:fix|build|create|add|update|check|verify|test|research|investigate|implement|design)\s+(.+?)(?:\.|$|\n)`),
|
||||
}
|
||||
sequenceRe = regexp.MustCompile(`(?i)(?:first|then|next|finally|after that)[,.]?\s+(.+?)(?:\.|$|\n)`)
|
||||
)
|
||||
|
||||
// extractTasks — extract task-like items from a message. The newline
|
||||
// consumption of the numbered/bullet patterns (alternates of dense lists
|
||||
// are skipped) is the ORACLE's behavior, pinned by the truth battery.
|
||||
func extractTasks(message, channel string) []Task {
|
||||
message = codeBlockRe.ReplaceAllString(message, "")
|
||||
message = inlineCodeRe.ReplaceAllString(message, "")
|
||||
|
||||
var tasks []Task
|
||||
|
||||
numbered := numberedRe.FindAllStringSubmatch(message, -1)
|
||||
if len(numbered) >= 2 {
|
||||
for _, m := range numbered {
|
||||
text := strings.TrimSpace(m[2])
|
||||
if runeLen(text) > 10 {
|
||||
tasks = append(tasks, Task{text, "normal", "numbered_list"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bullets := bulletRe.FindAllStringSubmatch(message, -1)
|
||||
if len(bullets) >= 2 {
|
||||
for _, m := range bullets {
|
||||
text := strings.TrimSpace(m[1])
|
||||
if runeLen(text) > 10 {
|
||||
tasks = append(tasks, Task{text, "normal", "bullet_list"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, re := range actionRes {
|
||||
for _, m := range re.FindAllStringSubmatch(message, -1) {
|
||||
text := strings.TrimSpace(m[1])
|
||||
if runeLen(text) > 10 && runeLen(text) < 200 {
|
||||
tasks = append(tasks, Task{text, "normal", "action_verb"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequence := sequenceRe.FindAllStringSubmatch(message, -1)
|
||||
if len(sequence) >= 2 {
|
||||
for _, m := range sequence {
|
||||
text := strings.TrimSpace(m[1])
|
||||
if runeLen(text) > 10 {
|
||||
tasks = append(tasks, Task{text, "normal", "sequence"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate by description[:50].lower() — first 50 CHARS (runes).
|
||||
seen := map[string]bool{}
|
||||
var unique []Task
|
||||
for _, t := range tasks {
|
||||
r := []rune(t.Description)
|
||||
if len(r) > 50 {
|
||||
r = r[:50]
|
||||
}
|
||||
key := strings.ToLower(string(r))
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
unique = append(unique, t)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
func runeLen(s string) int { return len([]rune(s)) }
|
||||
|
||||
func runeSlice(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) > n {
|
||||
r = r[:n]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func runeLast(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) > n {
|
||||
return string(r[len(r)-n:])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ── store_tasks ──────────────────────────────────────────────────────────
|
||||
|
||||
func (e *Env) storeTasks(tasks []Task, channel, sourceMessage string) ([]*OrderedMap, error) {
|
||||
db, err := connect(e.dbPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
var stored []*OrderedMap
|
||||
ts := e.nowISO()
|
||||
for _, task := range tasks {
|
||||
var maxID sql.NullInt64
|
||||
if err := db.QueryRow("SELECT MAX(id) as max_id FROM tasks").Scan(&maxID); err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
nextID := maxID.Int64 + 1
|
||||
taskID := fmt.Sprintf("AT%d", nextID)
|
||||
priority := task.Priority
|
||||
if priority == "" {
|
||||
priority = "normal"
|
||||
}
|
||||
if _, err := db.Exec(
|
||||
`INSERT INTO tasks (task_id, description, source_message, source_channel,
|
||||
status, priority, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`,
|
||||
taskID, task.Description, runeSlice(sourceMessage, 500), channel, priority, ts, ts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := e.logCompletion(taskID, "created", task.Description, channel, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stored = append(stored, NewOrderedMap(KV{"id", taskID}, KV{"description", task.Description}))
|
||||
}
|
||||
return stored, nil
|
||||
}
|
||||
|
||||
// ── complete_task ────────────────────────────────────────────────────────
|
||||
|
||||
func (e *Env) completeTask(taskID, notes string) (*OrderedMap, error) {
|
||||
db, err := connect(e.dbPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := e.nowISO()
|
||||
var description string
|
||||
var row sql.NullString
|
||||
if err := db.QueryRow(
|
||||
"SELECT description FROM tasks WHERE task_id=? AND status='active'", taskID,
|
||||
).Scan(&row); err == nil && row.Valid {
|
||||
description = row.String
|
||||
}
|
||||
if _, err := db.Exec(
|
||||
`UPDATE tasks SET status='completed', completion_notes=?, completed_at=?, updated_at=?
|
||||
WHERE task_id=? AND status='active'`,
|
||||
notes, ts, ts, taskID); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := db.Exec(
|
||||
`INSERT INTO completion_log (task_id, action, details, timestamp)
|
||||
VALUES (?, 'completed', ?, ?)`,
|
||||
taskID, notes, ts); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
db.Close()
|
||||
|
||||
checkResults := e.runAutoChecks(description)
|
||||
if err := e.logCompletion(taskID, "completed", notes, "", checkResults); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checks := make([]any, 0, len(checkResults))
|
||||
for _, c := range checkResults {
|
||||
checks = append(checks, c)
|
||||
}
|
||||
return NewOrderedMap(KV{"completed", taskID}, KV{"checks", checks}), nil
|
||||
}
|
||||
|
||||
// ── run_auto_checks ──────────────────────────────────────────────────────
|
||||
|
||||
// Checker — one named auto check behind the dispatch seam: the name feeds
|
||||
// the result map, the func feeds runAutoChecks. Compile-time checked,
|
||||
// unlike the oracle's module-global monkeypatching.
|
||||
type Checker struct {
|
||||
Name string
|
||||
Run func() *OrderedMap
|
||||
}
|
||||
|
||||
// runAutoChecks — dispatch checks by task content; the check funcs sit on
|
||||
// Env so dispatch-level stubs can record the call sequence.
|
||||
func (e *Env) runAutoChecks(description string) []*OrderedMap {
|
||||
descLower := strings.ToLower(description)
|
||||
var checkers []Checker
|
||||
if containsAny(descLower, "code", "script", "build", "fix", "implement", "refactor") {
|
||||
checkers = append(checkers, Checker{"compile", e.compileCheck})
|
||||
}
|
||||
if containsAny(descLower, "system", "plugin", "microkernel", "memory", "neuron", "cache") {
|
||||
checkers = append(checkers, Checker{"benchmark", e.benchmarkCheck})
|
||||
}
|
||||
checkers = append(checkers, Checker{"sanity", e.sanityCheck})
|
||||
results := make([]*OrderedMap, 0, len(checkers))
|
||||
for _, c := range checkers {
|
||||
if r := c.Run(); r != nil {
|
||||
results = append(results, r)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func containsAny(s string, subs ...string) bool {
|
||||
for _, sub := range subs {
|
||||
if strings.Contains(s, sub) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── list_tasks ───────────────────────────────────────────────────────────
|
||||
|
||||
var taskColumns = []string{
|
||||
"id", "task_id", "description", "source_message", "source_channel",
|
||||
"status", "priority", "research_links", "completion_notes",
|
||||
"created_at", "completed_at", "updated_at",
|
||||
}
|
||||
|
||||
func (e *Env) listTasks(status string, limit int) ([]*OrderedMap, error) {
|
||||
db, err := connect(e.dbPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(
|
||||
fmt.Sprintf(
|
||||
"SELECT %s FROM tasks WHERE status=? ORDER BY created_at DESC LIMIT ?",
|
||||
strings.Join(taskColumns, ", ")),
|
||||
status, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
vals := make([]any, len(taskColumns))
|
||||
ptrs := make([]any, len(taskColumns))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
var out []*OrderedMap
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
om := NewOrderedMap()
|
||||
for i, col := range taskColumns {
|
||||
om.Set(col, sqliteValue(vals[i]))
|
||||
}
|
||||
out = append(out, om)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func sqliteValue(v any) any {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case []byte:
|
||||
return string(t)
|
||||
case int64:
|
||||
return int(t)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// The three auto checks (compile / benchmark / sanity) — subprocess IO
|
||||
// behind the CheckRunner seam; decision logic is 1:1 with the oracle,
|
||||
// including the (\d+)/\1 backreference semantics of the benchmark pass
|
||||
// pattern (RE2 has no backreferences — hasEqualNN replicates them).
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func realRunner(argv []string, timeout time.Duration, cwd string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
|
||||
if cwd != "" {
|
||||
cmd.Dir = cwd
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("timed out after %s", timeout)
|
||||
}
|
||||
return string(out), err
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func pythonBin() string { return envOr("RUNE_PYTHON_BIN", "python3") }
|
||||
|
||||
// checkResult builds {"check", "passed", "details"} or, on error,
|
||||
// {"check", "passed": false, "error"} — key order matches the oracle.
|
||||
func checkResult(name string, passed bool, details string) *OrderedMap {
|
||||
return NewOrderedMap(KV{"check", name}, KV{"passed", passed}, KV{"details", details})
|
||||
}
|
||||
|
||||
func checkError(name string, err error) *OrderedMap {
|
||||
// error TEXT is parity (runeSlice(err.Error(), 100)): the %w chain above
|
||||
// unwraps to the same message, so the recorded bytes are unchanged.
|
||||
return NewOrderedMap(KV{"check", name}, KV{"passed", false}, KV{"error", runeSlice(err.Error(), 100)})
|
||||
}
|
||||
|
||||
// runCompileCheck — check that all Python scripts compile.
|
||||
func (e *Env) runCompileCheck() *OrderedMap {
|
||||
checkScript := "import py_compile, glob, sys; " +
|
||||
"failed = []; " +
|
||||
"[failed.append(f) for f in glob.glob('*.py') " +
|
||||
"if not py_compile.compile(f, doraise=True)]; " +
|
||||
"print(f'FAIL:{len(failed)}' if failed else 'PASS')"
|
||||
stdout, err := e.Runner([]string{pythonBin(), "-c", checkScript}, 30*time.Second, e.ScriptsDir)
|
||||
if err != nil {
|
||||
return checkError("compile", fmt.Errorf("compile check: %w", err))
|
||||
}
|
||||
return checkResult("compile", strings.Contains(stdout, "PASS"), runeSlice(strings.TrimSpace(stdout), 200))
|
||||
}
|
||||
|
||||
// runBenchmarkCheck — run RuneBench; pass = "N/N" (same number, the
|
||||
// oracle's backreference), or ALL, or PASS.
|
||||
func (e *Env) runBenchmarkCheck() *OrderedMap {
|
||||
stdout, err := e.Runner(
|
||||
[]string{pythonBin(), filepath.Join(e.ScriptsDir, "rune_bench.py"), "run"},
|
||||
120*time.Second, "")
|
||||
if err != nil {
|
||||
return checkError("benchmark", fmt.Errorf("benchmark check: %w", err))
|
||||
}
|
||||
upper := strings.ToUpper(stdout)
|
||||
passed := hasEqualNN(stdout) || strings.Contains(upper, "ALL") || strings.Contains(upper, "PASS")
|
||||
return checkResult("benchmark", passed, runeLast(strings.TrimSpace(stdout), 200))
|
||||
}
|
||||
|
||||
// runSanityCheck — basic sanity: can we import key modules?
|
||||
func (e *Env) runSanityCheck() *OrderedMap {
|
||||
stdout, err := e.Runner([]string{pythonBin(), "-c",
|
||||
"import sys; sys.path.insert(0, '.'); import memory_store, warm_router, microkernel; print('PASS')"},
|
||||
10*time.Second, e.ScriptsDir)
|
||||
if err != nil {
|
||||
return checkError("sanity", fmt.Errorf("sanity check: %w", err))
|
||||
}
|
||||
return checkResult("sanity", strings.Contains(stdout, "PASS"), runeSlice(strings.TrimSpace(stdout), 200))
|
||||
}
|
||||
|
||||
// hasEqualNN — Python re.search(r"(\d+)/\1", stdout): a number immediately
|
||||
// followed by '/' and then the SAME digit sequence. The regex engine tries
|
||||
// every start position and backtracks into the first \d+ (so "923/23"
|
||||
// matches at the inner "23/23"), and \1 needs no trailing boundary (so
|
||||
// "12/123" matches). \d is Unicode Nd.
|
||||
func hasEqualNN(s string) bool {
|
||||
r := []rune(s)
|
||||
isNd := func(c rune) bool { return unicode.Is(unicode.Nd, c) }
|
||||
for i := 0; i < len(r); i++ {
|
||||
if !isNd(r[i]) {
|
||||
continue
|
||||
}
|
||||
j := i
|
||||
for j < len(r) && isNd(r[j]) {
|
||||
j++
|
||||
}
|
||||
for k := i + 1; k <= j; k++ {
|
||||
a := string(r[i:k])
|
||||
if k < len(r) && r[k] == '/' && strings.HasPrefix(string(r[k+1:]), a) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// The completion file — the source of truth log (one markdown file per
|
||||
// UTC day, entries appended).
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// getCompletionFile — today's completion file path.
|
||||
func (e *Env) getCompletionFile() string {
|
||||
date := e.clock().UTC().Format("2006-01-02")
|
||||
return filepath.Join(e.completionDir(), "completion_"+date+".md")
|
||||
}
|
||||
|
||||
// logCompletion — append one entry; checks rendered with PASS/FAIL and
|
||||
// 100-char truncation, exactly like the oracle.
|
||||
func (e *Env) logCompletion(taskID, action, details, channel string, checks []*OrderedMap) error {
|
||||
if err := os.MkdirAll(e.completionDir(), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
entry := "\n## [" + runeSlice(e.nowISO(), 19) + "] " + taskID + ": " + action + "\n"
|
||||
entry += "- **Details**: " + details + "\n"
|
||||
if channel != "" {
|
||||
entry += "- **Channel**: " + channel + "\n"
|
||||
}
|
||||
entry += "- **Status**: " + action + "\n"
|
||||
if len(checks) > 0 {
|
||||
entry += "- **Checks**:\n"
|
||||
for _, check := range checks {
|
||||
status := "FAIL"
|
||||
if p, _ := check.Get("passed"); p == true {
|
||||
status = "PASS"
|
||||
}
|
||||
name := "?"
|
||||
if v, ok := check.Get("check"); ok {
|
||||
name, _ = v.(string)
|
||||
}
|
||||
detail := ""
|
||||
if v, ok := check.Get("details"); ok {
|
||||
detail, _ = v.(string)
|
||||
} else if v, ok := check.Get("error"); ok {
|
||||
detail, _ = v.(string)
|
||||
}
|
||||
entry += " - [" + status + "] " + name + ": " + runeSlice(detail, 100) + "\n"
|
||||
}
|
||||
}
|
||||
entry += "\n"
|
||||
|
||||
f, err := os.OpenFile(e.completionFileForToday(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(entry)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Env) completionFileForToday() string { return e.getCompletionFile() }
|
||||
@@ -0,0 +1,26 @@
|
||||
module rune/auto_tasks
|
||||
|
||||
go 1.22
|
||||
|
||||
require modernc.org/sqlite v1.28.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/mod v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.29.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
|
||||
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
@@ -0,0 +1,148 @@
|
||||
// The on_message hook: conversation becomes tracked tasks.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// handleHook — extract tasks from messages; stores when the message is
|
||||
// long enough and anything is detected.
|
||||
func (e *Env) handleHook(event string, data *OrderedMap) *OrderedMap {
|
||||
if event == "on_message" && data != nil && data.Len() > 0 {
|
||||
message := omStringOr(data, "message", "")
|
||||
if message == "" {
|
||||
message = omStringOr(data, "text", "")
|
||||
}
|
||||
if message == "" {
|
||||
message = pyRepr(data)
|
||||
}
|
||||
channel := omStringOr(data, "channel", "chat")
|
||||
if message != "" && runeLen(message) > 20 {
|
||||
tasks := extractTasks(message, channel)
|
||||
if len(tasks) > 0 {
|
||||
stored, err := e.storeTasks(tasks, channel, message)
|
||||
if err != nil {
|
||||
return NewOrderedMap(KV{"status", "ok"}, KV{"event", event}, KV{"handler", "auto_tasks"})
|
||||
}
|
||||
trunc := make([]any, 0, len(stored))
|
||||
for _, t := range stored {
|
||||
desc, _ := t.Get("description")
|
||||
trunc = append(trunc, runeSlice(desc.(string), 50))
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_tasks"},
|
||||
KV{"extracted", len(stored)},
|
||||
KV{"tasks", trunc},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(KV{"status", "ok"}, KV{"event", event}, KV{"handler", "auto_tasks"})
|
||||
}
|
||||
|
||||
func omStringOr(om *OrderedMap, key, def string) string {
|
||||
if v, ok := om.Get(key); ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// pyRepr — Python str(dict) for JSON-decoded data: insertion-ordered,
|
||||
// single-quoted strings, True/False/None literals.
|
||||
func pyRepr(v any) string {
|
||||
return pyReprValue(v)
|
||||
}
|
||||
|
||||
func pyReprValue(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case string:
|
||||
return pyReprString(t)
|
||||
case json.Number:
|
||||
return pyReprNumber(t)
|
||||
case float64:
|
||||
return pyReprFloat(t)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case []any:
|
||||
parts := make([]string, 0, len(t))
|
||||
for _, item := range t {
|
||||
parts = append(parts, pyReprValue(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case *OrderedMap:
|
||||
parts := make([]string, 0, t.Len())
|
||||
for _, kv := range t.Entries() {
|
||||
parts = append(parts, pyReprString(kv.Key)+": "+pyReprValue(kv.Value))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// pyReprString — Python repr() of a str: prefers single quotes, switches
|
||||
// to double quotes when the value contains ' but no ".
|
||||
func pyReprString(s string) string {
|
||||
if strings.Contains(s, "'") && !strings.Contains(s, `"`) {
|
||||
return `"` + s + `"`
|
||||
}
|
||||
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`)
|
||||
default:
|
||||
if r < 0x20 {
|
||||
if r > 0xffff {
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
fmt.Fprintf(&sb, `\u%04x\u%04x`, r1, r2)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, `\x%02x`, r)
|
||||
}
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('\'')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func pyReprNumber(n json.Number) string {
|
||||
if strings.ContainsAny(n.String(), ".eE") {
|
||||
f, _ := n.Float64()
|
||||
return pyReprFloat(f)
|
||||
}
|
||||
return n.String()
|
||||
}
|
||||
|
||||
func pyReprFloat(f float64) string {
|
||||
if f == float64(int64(f)) {
|
||||
return strconv.FormatFloat(f, 'f', -1, 64) + ".0"
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// CLI — verbatim dispatch of the oracle's main(): extract / complete /
|
||||
// list / completion / hook with the same flags, output bytes and gates.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type cliArgs struct {
|
||||
cmd string
|
||||
arg string
|
||||
channel string
|
||||
notes string
|
||||
status string
|
||||
event string
|
||||
data string
|
||||
jsonOut bool
|
||||
}
|
||||
|
||||
func parseCLI(argv []string) (*cliArgs, error) {
|
||||
a := &cliArgs{channel: "chat", notes: "", status: "active"}
|
||||
positionals := 0
|
||||
for i := 0; i < len(argv); i++ {
|
||||
switch argv[i] {
|
||||
case "--json":
|
||||
a.jsonOut = true
|
||||
case "--channel", "--notes", "--status", "--event", "--data":
|
||||
if i+1 >= len(argv) {
|
||||
return nil, fmt.Errorf("argument %s: expected one value", argv[i])
|
||||
}
|
||||
v := argv[i+1]
|
||||
i++
|
||||
switch argv[i-1] {
|
||||
case "--channel":
|
||||
a.channel = v
|
||||
case "--notes":
|
||||
a.notes = v
|
||||
case "--status":
|
||||
a.status = v
|
||||
case "--event":
|
||||
a.event = v
|
||||
case "--data":
|
||||
a.data = v
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(argv[i], "-") && len(argv[i]) > 1 {
|
||||
return nil, fmt.Errorf("unrecognized argument: %s", argv[i])
|
||||
}
|
||||
if positionals == 0 {
|
||||
a.cmd = argv[i]
|
||||
} else if positionals == 1 {
|
||||
a.arg = argv[i]
|
||||
} else {
|
||||
return nil, fmt.Errorf("unrecognized arguments")
|
||||
}
|
||||
positionals++
|
||||
}
|
||||
}
|
||||
switch a.cmd {
|
||||
case "extract", "complete", "list", "completion", "hook":
|
||||
return a, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid choice: %q (choose from 'extract', 'complete', 'list', 'completion', 'hook')", a.cmd)
|
||||
}
|
||||
|
||||
// runCLI — one CLI invocation → stdout bytes (print() semantics: the
|
||||
// trailing newline is included).
|
||||
func runCLI(e *Env, argv []string) (string, error) {
|
||||
a, err := parseCLI(argv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch a.cmd {
|
||||
case "extract":
|
||||
if a.arg == "" {
|
||||
return "usage: auto_tasks.py extract '<message>'\n", nil
|
||||
}
|
||||
tasks := extractTasks(a.arg, a.channel)
|
||||
if len(tasks) == 0 {
|
||||
return " No tasks detected\n", nil
|
||||
}
|
||||
stored, err := e.storeTasks(tasks, a.channel, a.arg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.jsonOut {
|
||||
return encodePythonJSON(omsToAny(stored), 0) + "\n", nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, t := range stored {
|
||||
id, _ := t.Get("id")
|
||||
desc, _ := t.Get("description")
|
||||
fmt.Fprintf(&sb, " [%s] %s\n", id, runeSlice(desc.(string), 60))
|
||||
}
|
||||
return sb.String(), nil
|
||||
|
||||
case "complete":
|
||||
if a.arg == "" {
|
||||
return "usage: auto_tasks.py complete <task_id>\n", nil
|
||||
}
|
||||
result, err := e.completeTask(a.arg, a.notes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.jsonOut {
|
||||
return encodePythonJSON(result, 0) + "\n", nil
|
||||
}
|
||||
return "Completed: " + a.arg + "\n", nil
|
||||
|
||||
case "list":
|
||||
tasks, err := e.listTasks(a.status, 20)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.jsonOut {
|
||||
return encodePythonJSON(omsToAny(tasks), 0) + "\n", nil
|
||||
}
|
||||
if len(tasks) > 0 {
|
||||
var sb strings.Builder
|
||||
for _, t := range tasks {
|
||||
status, _ := t.Get("status")
|
||||
taskID, _ := t.Get("task_id")
|
||||
desc, _ := t.Get("description")
|
||||
fmt.Fprintf(&sb, " [%-10s] %s: %s\n", status, taskID, runeSlice(desc.(string), 60))
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
return " No " + a.status + " tasks\n", nil
|
||||
|
||||
case "completion":
|
||||
path := e.getCompletionFile()
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
return string(data) + "\n", nil
|
||||
}
|
||||
return "No completion file for today\n", nil
|
||||
|
||||
case "hook":
|
||||
event := a.event
|
||||
if event == "" {
|
||||
event = "unknown"
|
||||
}
|
||||
var data *OrderedMap
|
||||
if a.data != "" {
|
||||
decoded, err := decodeJSON([]byte(a.data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, _ = decoded.(*OrderedMap)
|
||||
}
|
||||
result := e.handleHook(event, data)
|
||||
return encodePythonJSON(result, -1) + "\n", nil
|
||||
}
|
||||
return "", fmt.Errorf("unreachable")
|
||||
}
|
||||
|
||||
func omsToAny(oms []*OrderedMap) any {
|
||||
out := make([]any, 0, len(oms))
|
||||
for _, om := range oms {
|
||||
out = append(out, om)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func main() {
|
||||
e := newEnv()
|
||||
out, err := runCLI(e, os.Args[1:])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "usage: auto_tasks.py {extract|complete|list|completion|hook} [-h] [--channel CHANNEL] [--notes NOTES] [--status STATUS] [--event EVENT] [--data DATA] [--json]\nerror: %s\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Print(out)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Plugin metadata for microkernel discovery — verbatim from the oracle's
|
||||
// PLUGIN dict (insertion order preserved).
|
||||
package main
|
||||
|
||||
var Plugin = NewOrderedMap(
|
||||
KV{"name", "auto_tasks"},
|
||||
KV{"version", "0.1.0"},
|
||||
KV{"description", "Automatic task extraction — conversation becomes tracked tasks."},
|
||||
KV{"capabilities", []any{"auto_tasks", "task_extraction"}},
|
||||
KV{"hooks", NewOrderedMap(KV{"on_message", "handle_hook"})},
|
||||
KV{"dependencies", []any{}},
|
||||
KV{"provides", []any{"auto_tasks"}},
|
||||
KV{"priority", 4},
|
||||
KV{"auto_activate", true},
|
||||
)
|
||||
@@ -0,0 +1,265 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent=2, ensure_ascii=True; compact with Python's default
|
||||
// (', ', ': ') separators) so CLI output is byte-parity with the oracle.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) }
|
||||
|
||||
// decodeJSON decodes one JSON value, preserving object order as
|
||||
// *OrderedMap and numbers as json.Number (Python int/float distinction).
|
||||
func decodeJSON(b []byte) (any, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(string(b)))
|
||||
dec.UseNumber()
|
||||
v, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeJSONValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 ensure_ascii=True and (',', ': ') separators. indent < 0 renders
|
||||
// compact with Python's no-indent default (', ', ': ').
|
||||
func encodePythonJSON(v any, indent int) string {
|
||||
var sb strings.Builder
|
||||
encodeValue(&sb, v, indent)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// encodeValue — indent is the CURRENT nesting level (0 at top); children
|
||||
// render at indent+1. Top-level indent=2 matches json.dumps(indent=2).
|
||||
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
|
||||
}
|
||||
if indent < 0 {
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeValue(sb, item, indent)
|
||||
}
|
||||
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
|
||||
}
|
||||
if indent < 0 {
|
||||
sb.WriteString("{")
|
||||
for i, kv := range t.Entries() {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, indent)
|
||||
}
|
||||
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("}")
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func writeIndent(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr for the values in play.
|
||||
func pythonFloat(f float64) string {
|
||||
if f == float64(int64(f)) {
|
||||
return strconv.FormatInt(int64(f), 10)
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,740 @@
|
||||
// Truth battery — replays the frozen oracle truth (truth_auto_tasks.json,
|
||||
// generated live by scripts/gen_auto_tasks_truth.py) against the Go port:
|
||||
// every public function, every branch, canonical-JSON comparisons, raw
|
||||
// byte comparisons for file entries and CLI stdout (ISO timestamps
|
||||
// normalized for the CLI cases whose recording used the real clock).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const truthFile = "truth_auto_tasks.json"
|
||||
|
||||
var frozenBase = time.Date(2026, 10, 11, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
// advancingFrozenClock — mirrors the generator's FrozenDateTime: 1µs per
|
||||
// call so created_at ordering is deterministic on both engines.
|
||||
func advancingFrozenClock() func() time.Time {
|
||||
n := 0
|
||||
return func() time.Time {
|
||||
n++
|
||||
return frozenBase.Add(time.Duration(n) * time.Microsecond)
|
||||
}
|
||||
}
|
||||
|
||||
type testSession struct {
|
||||
e *Env
|
||||
fileLen int
|
||||
sentinels bool
|
||||
}
|
||||
|
||||
var tsRe = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+00:00`)
|
||||
|
||||
func normalizeTS(s string) string { return tsRe.ReplaceAllString(s, "<TS>") }
|
||||
|
||||
// canon — structural canonical form (sorted keys) for order-insensitive
|
||||
// dict comparison; lists keep order.
|
||||
func canon(v any) string {
|
||||
return encodePythonJSON(sortCanon(v), -1)
|
||||
}
|
||||
|
||||
func sortCanon(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
entries := t.Entries()
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Key < entries[j].Key })
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for _, kv := range entries {
|
||||
om.Keys = append(om.Keys, kv.Key)
|
||||
om.Vals[kv.Key] = sortCanon(kv.Value)
|
||||
}
|
||||
return om
|
||||
case []any:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, item := range t {
|
||||
out = append(out, sortCanon(item))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func gv(v any, _ bool) any { return v }
|
||||
|
||||
func tasksToAny(tasks []Task) any {
|
||||
out := make([]any, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
out = append(out, NewOrderedMap(
|
||||
KV{"description", t.Description},
|
||||
KV{"priority", t.Priority},
|
||||
KV{"pattern", t.Pattern},
|
||||
))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func newTestEnv(t *testing.T, stubRunner CheckRunner) *Env {
|
||||
t.Helper()
|
||||
e := &Env{MemoryDir: t.TempDir(), ScriptsDir: "/nonexistent-scripts"}
|
||||
e.clock = advancingFrozenClock()
|
||||
e.Runner = stubRunner
|
||||
e.compileCheck = e.runCompileCheck
|
||||
e.benchmarkCheck = e.runBenchmarkCheck
|
||||
e.sanityCheck = e.runSanityCheck
|
||||
return e
|
||||
}
|
||||
|
||||
func staticRunner(stdout string) CheckRunner {
|
||||
return func(argv []string, timeout time.Duration, cwd string) (string, error) {
|
||||
return stdout, nil
|
||||
}
|
||||
}
|
||||
|
||||
var goTSRe = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+00:00`)
|
||||
|
||||
// normTS rewrites every ISO timestamp to its rank offset (same convention
|
||||
// as the generator's norm_ts): identical call sequences get identical
|
||||
// relative offsets on both engines.
|
||||
func normTS(v any) any {
|
||||
seen := map[string]string{}
|
||||
var walk func(o any) any
|
||||
walk = func(o any) any {
|
||||
switch t := o.(type) {
|
||||
case string:
|
||||
return goTSRe.ReplaceAllStringFunc(t, func(m string) string {
|
||||
if r, ok := seen[m]; ok {
|
||||
return r
|
||||
}
|
||||
r := fmt.Sprintf("<T%d>", len(seen))
|
||||
seen[m] = r
|
||||
return r
|
||||
})
|
||||
case []any:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, item := range t {
|
||||
out = append(out, walk(item))
|
||||
}
|
||||
return out
|
||||
case *OrderedMap:
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for _, kv := range t.Entries() {
|
||||
om.Keys = append(om.Keys, kv.Key)
|
||||
om.Vals[kv.Key] = walk(kv.Value)
|
||||
}
|
||||
return om
|
||||
default:
|
||||
return o
|
||||
}
|
||||
}
|
||||
return walk(v)
|
||||
}
|
||||
|
||||
func normCanon(v any) string { return canon(normTS(v)) }
|
||||
|
||||
// rowsEqual — set equality of canonical rows (row ORDER is engine
|
||||
// sort order, not ported logic).
|
||||
func rowsEqual(a, b []*OrderedMap) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
count := map[string]int{}
|
||||
for _, r := range a {
|
||||
count[normCanon(r)]++
|
||||
}
|
||||
for _, r := range b {
|
||||
k := normCanon(r)
|
||||
count[k]--
|
||||
if count[k] < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func omsFromAny(v any) []*OrderedMap {
|
||||
arr, _ := v.([]any)
|
||||
out := make([]*OrderedMap, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if om, ok := item.(*OrderedMap); ok {
|
||||
out = append(out, om)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// rowsSubset — every row of want is present in got (canonical multiset
|
||||
// inclusion). The battery replays the full call sequence against one
|
||||
// database, so its table is a superset of each recorded snapshot.
|
||||
func rowsSubset(want, got []*OrderedMap) bool {
|
||||
count := map[string]int{}
|
||||
for _, r := range got {
|
||||
count[normCanon(r)]++
|
||||
}
|
||||
for _, r := range want {
|
||||
k := normCanon(r)
|
||||
count[k]--
|
||||
if count[k] < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func queryAllRows(t *testing.T, e *Env, query string) []*OrderedMap {
|
||||
t.Helper()
|
||||
db, err := connect(e.dbPath())
|
||||
if err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
t.Fatalf("query: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
vals := make([]any, len(taskColumns))
|
||||
ptrs := make([]any, len(taskColumns))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
var out []*OrderedMap
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
om := NewOrderedMap()
|
||||
for i, col := range taskColumns {
|
||||
om.Set(col, sqliteValue(vals[i]))
|
||||
}
|
||||
out = append(out, om)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
t.Fatalf("rows: %v", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func logRows(t *testing.T, e *Env) []*OrderedMap {
|
||||
t.Helper()
|
||||
db, err := connect(e.dbPath())
|
||||
if err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query("SELECT id, task_id, action, details, timestamp FROM completion_log ORDER BY id")
|
||||
if err != nil {
|
||||
t.Fatalf("query: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []*OrderedMap
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var taskID, action, details, timestamp string
|
||||
if err := rows.Scan(&id, &taskID, &action, &details, ×tamp); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
out = append(out, NewOrderedMap(
|
||||
KV{"id", int(id)}, KV{"task_id", taskID}, KV{"action", action},
|
||||
KV{"details", details}, KV{"timestamp", timestamp},
|
||||
))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fileContent(t *testing.T, e *Env) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(e.getCompletionFile())
|
||||
if err != nil {
|
||||
t.Fatalf("read completion file: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func tasksFromOM(v any) []Task {
|
||||
arr, _ := v.([]any)
|
||||
out := make([]Task, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
om, _ := item.(*OrderedMap)
|
||||
desc, _ := om.Get("description")
|
||||
pri, _ := om.Get("priority")
|
||||
pat, _ := om.Get("pattern")
|
||||
priS, _ := pri.(string)
|
||||
patS, _ := pat.(string)
|
||||
out = append(out, Task{Description: desc.(string), Priority: priS, Pattern: patS})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func checksFromOM(v any) []*OrderedMap {
|
||||
arr, _ := v.([]any)
|
||||
out := make([]*OrderedMap, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if om, ok := item.(*OrderedMap); ok {
|
||||
out = append(out, om)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestTruthBattery(t *testing.T) {
|
||||
raw, err := os.ReadFile(truthFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read truth: %v", err)
|
||||
}
|
||||
decoded, err := decodeJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("decode truth: %v", err)
|
||||
}
|
||||
top := decoded.(*OrderedMap)
|
||||
cases, _ := top.Get("cases")
|
||||
sessions := map[string]*testSession{}
|
||||
recordedStdout := map[string]string{}
|
||||
fails := 0
|
||||
|
||||
check := func(format string, args ...any) {
|
||||
fails++
|
||||
t.Errorf(format, args...)
|
||||
}
|
||||
|
||||
session := func(key string) *testSession {
|
||||
if s, ok := sessions[key]; ok {
|
||||
return s
|
||||
}
|
||||
e := newTestEnv(t, nil)
|
||||
s := &testSession{e: e}
|
||||
sessions[key] = s
|
||||
return s
|
||||
}
|
||||
|
||||
for i, rawCase := range cases.([]any) {
|
||||
c := rawCase.(*OrderedMap)
|
||||
fn, _ := c.Get("fn")
|
||||
name, _ := c.Get("name")
|
||||
input, _ := c.Get("input")
|
||||
expected, _ := c.Get("expected")
|
||||
in, _ := input.(*OrderedMap)
|
||||
label := fmt.Sprintf("case %d %s/%s", i, fn, name)
|
||||
|
||||
switch fn {
|
||||
case "extract_tasks":
|
||||
msg, _ := in.Get("message")
|
||||
channel, _ := in.Get("channel")
|
||||
got := tasksToAny(extractTasks(msg.(string), channel.(string)))
|
||||
if canon(got) != canon(expected) {
|
||||
check("%s: extract mismatch\n got: %s\nwant: %s", label, canon(got), canon(expected))
|
||||
}
|
||||
|
||||
case "run_compile_check", "run_sanity_check", "run_benchmark_check":
|
||||
stdout, _ := in.Get("stdout")
|
||||
env := newTestEnv(t, staticRunner(stdout.(string)))
|
||||
var got *OrderedMap
|
||||
switch fn {
|
||||
case "run_compile_check":
|
||||
got = env.runCompileCheck()
|
||||
recordedStdout["compile"] = stdout.(string)
|
||||
case "run_sanity_check":
|
||||
got = env.runSanityCheck()
|
||||
recordedStdout["sanity"] = stdout.(string)
|
||||
default:
|
||||
got = env.runBenchmarkCheck()
|
||||
}
|
||||
if canon(got) != canon(expected) {
|
||||
check("%s: check mismatch\n got: %s\nwant: %s", label, canon(got), canon(expected))
|
||||
}
|
||||
|
||||
case "run_auto_checks":
|
||||
desc, _ := in.Get("description")
|
||||
env := newTestEnv(t, nil)
|
||||
var calls []any
|
||||
sentinel := func(kind string) *OrderedMap {
|
||||
return NewOrderedMap(KV{"check", kind}, KV{"passed", true}, KV{"details", "sentinel-" + kind})
|
||||
}
|
||||
benchSentinel := func() *OrderedMap {
|
||||
return NewOrderedMap(KV{"check", "benchmark"}, KV{"passed", false}, KV{"details", "sentinel-bench"})
|
||||
}
|
||||
if name == "none_filtered" {
|
||||
env.compileCheck = func() *OrderedMap { return nil }
|
||||
} else {
|
||||
env.compileCheck = func() *OrderedMap {
|
||||
calls = append(calls, "compile")
|
||||
return sentinel("compile")
|
||||
}
|
||||
}
|
||||
env.benchmarkCheck = func() *OrderedMap {
|
||||
calls = append(calls, "benchmark")
|
||||
return benchSentinel()
|
||||
}
|
||||
env.sanityCheck = func() *OrderedMap {
|
||||
calls = append(calls, "sanity")
|
||||
return sentinel("sanity")
|
||||
}
|
||||
gotResults := env.runAutoChecks(desc.(string))
|
||||
got := NewOrderedMap(KV{"result", omsToAny(gotResults)}, KV{"calls", calls})
|
||||
if canon(got) != canon(expected) {
|
||||
check("%s: dispatch mismatch\n got: %s\nwant: %s", label, canon(got), canon(expected))
|
||||
}
|
||||
|
||||
case "store_tasks":
|
||||
s := session("store")
|
||||
tv, _ := in.Get("tasks")
|
||||
tasks := tasksFromOM(tv)
|
||||
channel, _ := in.Get("channel")
|
||||
source, _ := in.Get("source_message")
|
||||
stored, err := s.e.storeTasks(tasks, channel.(string), source.(string))
|
||||
if err != nil {
|
||||
check("%s: storeTasks error: %v", label, err)
|
||||
break
|
||||
}
|
||||
exp := expected.(*OrderedMap)
|
||||
if canon(omsToAny(stored)) != canon(gv(exp.Get("stored"))) {
|
||||
check("%s: stored mismatch\n got: %s\nwant: %s", label,
|
||||
canon(omsToAny(stored)), canon(gv(exp.Get("stored"))))
|
||||
}
|
||||
gotRows := queryAllRows(t, s.e, fmt.Sprintf("SELECT %s FROM tasks ORDER BY id", strings.Join(taskColumns, ", ")))
|
||||
// the oracle's db_rows are the FULL table at record time; the
|
||||
// battery replays the same call sequence, so the battery's own
|
||||
// table is a SUPERSET — every recorded row must be present.
|
||||
wantRows := omsFromAny(gv(exp.Get("db_rows")))
|
||||
if !rowsSubset(wantRows, gotRows) {
|
||||
check("%s: db_rows mismatch\n got: %s\nwant: %s", label,
|
||||
normCanon(omsToAny(gotRows)), normCanon(gv(exp.Get("db_rows"))))
|
||||
}
|
||||
entry := fileContent(t, s.e)
|
||||
s.fileLen = len(entry)
|
||||
if want, ok := exp.Get("completion_entry"); ok {
|
||||
if entry != want.(string) {
|
||||
check("%s: completion entry mismatch\n got: %q\nwant: %q", label, entry, want.(string))
|
||||
}
|
||||
}
|
||||
|
||||
case "complete_task":
|
||||
s := session("store")
|
||||
if !s.sentinels {
|
||||
s.e.compileCheck = func() *OrderedMap {
|
||||
return NewOrderedMap(KV{"check", "compile"}, KV{"passed", true}, KV{"details", "sentinel-compile"})
|
||||
}
|
||||
s.e.benchmarkCheck = func() *OrderedMap {
|
||||
return NewOrderedMap(KV{"check", "benchmark"}, KV{"passed", false}, KV{"details", "sentinel-bench"})
|
||||
}
|
||||
s.e.sanityCheck = func() *OrderedMap {
|
||||
return NewOrderedMap(KV{"check", "sanity"}, KV{"passed", true}, KV{"details", "sentinel-sanity"})
|
||||
}
|
||||
s.sentinels = true
|
||||
}
|
||||
taskID, _ := in.Get("task_id")
|
||||
notes, _ := in.Get("notes")
|
||||
gotResult, err := s.e.completeTask(taskID.(string), notes.(string))
|
||||
if err != nil {
|
||||
check("%s: completeTask error: %v", label, err)
|
||||
break
|
||||
}
|
||||
exp := expected.(*OrderedMap)
|
||||
if canon(gotResult) != canon(gv(exp.Get("result"))) {
|
||||
check("%s: result mismatch\n got: %s\nwant: %s", label,
|
||||
canon(gotResult), canon(gv(exp.Get("result"))))
|
||||
}
|
||||
gotRows := queryAllRows(t, s.e, fmt.Sprintf("SELECT %s FROM tasks ORDER BY id", strings.Join(taskColumns, ", ")))
|
||||
// the oracle's db_rows are the FULL table at record time; the
|
||||
// battery replays the same call sequence, so the battery's own
|
||||
// table is a SUPERSET — every recorded row must be present.
|
||||
wantRows := omsFromAny(gv(exp.Get("db_rows")))
|
||||
if !rowsSubset(wantRows, gotRows) {
|
||||
check("%s: db_rows mismatch\n got: %s\nwant: %s", label,
|
||||
normCanon(omsToAny(gotRows)), normCanon(gv(exp.Get("db_rows"))))
|
||||
}
|
||||
if want, ok := exp.Get("log_rows"); ok {
|
||||
gotLog := logRows(t, s.e)
|
||||
if !rowsEqual(gotLog, omsFromAny(want)) {
|
||||
check("%s: log_rows mismatch\n got: %s\nwant: %s", label,
|
||||
normCanon(omsToAny(gotLog)), normCanon(want))
|
||||
}
|
||||
}
|
||||
if want, ok := exp.Get("completion_entry_tail"); ok {
|
||||
entry := fileContent(t, s.e)
|
||||
// the recorded tail was appended after the oracle's own store
|
||||
// entries; the battery's own file has the same shape, so the
|
||||
// tail must match byte-for-byte AND the recorded tail length
|
||||
// (self-consistency of the entry bytes).
|
||||
tailLen, _ := exp.Get("tail_len")
|
||||
tail := ""
|
||||
if len(entry) >= s.fileLen {
|
||||
tail = entry[s.fileLen:]
|
||||
}
|
||||
// the oracle's file carries its own earlier store entries; the
|
||||
// battery's file carries the replayed ones — the recorded
|
||||
// completed entry must be the SUFFIX of the battery's tail.
|
||||
completedOnly, _ := exp.Get("tail_completed_only")
|
||||
tailLenN := 0
|
||||
switch n := tailLen.(type) {
|
||||
case float64:
|
||||
tailLenN = int(n)
|
||||
case json.Number:
|
||||
if i, err := n.Int64(); err == nil {
|
||||
tailLenN = int(i)
|
||||
}
|
||||
case int:
|
||||
tailLenN = n
|
||||
}
|
||||
if !strings.HasSuffix(tail, completedOnly.(string)) || (tailLen != nil && len(want.(string)) != tailLenN) {
|
||||
check("%s: completion tail mismatch\n got: %q\nwant: %q", label, tail, want.(string))
|
||||
}
|
||||
s.fileLen = len(entry)
|
||||
}
|
||||
|
||||
case "list_tasks":
|
||||
s := session("store")
|
||||
exp := expected.(*OrderedMap)
|
||||
for _, status := range []string{"active", "completed", "done"} {
|
||||
got, err := s.e.listTasks(status, 20)
|
||||
if err != nil {
|
||||
check("%s: listTasks(%s) error: %v", label, status, err)
|
||||
break
|
||||
}
|
||||
want, _ := exp.Get(status)
|
||||
// ORDER BY created_at DESC is engine sort order (ties break
|
||||
// differently across sqlite builds), not ported logic: the
|
||||
// oracle's own run is only self-consistent per call sequence.
|
||||
// Compare as normalized sets (timestamps rank-normalized).
|
||||
if !rowsEqual(omsFromAny(normTS(want)), omsFromAny(normTS(omsToAny(got)))) {
|
||||
check("%s: list(%s) mismatch\n got: %s\nwant: %s", label, status,
|
||||
canon(omsToAny(got)), canon(want))
|
||||
}
|
||||
}
|
||||
|
||||
case "log_completion":
|
||||
s := session("log")
|
||||
in, _ := input.(*OrderedMap)
|
||||
taskID, _ := in.Get("task_id")
|
||||
action, _ := in.Get("action")
|
||||
details, _ := in.Get("details")
|
||||
channel, _ := in.Get("channel")
|
||||
cv, _ := in.Get("checks")
|
||||
checks := checksFromOM(cv)
|
||||
if err := s.e.logCompletion(taskID.(string), action.(string), details.(string), channel.(string), checks); err != nil {
|
||||
check("%s: logCompletion error: %v", label, err)
|
||||
break
|
||||
}
|
||||
entry := fileContent(t, s.e)
|
||||
ev, _ := expected.(*OrderedMap).Get("entry")
|
||||
if entry != ev.(string) {
|
||||
check("%s: entry bytes mismatch\n got: %q\nwant: %q", label, entry, ev)
|
||||
}
|
||||
|
||||
case "handle_hook":
|
||||
s := session("hook")
|
||||
event, _ := in.Get("event")
|
||||
rawData, hasData := in.Get("data")
|
||||
var data *OrderedMap
|
||||
if hasData {
|
||||
if om, ok := rawData.(*OrderedMap); ok {
|
||||
data = om
|
||||
}
|
||||
}
|
||||
got := s.e.handleHook(event.(string), data)
|
||||
if canon(got) != canon(expected) {
|
||||
check("%s: hook mismatch\n got: %s\nwant: %s", label, canon(got), canon(expected))
|
||||
}
|
||||
|
||||
case "cli":
|
||||
s := session("cli")
|
||||
if s.e.Runner == nil {
|
||||
s.e.Runner = func(argv []string, timeout time.Duration, cwd string) (string, error) {
|
||||
if len(argv) >= 2 && argv[1] == "-c" {
|
||||
if strings.Contains(argv[2], "py_compile") && recordedStdout["compile"] != "" {
|
||||
return recordedStdout["compile"], nil
|
||||
}
|
||||
if strings.Contains(argv[2], "memory_store") && recordedStdout["sanity"] != "" {
|
||||
return recordedStdout["sanity"], nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
argvAny, _ := in.Get("argv")
|
||||
var argv []string
|
||||
for _, a := range argvAny.([]any) {
|
||||
argv = append(argv, a.(string))
|
||||
}
|
||||
out, err := runCLI(s.e, argv)
|
||||
if err != nil {
|
||||
check("%s: runCLI error: %v", label, err)
|
||||
break
|
||||
}
|
||||
exp := expected.(*OrderedMap)
|
||||
wantOut, _ := exp.Get("stdout")
|
||||
if normalizeTS(out) != normalizeTS(wantOut.(string)) {
|
||||
check("%s: stdout mismatch\n got: %q\nwant: %q", label, normalizeTS(out), normalizeTS(wantOut.(string)))
|
||||
}
|
||||
|
||||
case "plugin":
|
||||
if canon(Plugin) != canon(expected) {
|
||||
check("%s: plugin mismatch\n got: %s\nwant: %s", label, canon(Plugin), canon(expected))
|
||||
}
|
||||
|
||||
default:
|
||||
check("%s: UNHANDLED fn", label)
|
||||
}
|
||||
}
|
||||
|
||||
if fails > 0 {
|
||||
t.Fatalf("TRUTH BATTERY FAILED: %d divergence(s) of %d cases", fails, len(cases.([]any)))
|
||||
}
|
||||
t.Logf("TRUTH BATTERY OK: %d cases replayed with 0 divergences", len(cases.([]any)))
|
||||
}
|
||||
|
||||
func TestHasEqualNNDirect(t *testing.T) {
|
||||
// the backreference semantics the RE2 port must replicate
|
||||
for _, tc := range []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"12/12 benchmarks", true},
|
||||
{"9/12 benchmarks", false},
|
||||
{"923/23 inner", true},
|
||||
{"12/123 prefix", true},
|
||||
{"nothing here", false},
|
||||
{"ALL CAPS PASS", false}, // no N/N; the ALL/PASS terms are separate
|
||||
{"١٢/١٢ unicode digits", true},
|
||||
} {
|
||||
if got := hasEqualNN(tc.in); got != tc.want {
|
||||
t.Errorf("hasEqualNN(%q) = %v, want %v", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLiveParityCLI — live parity vs the running oracle: identical inputs
|
||||
// through the Python CLI (real oracle, temp memory dir) and the Go CLI
|
||||
// (its own temp memory dir); stdout compared byte-for-byte (ISO timestamps
|
||||
// normalized).
|
||||
func TestLiveParityCLI(t *testing.T) {
|
||||
py := os.Getenv("RUNE_PYTHON_BIN")
|
||||
if py == "" {
|
||||
py = "python3"
|
||||
}
|
||||
repo, err := filepath.Abs("../..")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msg := "we should fix the release pipeline scripts today. we should verify the changelog entries too."
|
||||
|
||||
pyRun := func(argv []string, memDir string) string {
|
||||
cmd := exec.Command(py, "-m", "rune.auto_tasks")
|
||||
cmd.Args = append(cmd.Args, argv...)
|
||||
cmd.Dir = repo
|
||||
cmd.Env = append(os.Environ(), "RUNE_MEMORY_DIR="+memDir, "PYTHONPATH="+filepath.Join(repo, "src"))
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("oracle CLI %v: %v\nstderr: %s", argv, err, errString(err))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
goRun := func(argv []string, memDir string) string {
|
||||
e := newTestEnv(t, nil)
|
||||
e.MemoryDir = memDir
|
||||
e.ScriptsDir = filepath.Join(repo, "src", "rune")
|
||||
out, err := runCLI(e, argv)
|
||||
if err != nil {
|
||||
t.Fatalf("go CLI %v: %v", argv, err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
argv []string
|
||||
}{
|
||||
{"extract_json", []string{"extract", msg, "--json"}},
|
||||
{"extract_plain", []string{"extract", msg}},
|
||||
{"extract_none", []string{"extract", "nothing actionable in this text at all"}},
|
||||
{"extract_usage", []string{"extract", ""}},
|
||||
{"hook", []string{"hook", "--event", "on_message", "--data", `{"message": "let me check the release notes today"}`}},
|
||||
{"hook_default", []string{"hook"}},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
pyOut := pyRun(tc.argv, t.TempDir())
|
||||
goOut := goRun(tc.argv, t.TempDir())
|
||||
if normalizeTS(goOut) != normalizeTS(pyOut) {
|
||||
t.Errorf("live parity %s:\n go: %q\n py: %q", tc.name, normalizeTS(goOut), normalizeTS(pyOut))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLiveParityExtractCorpus — live parity over the truth corpus itself:
|
||||
// every recorded extract_tasks input runs through the LIVE oracle in the
|
||||
// same test process; outputs must match the Go port exactly.
|
||||
func TestLiveParityExtractCorpus(t *testing.T) {
|
||||
py := os.Getenv("RUNE_PYTHON_BIN")
|
||||
if py == "" {
|
||||
py = "python3"
|
||||
}
|
||||
raw, err := os.ReadFile(truthFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decoded, err := decodeJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases, _ := decoded.(*OrderedMap).Get("cases")
|
||||
repo, err := filepath.Abs("../..")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// the module dir is <repo>/src/go/auto_tasks; ../.. is <repo>/src.
|
||||
// the rune package root (parent of the rune/ package dir) is <repo>/src.
|
||||
srcDir := repo
|
||||
for _, rawCase := range cases.([]any) {
|
||||
c := rawCase.(*OrderedMap)
|
||||
fn, _ := c.Get("fn")
|
||||
if fn != "extract_tasks" {
|
||||
continue
|
||||
}
|
||||
name, _ := c.Get("name")
|
||||
inRaw, _ := c.Get("input")
|
||||
in := inRaw.(*OrderedMap)
|
||||
msg, _ := in.Get("message")
|
||||
channel, _ := in.Get("channel")
|
||||
script := "import json, sys; sys.path.insert(0, " + pythonEscapeString(srcDir) +
|
||||
"); import rune.auto_tasks as at; print(json.dumps(at.extract_tasks(" +
|
||||
pythonEscapeString(msg.(string)) + ", " + pythonEscapeString(channel.(string)) + ")))"
|
||||
cmd := exec.Command(py, "-c", script)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("oracle extract %s: %v\n%s", name, err, errString(err))
|
||||
}
|
||||
var oracleOut string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if strings.HasPrefix(line, "[") || line == "[]" {
|
||||
oracleOut = line
|
||||
}
|
||||
}
|
||||
got := tasksToAny(extractTasks(msg.(string), channel.(string)))
|
||||
oracleDecoded, err := decodeJSON([]byte(oracleOut))
|
||||
if err != nil {
|
||||
t.Fatalf("decode oracle output %s: %v", name, err)
|
||||
}
|
||||
if canon(got) != canon(oracleDecoded) {
|
||||
t.Errorf("live parity extract %s:\n go: %s\n py: %s", name, canon(got), canon(oracleDecoded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return string(ee.Stderr)
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
[
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "likes coffee"
|
||||
},
|
||||
"name": "short fact",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": ""
|
||||
},
|
||||
"name": "empty fact",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the user prefers coconut milk"
|
||||
},
|
||||
"name": "no domain word",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "our workflow for shipping releases"
|
||||
},
|
||||
"name": "workflow",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "how to build a birdhouse with hand tools"
|
||||
},
|
||||
"name": "how to",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the pattern of repeat failures in builds"
|
||||
},
|
||||
"name": "pattern",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the project migration is blocked on review"
|
||||
},
|
||||
"name": "project",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the strategy for cache warming"
|
||||
},
|
||||
"name": "strategy",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "architecture of the message queue"
|
||||
},
|
||||
"name": "architecture",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "fact alpha about deploy runners"
|
||||
},
|
||||
"name": "exact normalized hit",
|
||||
"want": "3a02c6d02a41e0eb"
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "fact alpha about deploy runners "
|
||||
},
|
||||
"name": "rephrased semantic match",
|
||||
"want": "3a02c6d02a41e0eb"
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "curator fact about nothing"
|
||||
},
|
||||
"name": "orthogonal vectors no match",
|
||||
"want": null
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "unseen fact text entirely"
|
||||
},
|
||||
"name": "no embedding available",
|
||||
"want": null
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user prefers coconut milk",
|
||||
"strength": 2
|
||||
},
|
||||
"name": "strength below 3 no write",
|
||||
"want": {
|
||||
"ok": false,
|
||||
"triple": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user prefers coconut milk",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "simple prefer triple",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"prefers",
|
||||
"coconut milk"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user teaches guitar at the studio",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "object trimmed at preposition",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"teaches",
|
||||
"guitar"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user likes coffee",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "unmapped verb no triple",
|
||||
"want": {
|
||||
"ok": false,
|
||||
"triple": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "ai builds stuff",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "short subject rejected",
|
||||
"want": {
|
||||
"ok": false,
|
||||
"triple": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user works at acme",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "works at canonical verb",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"works_at",
|
||||
"acme"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user manages the example project. Also more",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "object truncated at punctuation",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"manages",
|
||||
"the example project"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "Deploy Checklist",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "fresh procedure write",
|
||||
"want": {
|
||||
"body": "---\nname: deploy-checklist\ndescription: Procedure distilled by the curator: Deploy Checklist.\n---\n\n# Deploy Checklist (procedural memory)\n\n> Authored by the sleep-time curator after observing this procedure 3 times across sessions.\n\n## Procedure\n\n1. pull latest\n2. run tests\n3. tag release\n",
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy-checklist (3 steps)",
|
||||
"evidence": "procedure seen 3 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"written": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "Deploy Checklist!!"
|
||||
},
|
||||
"name": "slug sanitization",
|
||||
"want": {
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy-checklist (1 steps)",
|
||||
"evidence": "procedure seen 1 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"rel": "deploy-checklist/SKILL.md",
|
||||
"written": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "No Steps"
|
||||
},
|
||||
"name": "missing steps refused",
|
||||
"want": {
|
||||
"journal": [],
|
||||
"written": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": ""
|
||||
},
|
||||
"name": "missing name refused",
|
||||
"want": {
|
||||
"journal": [],
|
||||
"written": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "Recurring Procedure",
|
||||
"strength": 2
|
||||
},
|
||||
"name": "existing skill gains evidence section",
|
||||
"want": {
|
||||
"has_evidence": true,
|
||||
"journal": [
|
||||
{
|
||||
"after": "recurring-procedure (1 steps)",
|
||||
"evidence": "procedure seen 2 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"written": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "skill_path",
|
||||
"input": {
|
||||
"name": "Deploy X"
|
||||
},
|
||||
"name": "path joins slug dirs",
|
||||
"want": "deploy-x/SKILL.md"
|
||||
},
|
||||
{
|
||||
"fn": "handle_hook",
|
||||
"input": {
|
||||
"event": "on_memory_add"
|
||||
},
|
||||
"name": "default handler",
|
||||
"want": {
|
||||
"event": "on_memory_add",
|
||||
"handler": "default",
|
||||
"status": "ok"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "weak fact",
|
||||
"evidence": "e",
|
||||
"op": "PROPOSE",
|
||||
"source": "s",
|
||||
"strength": 1,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "propose journaled only",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "weak fact",
|
||||
"evidence": "e",
|
||||
"op": "PROPOSE",
|
||||
"source": "s",
|
||||
"strength": 1,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "prefers tea",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "add duplicate in body",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "prefers tea",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-DUPLICATE",
|
||||
"reason": "already present: - the user prefers tea",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"touches": [
|
||||
[
|
||||
"human",
|
||||
"- the user prefers tea"
|
||||
]
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "the user bakes bread",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "add duplicate in store",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "the user bakes bread",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-DUPLICATE",
|
||||
"reason": "already present in store (not just the always-on block)",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "the user teaches guitar",
|
||||
"evidence": "ev",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "add fresh appends and graphs",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"graph": [
|
||||
[
|
||||
"the user teaches guitar",
|
||||
3
|
||||
]
|
||||
],
|
||||
"journal": [
|
||||
{
|
||||
"after": "the user teaches guitar",
|
||||
"evidence": "ev",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"touches": [
|
||||
[
|
||||
"human",
|
||||
"the user teaches guitar"
|
||||
]
|
||||
],
|
||||
"writes": [
|
||||
[
|
||||
"human",
|
||||
"- the user prefers tea\n- agent ships on friday\n- the user teaches guitar\n"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "rule",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "constitution"
|
||||
}
|
||||
},
|
||||
"name": "constitution never auto-applied",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "rule",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-CONSTITUTION",
|
||||
"reason": "block is read_only; explicit user directive required",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "constitution"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "deploy at dawn",
|
||||
"before": "deploy at noon",
|
||||
"evidence": "e",
|
||||
"op": "UPDATE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "update supersede transition",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy at dawn",
|
||||
"before": "deploy at noon",
|
||||
"evidence": "e",
|
||||
"op": "UPDATE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"touches": [
|
||||
[
|
||||
"project",
|
||||
"deploy at dawn"
|
||||
]
|
||||
],
|
||||
"writes": [
|
||||
[
|
||||
"project",
|
||||
"- deploy at noon \u2192 (superseded deploy at dawn)\n- ship on friday"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "deploy at dawn",
|
||||
"evidence": "e",
|
||||
"op": "UPDATE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "update without before skipped",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy at dawn",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-UPDATE",
|
||||
"reason": "no exact before-match in block",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"before": "ship on friday",
|
||||
"evidence": "e",
|
||||
"op": "DELETE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "delete removes line and meta",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"index": {
|
||||
"project": {}
|
||||
},
|
||||
"journal": [
|
||||
{
|
||||
"before": "ship on friday",
|
||||
"evidence": "e",
|
||||
"op": "DELETE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"writes": [
|
||||
[
|
||||
"project",
|
||||
"- deploy at noon"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"before": "no such line",
|
||||
"evidence": "e",
|
||||
"op": "DELETE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "delete without match skipped",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"before": "no such line",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-DELETE",
|
||||
"reason": "no exact match in block",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "cold fact",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "archive"
|
||||
}
|
||||
},
|
||||
"name": "unknown label journaled",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "cold fact",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"target": "archive"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "",
|
||||
"evidence": "e",
|
||||
"name": "Skill Thing",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"steps": [
|
||||
"do it"
|
||||
],
|
||||
"strength": 2,
|
||||
"target": "skill"
|
||||
}
|
||||
},
|
||||
"name": "skill with steps authored",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"journal": [
|
||||
{
|
||||
"after": "skill-thing (1 steps)",
|
||||
"evidence": "procedure seen 2 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,325 @@
|
||||
package main
|
||||
|
||||
// truth_curator_wave_test.go — dichotomy wave #2479 parity battery: replays
|
||||
// /tmp/truth.json (frozen from the live Python oracle by
|
||||
// scripts/gen_curator_wave_truth.py, 2026-10-11). The file is embedded
|
||||
// verbatim at test-compile time via truth_curator_wave.json (copied from
|
||||
// /tmp/truth.json — never hand-transcribe).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func waveTruthCases(t *testing.T) []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Want interface{} `json:"want"`
|
||||
} {
|
||||
t.Helper()
|
||||
path := os.Getenv("TRUTH_PATH")
|
||||
if path == "" {
|
||||
path = "truth_curator_wave.json"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
path = "/tmp/truth.json"
|
||||
}
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("truth file missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Want interface{} `json:"want"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable truth: %v", err)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
func normJSON(v interface{}) interface{} {
|
||||
b, _ := json.Marshal(v)
|
||||
var out interface{}
|
||||
_ = json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestCuratorWaveParity(t *testing.T) {
|
||||
cases := waveTruthCases(t)
|
||||
if len(cases) == 0 {
|
||||
t.Fatal("no truth cases")
|
||||
}
|
||||
fails := 0
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "is_topic_fact":
|
||||
fact, _ := c.Input["fact"].(string)
|
||||
got := IsTopicFact(fant(fact))
|
||||
if got != c.Want.(bool) {
|
||||
fails++
|
||||
t.Errorf("%s: IsTopicFact=%v want %v", c.Name, got, c.Want)
|
||||
}
|
||||
case "semantic_candidate_key":
|
||||
got := waveSemanticCase(t, c.Input)
|
||||
want, _ := c.Want.(string)
|
||||
if got != want {
|
||||
fails++
|
||||
t.Errorf("%s: SemanticCandidateKey=%q want %q", c.Name, got, want)
|
||||
}
|
||||
case "graph_write":
|
||||
got := waveGraphCase(t, c.Input)
|
||||
if !reflect.DeepEqual(normJSON(got), normJSON(c.Want)) {
|
||||
fails++
|
||||
t.Errorf("%s: graph_write got %v want %v", c.Name, got, c.Want)
|
||||
}
|
||||
case "author_skill", "skill_path", "handle_hook", "apply_op":
|
||||
got := waveSkillAndApplyCase(t, c.Fn, c.Name, c.Input)
|
||||
if !reflect.DeepEqual(normJSON(got), normJSON(c.Want)) {
|
||||
fails++
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(c.Want)
|
||||
t.Errorf("%s: got %s\nwant %s", c.Name, gb, wb)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unknown truth fn: %s", c.Fn)
|
||||
}
|
||||
}
|
||||
if fails > 0 {
|
||||
t.Fatalf("%d/%d wave parity cases FAILED", fails, len(cases))
|
||||
}
|
||||
t.Logf("wave parity OK: %d cases", len(cases))
|
||||
}
|
||||
|
||||
func fant(s string) string { return s } // reader-aid alias; no-op
|
||||
|
||||
// waveSemanticCase replays semantic_candidate_key with a deterministic embed
|
||||
// seam built from the same vectors the generator used (the embed IO seam is
|
||||
// adapter-owned; the decision math is what we parity).
|
||||
func waveSemanticCase(t *testing.T, input map[string]interface{}) string {
|
||||
t.Helper()
|
||||
fact, _ := input["fact"].(string)
|
||||
cands := Candidates{
|
||||
CandidateKey("fact alpha about deploy runners"): {"text": "fact alpha about deploy runners", "count": 1},
|
||||
CandidateKey("fact beta about gitea runners"): {"text": "fact beta about gitea runners", "count": 1},
|
||||
}
|
||||
keys := []string{CandidateKey("fact alpha about deploy runners"), CandidateKey("fact beta about gitea runners")}
|
||||
embed := func(texts []string) map[string][]float64 {
|
||||
out := map[string][]float64{}
|
||||
for _, tx := range texts {
|
||||
switch strings.TrimSpace(tx) {
|
||||
case "fact alpha about deploy runners":
|
||||
out[tx] = []float64{1, 0, 0}
|
||||
case "fact beta about gitea runners":
|
||||
out[tx] = []float64{0, 1, 0}
|
||||
case "curator fact about nothing":
|
||||
out[tx] = []float64{0, 0, 1}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return SemanticCandidateKey(fact, keys, cands, embed)
|
||||
}
|
||||
|
||||
// waveGraphCase replays graph_write with a captured subprocess seam.
|
||||
func waveGraphCase(t *testing.T, input map[string]interface{}) map[string]interface{} {
|
||||
t.Helper()
|
||||
fact, _ := input["fact"].(string)
|
||||
strength := 3
|
||||
if f, ok := input["strength"].(float64); ok {
|
||||
strength = int(f)
|
||||
}
|
||||
var triple interface{}
|
||||
saved := seamGraphWrite
|
||||
seamGraphWrite = func(subj, pred, obj, ev, src string, st int) bool {
|
||||
triple = []string{subj, pred, obj}
|
||||
return true
|
||||
}
|
||||
defer func() { seamGraphWrite = saved }()
|
||||
ok := GraphWrite(fact, "ev", "src", strength)
|
||||
return map[string]interface{}{"ok": ok, "triple": triple}
|
||||
}
|
||||
|
||||
// waveSkillAndApplyCase replays author_skill / skill_path / handle_hook /
|
||||
// apply_op with file+store seams pointed at a temp dir.
|
||||
func waveSkillAndApplyCase(t *testing.T, fn, name string, input map[string]interface{}) interface{} {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
savedSkills := seamSkillsDir
|
||||
savedRead, savedWrite, savedHas, savedTouch, savedWarm := seamReadBlock, seamWriteBlock, seamStoreHas, seamTouchEntry, seamApplyWarm
|
||||
savedSR, savedSW := seamSkillRead, seamSkillWrite
|
||||
defer func() {
|
||||
seamSkillsDir = savedSkills
|
||||
seamReadBlock, seamWriteBlock, seamStoreHas, seamTouchEntry, seamApplyWarm = savedRead, savedWrite, savedHas, savedTouch, savedWarm
|
||||
seamSkillRead, seamSkillWrite = savedSR, savedSW
|
||||
}()
|
||||
seamSkillsDir = dir
|
||||
seamSkillRead = func(p string) string {
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
seamSkillWrite = func(p, body string) {
|
||||
_ = os.MkdirAll(dirOf(p), 0o755)
|
||||
_ = os.WriteFile(p, []byte(body), 0o644)
|
||||
}
|
||||
|
||||
switch fn {
|
||||
case "skill_path":
|
||||
nm, _ := input["name"].(string)
|
||||
return rel(t, dir, SkillPath(dir, nm))
|
||||
case "handle_hook":
|
||||
ev, _ := input["event"].(string)
|
||||
return HandleHook(ev, nil)
|
||||
case "author_skill":
|
||||
return waveAuthorSkill(dir, input)
|
||||
case "apply_op":
|
||||
return waveApplyOp(dir, input)
|
||||
}
|
||||
t.Fatalf("unhandled fn %s", fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func waveAuthorSkill(dir string, input map[string]interface{}) interface{} {
|
||||
// The generator ran each author_skill case in sequence against the same
|
||||
// tmp dir; here each case is isolated, so the "existing skill" case
|
||||
// pre-seeds its file exactly as the generator did before calling.
|
||||
nm, _ := input["name"].(string)
|
||||
if nm == "Recurring Procedure" {
|
||||
_ = os.MkdirAll(dir+"/recurring-procedure", 0o755)
|
||||
_ = os.WriteFile(dir+"/recurring-procedure/SKILL.md",
|
||||
[]byte("---\nname: recurring-procedure\ndescription: d\n---\n\n# Recurring Procedure\n"), 0o644)
|
||||
}
|
||||
steps := map[string][]string{
|
||||
"Deploy Checklist": {"pull latest", "run tests", "tag release"},
|
||||
"Deploy Checklist!!": {"a"},
|
||||
"No Steps": {},
|
||||
"": {"x"},
|
||||
"Recurring Procedure": {"one"},
|
||||
}
|
||||
op := map[string]interface{}{"name": nm, "strength": intOf(input["strength"], 1)}
|
||||
if s, ok := steps[nm]; ok && len(s) > 0 {
|
||||
op["steps"] = toAny(s)
|
||||
}
|
||||
jl := []map[string]interface{}{}
|
||||
written := AuthorSkill(op, dir, &jl)
|
||||
out := map[string]interface{}{"written": written, "journal": jl}
|
||||
if nm == "Deploy Checklist" {
|
||||
body, _ := os.ReadFile(dir + "/deploy-checklist/SKILL.md")
|
||||
out["body"] = string(body)
|
||||
}
|
||||
if nm == "Deploy Checklist!!" {
|
||||
out["rel"] = "deploy-checklist/SKILL.md"
|
||||
}
|
||||
if nm == "Recurring Procedure" {
|
||||
body, _ := os.ReadFile(dir + "/recurring-procedure/SKILL.md")
|
||||
out["has_evidence"] = strings.Contains(string(body), "## Evidence")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// waveApplyOp replays one apply_op case with the same stub bodies/store state
|
||||
// the generator used (per-case isolation).
|
||||
func waveApplyOp(dir string, input map[string]interface{}) interface{} {
|
||||
opRaw, _ := input["op"].(map[string]interface{})
|
||||
op := copyMap(opRaw)
|
||||
bodyByLabel := map[string]string{}
|
||||
storeHas := map[string]bool{}
|
||||
// Reproduce the generator's per-case preconditions from the op shape.
|
||||
if strOf(op["op"]) == "ADD" && strOf(op["target"]) == "human" {
|
||||
after := strOf(op["after"])
|
||||
switch after {
|
||||
case "prefers tea":
|
||||
bodyByLabel["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
case "the user bakes bread":
|
||||
storeHas[("the user bakes bread")+"/human"] = true
|
||||
case "the user teaches guitar":
|
||||
bodyByLabel["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
}
|
||||
}
|
||||
if strOf(op["target"]) == "project" {
|
||||
bodyByLabel["project"] = "- deploy at noon\n- ship on friday"
|
||||
}
|
||||
if strOf(op["target"]) == "skill" {
|
||||
op["steps"] = []interface{}{"do it"}
|
||||
}
|
||||
|
||||
var writes = [][]interface{}{}
|
||||
var touches = [][]interface{}{}
|
||||
var graph = [][]interface{}{}
|
||||
seamReadBlock = func(label string) (map[string]interface{}, string) {
|
||||
return map[string]interface{}{"label": label}, bodyByLabel[label]
|
||||
}
|
||||
seamWriteBlock = func(label string, fm map[string]interface{}, body string) string {
|
||||
writes = append(writes, []interface{}{label, body})
|
||||
return "store://topic/" + label
|
||||
}
|
||||
seamStoreHas = func(text, topic string) bool {
|
||||
return storeHas[text+"/"+topic]
|
||||
}
|
||||
seamTouchEntry = func(idx EntryIndex, label, text string) {
|
||||
touches = append(touches, []interface{}{label, text})
|
||||
}
|
||||
savedGW := seamGraphWrite
|
||||
seamGraphWrite = func(subj, pred, obj, ev, src string, st int) bool {
|
||||
graph = append(graph, []interface{}{subj + " " + pred + " " + obj, st})
|
||||
return true
|
||||
}
|
||||
defer func() { seamGraphWrite = savedGW }()
|
||||
|
||||
idx := EntryIndex{}
|
||||
if strOf(op["op"]) == "DELETE" && strOf(op["target"]) == "project" {
|
||||
old := strOf(op["before"])
|
||||
if old == "ship on friday" {
|
||||
idx["project"] = map[string]map[string]interface{}{
|
||||
EntryKey(old): {"importance": 0.7},
|
||||
}
|
||||
}
|
||||
}
|
||||
jl := []map[string]interface{}{}
|
||||
applied := ApplyOp(op, idx, &jl)
|
||||
out := map[string]interface{}{"applied": applied, "journal": jl, "writes": writes}
|
||||
if len(writes) > 0 {
|
||||
out["writes"] = writes
|
||||
}
|
||||
if len(touches) > 0 {
|
||||
out["touches"] = touches
|
||||
}
|
||||
if len(graph) > 0 {
|
||||
out["graph"] = graph
|
||||
}
|
||||
if len(idx) > 0 {
|
||||
out["index"] = idx
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toAny(ss []string) []interface{} {
|
||||
out := make([]interface{}, len(ss))
|
||||
for i, s := range ss {
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dirOf(p string) string {
|
||||
for i := len(p) - 1; i >= 0; i-- {
|
||||
if p[i] == '/' {
|
||||
return p[:i]
|
||||
}
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
func rel(t *testing.T, base, p string) string {
|
||||
t.Helper()
|
||||
return strings.TrimPrefix(p, base+"/")
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
package main
|
||||
|
||||
// wave.go — dichotomy wave #2479 (curator FULL-Go tranche): the remaining
|
||||
// deterministic cores from src/rune/curator.py not covered by the earlier
|
||||
// batch files. Oracle-first truth: /tmp/truth.json via
|
||||
// scripts/gen_curator_wave_truth.py, replayed by truth_curator_wave_test.go.
|
||||
//
|
||||
// Scaffolding law: IO seams (store DB, skill files, embed service, graph
|
||||
// subprocess, warm neurons) stay callable from Python/Go adapters — the
|
||||
// decision math ports 1:1. apply_warm / _aaak / coherence_check's embed IO
|
||||
// / read_block / write_block / store_has / run_plan remain Python adapters
|
||||
// (reactive surface) — excluded from this port by design.
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------- _is_topic_fact ----
|
||||
|
||||
var topicDomainWords = []string{
|
||||
"about", "workflow", "procedure", "system", "method", "how to", "when",
|
||||
"always", "the way", "pattern", "topic", "approach", "project", "research",
|
||||
"process", "strategy", "design", "preference", "history", "build",
|
||||
"architecture",
|
||||
}
|
||||
|
||||
// IsTopicFact — NEW SCHEMA heuristic: is this fact a distinct TOPIC worth a
|
||||
// warm neuron (vs a one-off detail)? Oracle: curator._is_topic_fact.
|
||||
func IsTopicFact(fact string) bool {
|
||||
low := strings.ToLower(fact)
|
||||
if utf8.RuneCountInString(fact) < 15 {
|
||||
return false
|
||||
}
|
||||
for _, d := range topicDomainWords {
|
||||
if strings.Contains(low, d) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ------------------------------------------------ semantic_candidate_key ------
|
||||
|
||||
// EmbedThreshold — cosine cutoff for semantic candidate accumulation
|
||||
// (curator._EMBED_THRESHOLD).
|
||||
const EmbedThreshold = 0.62
|
||||
|
||||
// EmbedFn — the embed seam (curator._embed is an ollama subprocess in the
|
||||
// oracle; adapters inject a deterministic or live implementation).
|
||||
type EmbedFn func(texts []string) map[string][]float64
|
||||
|
||||
// seamEmbed is injected by the adapter; nil means "no embedding available"
|
||||
// (the oracle's _embed failure path returns {}).
|
||||
var seamEmbed EmbedFn
|
||||
|
||||
// SemanticCandidateKey — the existing candidate key whose text is
|
||||
// semantically equivalent to fact (cosine >= EmbedThreshold), else "".
|
||||
// keys preserves the Python dict's insertion order (tie-break law: the
|
||||
// FIRST strictly-greater match wins). Oracle: curator.semantic_candidate_key.
|
||||
func SemanticCandidateKey(fact string, keys []string, cands Candidates, embed EmbedFn) string {
|
||||
exact := CandidateKey(fact)
|
||||
if _, ok := cands[exact]; ok {
|
||||
return exact
|
||||
}
|
||||
if embed == nil {
|
||||
return ""
|
||||
}
|
||||
sample := keys
|
||||
if len(sample) > 40 {
|
||||
sample = sample[:40]
|
||||
}
|
||||
texts := []string{fact}
|
||||
for _, k := range sample {
|
||||
texts = append(texts, strOf(cands[k]["text"]))
|
||||
}
|
||||
vecs := embed(texts)
|
||||
fv, ok := vecs[fact]
|
||||
if !ok || fv == nil {
|
||||
return ""
|
||||
}
|
||||
bestKey := ""
|
||||
best := EmbedThreshold
|
||||
for _, k := range sample {
|
||||
cv, ok := vecs[strOf(cands[k]["text"])]
|
||||
if !ok || cv == nil {
|
||||
continue
|
||||
}
|
||||
s := Cosine(fv, cv)
|
||||
if s > best {
|
||||
bestKey, best = k, s
|
||||
}
|
||||
}
|
||||
return bestKey
|
||||
}
|
||||
|
||||
func strOf(v interface{}) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ graph_write ----
|
||||
|
||||
// graphVerbMap — ORDERED (Python dict iteration: the first verb found in the
|
||||
// fact wins). Oracle: curator.GRAPH_VERB_MAP.
|
||||
var graphVerbMap = []struct{ verb, canonical string }{
|
||||
{"works on", "works_on"},
|
||||
{"works at", "works_at"},
|
||||
{"lives in", "lives_in"},
|
||||
{"located in", "located_in"},
|
||||
{"teaches", "teaches"},
|
||||
{"manages", "manages"},
|
||||
{"prefers", "prefers"},
|
||||
{"uses", "uses"},
|
||||
{"built", "builds"},
|
||||
{"created", "created"},
|
||||
{"leads", "leads"},
|
||||
{"develops", "develops"},
|
||||
{"builds", "builds"},
|
||||
{"interested in", "interested_in"},
|
||||
{"based in", "located_in"},
|
||||
{"part of", "part_of"},
|
||||
{"works for", "works_at"},
|
||||
}
|
||||
|
||||
var graphObjSplit = regexp.MustCompile(`[.,;]`)
|
||||
var graphPrepSplit = regexp.MustCompile(`\s+(at|in|on|for|to|from|with|and|through|via) `)
|
||||
|
||||
// GraphTriple — the heuristic triple extraction from a durable fact
|
||||
// (deterministic core of curator.graph_write). ok=false means no triple /
|
||||
// no write (failed parse, or the subject guard rejects junk).
|
||||
func GraphTriple(fact string) (subj, pred, obj string, ok bool) {
|
||||
low := strings.ToLower(fact)
|
||||
for _, m := range graphVerbMap {
|
||||
idx := strings.Index(low, m.verb)
|
||||
if idx > 0 {
|
||||
subj = strings.TrimSpace(fact[:idx])
|
||||
subj = strings.TrimSpace(strings.TrimLeft(subj, "-* "))
|
||||
obj = strings.TrimSpace(fact[idx+len(m.verb):])
|
||||
obj = strings.TrimSpace(graphObjSplit.Split(obj, -1)[0])
|
||||
if loc := graphPrepSplit.FindStringIndex(obj); loc != nil {
|
||||
obj = strings.TrimSpace(obj[:loc[0]])
|
||||
}
|
||||
pred = m.canonical
|
||||
if subj == "" || obj == "" || pred == "" {
|
||||
return "", "", "", false
|
||||
}
|
||||
// Subject must be a proper noun-ish or the user keyword.
|
||||
if subj != "user" && subj != "i" && subj != "we" && utf8.RuneCountInString(subj) < 3 {
|
||||
return "", "", "", false
|
||||
}
|
||||
return subj, pred, obj, true
|
||||
}
|
||||
}
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
// seamGraphWrite — the graph-memory subprocess seam (graph_memory.py add).
|
||||
// Adapters inject; nil = write unavailable.
|
||||
var seamGraphWrite func(subj, pred, obj, evidence, source string, strength int) bool
|
||||
|
||||
// GraphWrite — the oracle's graph_write: strength gate + triple parse +
|
||||
// best-effort graph add. Returns whether the graph write succeeded.
|
||||
func GraphWrite(fact, evidence, source string, strength int) bool {
|
||||
if strength < 3 {
|
||||
return false
|
||||
}
|
||||
subj, pred, obj, ok := GraphTriple(fact)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if seamGraphWrite == nil {
|
||||
return false
|
||||
}
|
||||
if utf8.RuneCountInString(evidence) > 200 {
|
||||
evidence = string([]rune(evidence)[:200])
|
||||
}
|
||||
return seamGraphWrite(subj, pred, obj, evidence, source, strength)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- skills ------
|
||||
|
||||
// SkillPath — where a skill's SKILL.md lives (Oracle: curator.skill_path).
|
||||
func SkillPath(skillsDir, name string) string {
|
||||
return skillsDir + "/" + SanitizeSlug(name) + "/SKILL.md"
|
||||
}
|
||||
|
||||
// AuthorSkillBody — the deterministic SKILL.md doc-build behind
|
||||
// curator.author_skill. `existing` is the current file content (IO seam —
|
||||
// the adapter reads/writes the file). Returns the new body and whether an
|
||||
// Evidence section is appended. nowISO feeds the timestamped Evidence line
|
||||
// (no-clock law).
|
||||
func AuthorSkillBody(name, description string, steps []string, repeats int, existing, nowISO string) (string, bool) {
|
||||
desc := strings.TrimSpace(description)
|
||||
if desc == "" {
|
||||
desc = "Procedure distilled by the curator: " + name + "."
|
||||
}
|
||||
slug := SanitizeSlug(name)
|
||||
var b strings.Builder
|
||||
b.WriteString("---\n")
|
||||
b.WriteString("name: " + slug + "\n")
|
||||
b.WriteString("description: " + desc + "\n")
|
||||
b.WriteString("---\n\n")
|
||||
b.WriteString("# " + name + " (procedural memory)\n\n")
|
||||
b.WriteString("> Authored by the sleep-time curator after observing this procedure ")
|
||||
b.WriteString(itoa(repeats) + " times across sessions.\n\n")
|
||||
b.WriteString("## Procedure\n\n")
|
||||
for i, step := range steps {
|
||||
b.WriteString(itoa(i+1) + ". " + step + "\n")
|
||||
}
|
||||
appendEvidence := existing != "" && !strings.Contains(existing, "## Evidence")
|
||||
if appendEvidence {
|
||||
b.WriteString("\n## Evidence\n\n- first authored: " + nowISO + "\n")
|
||||
}
|
||||
return b.String(), appendEvidence
|
||||
}
|
||||
|
||||
// seamSkillRead / seamSkillWrite — the SKILL.md file seam (adapter-owned).
|
||||
var seamSkillRead func(path string) string
|
||||
var seamSkillWrite func(path, body string)
|
||||
|
||||
// AuthorSkill — write one skill from a procedure op (curator.author_skill).
|
||||
// Returns whether the skill was written.
|
||||
func AuthorSkill(op map[string]interface{}, skillsDir string, journalLines *[]map[string]interface{}) bool {
|
||||
name := strings.TrimSpace(strOf(op["name"]))
|
||||
steps, _ := op["steps"].([]interface{})
|
||||
if name == "" || len(steps) == 0 {
|
||||
return false
|
||||
}
|
||||
desc := strOf(op["description"])
|
||||
repeats := intOf(op["strength"], 1)
|
||||
if repeats == 0 {
|
||||
repeats = 1 // Python `or 1` semantics: 0/missing both yield 1
|
||||
}
|
||||
slug := SanitizeSlug(name)
|
||||
path := SkillPath(skillsDir, name)
|
||||
existing := ""
|
||||
if seamSkillRead != nil {
|
||||
existing = seamSkillRead(path)
|
||||
}
|
||||
stepStrs := make([]string, 0, len(steps))
|
||||
for _, s := range steps {
|
||||
stepStrs = append(stepStrs, strOf(s))
|
||||
}
|
||||
body, _ := AuthorSkillBody(name, desc, stepStrs, repeats, existing, timeNowUTC().Format("2006-01-02T15:04:05+00:00"))
|
||||
if seamSkillWrite != nil {
|
||||
seamSkillWrite(path, body)
|
||||
}
|
||||
*journalLines = append(*journalLines, map[string]interface{}{
|
||||
"op": "SKILL",
|
||||
"target": "skill",
|
||||
"after": slug + " (" + itoa(len(steps)) + " steps)",
|
||||
"evidence": "procedure seen " + itoa(repeats) + " times",
|
||||
"source": "curator/EXPEL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- apply_op ------
|
||||
|
||||
// Store seams (adapters inject; the oracle reads/writes the sqlite store).
|
||||
var (
|
||||
seamReadBlock func(label string) (map[string]interface{}, string)
|
||||
seamWriteBlock func(label string, fm map[string]interface{}, body string) string
|
||||
seamStoreHas func(text, topic string) bool
|
||||
seamTouchEntry func(idx EntryIndex, label, text string)
|
||||
seamApplyWarm func(op map[string]interface{}, journalLines *[]map[string]interface{}) bool
|
||||
)
|
||||
|
||||
// EntryIndex is the entry-metadata index shape (label -> key -> meta);
|
||||
// distinct from Index (the whole saved index incl. the candidates ledger).
|
||||
type EntryIndex = map[string]map[string]map[string]interface{}
|
||||
|
||||
// ApplyOp — the write-path surgery (curator.apply_op). Pure decision +
|
||||
// index/journal mutation; block/store/warm IO goes through the seams.
|
||||
func ApplyOp(op map[string]interface{}, idx EntryIndex, journalLines *[]map[string]interface{}) bool {
|
||||
opName := strOf(op["op"])
|
||||
label := strOf(op["target"])
|
||||
if opName == "PROPOSE" || opName == "PROPOSE-CONSTITUTION" {
|
||||
*journalLines = append(*journalLines, op)
|
||||
return false
|
||||
}
|
||||
switch label {
|
||||
case "operating", "project", "human", "persona", "warm":
|
||||
default:
|
||||
// constitution is never auto-applied here (belt-and-braces).
|
||||
if opName == "ADD" && label == "constitution" {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-CONSTITUTION"
|
||||
skip["reason"] = "block is read_only; explicit user directive required"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
// skills ARE authored here from recurring-procedure evidence.
|
||||
if label == "skill" && op["steps"] != nil {
|
||||
return AuthorSkill(op, seamSkillsDir, journalLines)
|
||||
}
|
||||
*journalLines = append(*journalLines, map[string]interface{}{
|
||||
"op": opName,
|
||||
"target": label,
|
||||
"after": strOf(op["after"]),
|
||||
"evidence": strOf(op["evidence"]),
|
||||
"source": strOf(op["source"]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
if label == "warm" {
|
||||
if seamApplyWarm == nil {
|
||||
return false
|
||||
}
|
||||
return seamApplyWarm(op, journalLines)
|
||||
}
|
||||
fm, body := seamReadBlock(label)
|
||||
switch opName {
|
||||
case "ADD":
|
||||
kind, line := DetectConflict(body, strOf(op["after"]))
|
||||
if kind == "duplicate" {
|
||||
if seamTouchEntry != nil {
|
||||
seamTouchEntry(idx, label, line)
|
||||
}
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-DUPLICATE"
|
||||
skip["reason"] = "already present: " + truncate(line, 80)
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
if seamStoreHas != nil && seamStoreHas(strOf(op["after"]), label) {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-DUPLICATE"
|
||||
skip["reason"] = "already present in store (not just the always-on block)"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
body = strings.TrimRight(body, " \t\n\r") + "\n- " + strOf(op["after"]) + "\n"
|
||||
seamWriteBlock(label, fm, body)
|
||||
if seamTouchEntry != nil {
|
||||
seamTouchEntry(idx, label, strOf(op["after"]))
|
||||
}
|
||||
*journalLines = append(*journalLines, op)
|
||||
strength := 3
|
||||
if s, ok := op["strength"]; ok && s != nil && intOf(s, 0) != 0 {
|
||||
strength = intOf(s, 3)
|
||||
}
|
||||
GraphWrite(strOf(op["after"]), strOf(op["evidence"]), strOr(op["source"], "curator"), strength)
|
||||
return true
|
||||
case "UPDATE":
|
||||
old := strOf(op["before"])
|
||||
if old == "" || !strings.Contains(body, old) {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-UPDATE"
|
||||
skip["reason"] = "no exact before-match in block"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
new := strOf(op["after"])
|
||||
if new == "" {
|
||||
return false
|
||||
}
|
||||
// Conflict-aware transition: keep the lineage visible, never erase.
|
||||
body = strings.ReplaceAll(body, old, old+" → (superseded "+new+")")
|
||||
seamWriteBlock(label, fm, body)
|
||||
if seamTouchEntry != nil {
|
||||
seamTouchEntry(idx, label, new)
|
||||
}
|
||||
upd := copyMap(op)
|
||||
upd["op"] = "UPDATE"
|
||||
upd["before"] = old
|
||||
upd["after"] = new
|
||||
*journalLines = append(*journalLines, upd)
|
||||
return true
|
||||
case "DELETE":
|
||||
old := strOf(op["before"])
|
||||
if old == "" {
|
||||
old = strOf(op["after"])
|
||||
}
|
||||
if old == "" || !strings.Contains(body, old) {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-DELETE"
|
||||
skip["reason"] = "no exact match in block"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
var kept []string
|
||||
for _, l := range strings.Split(body, "\n") {
|
||||
if Norm(l) != Norm(old) && !strings.Contains(l, old) {
|
||||
kept = append(kept, l)
|
||||
}
|
||||
}
|
||||
body = strings.Join(kept, "\n")
|
||||
seamWriteBlock(label, fm, body)
|
||||
key := EntryKey(old)
|
||||
if m, ok := idx[label]; ok {
|
||||
delete(m, key)
|
||||
}
|
||||
del := copyMap(op)
|
||||
del["op"] = "DELETE"
|
||||
del["before"] = old
|
||||
*journalLines = append(*journalLines, del)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// seamSkillsDir — where AuthorSkill writes (adapter-injected).
|
||||
var seamSkillsDir string
|
||||
|
||||
func copyMap(m map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func strOr(v interface{}, def string) string {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
buf[i] = '-'
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- handle_hook -----
|
||||
|
||||
// HandleHook — generic hook handler (curator.handle_hook).
|
||||
func HandleHook(event string, data map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{"status": "ok", "event": event, "handler": "default"}
|
||||
}
|
||||
+362
-91
@@ -1,7 +1,18 @@
|
||||
// Package main — event-wait: the GO port of src/rune/event_wait.py's
|
||||
// deterministic core (Oracle-Battery Parity Pattern, #2479 GO batch 4).
|
||||
// The 15E-case-10 recovery state machine + event matching are the pure
|
||||
// transforms; the FIFO rendezvous and HTTP stay Python (reactive surface).
|
||||
// deterministic core (Oracle-Battery Parity Pattern, #2479 GO batch 4 →
|
||||
// FULL-port dichotomy wave).
|
||||
//
|
||||
// DICHOTOMY VERDICT: GO. The full deterministic surface is name-preserving
|
||||
// here: wr_root (resolver chain), fifo_path, open_fifo_reader (FIFO
|
||||
// rendezvous setup), register_process_wait (durable-record shaping),
|
||||
// _pgroup_alive/_cmdline_alive/liveness (/proc watchdog probes),
|
||||
// operation_wait, resume_or_recover (15E-case-10 recovery state machine)
|
||||
// and find_matching_event (durable event-log scan).
|
||||
//
|
||||
// EXCLUDED (reactive surface, stays Python): run_builder_under_events,
|
||||
// await_resume_event (select() block on the FIFO), post_process_event
|
||||
// (HTTP POST), _load_waitstore (dynamic bridge-module loader). These are
|
||||
// IO seams, not deterministic kernels — parity-first, no improvements.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -10,9 +21,333 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
canonicalWRRoot = "/mnt/workdrive/wr-bridge"
|
||||
)
|
||||
|
||||
func legacyWRRoot() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "/root/.local/share/mimo-wait-resume"
|
||||
}
|
||||
return filepath.Join(home, ".local", "share", "mimo-wait-resume")
|
||||
}
|
||||
|
||||
// wrRoot — the ONE wait/resume root authority (scripts/wr_root_resolver.py
|
||||
// resolution order, mirrored 1:1): RUNE_WR_ROOT env → canonical production
|
||||
// root (live store or dir exists) → legacy home root → canonical on demand.
|
||||
// The resolver-unavailable fallback branch is a partial-checkout artifact —
|
||||
// the resolver ships with the repo, so the chain below is its contract.
|
||||
func wrRoot() string {
|
||||
if env := os.Getenv("RUNE_WR_ROOT"); env != "" {
|
||||
return env
|
||||
}
|
||||
if hasLiveStore(canonicalWRRoot) || dirExists(canonicalWRRoot) {
|
||||
return canonicalWRRoot
|
||||
}
|
||||
legacy := legacyWRRoot()
|
||||
if hasLiveStore(legacy) || dirExists(legacy) {
|
||||
return legacy
|
||||
}
|
||||
return canonicalWRRoot
|
||||
}
|
||||
|
||||
func dirExists(root string) bool {
|
||||
info, err := os.Stat(root)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// hasLiveStore — the resolver's live-store test: events dir holding ≥1 record.
|
||||
func hasLiveStore(root string) bool {
|
||||
entries, err := os.ReadDir(filepath.Join(root, "events"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, e := range entries {
|
||||
if filepath.Ext(e.Name()) == ".json" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fifoPath — the operation's FIFO rendezvous point under the resolved root.
|
||||
func fifoPath(operationID string) string {
|
||||
return filepath.Join(wrRoot(), "resumes", operationID+".fifo")
|
||||
}
|
||||
|
||||
// openFifoReader — open the operation's FIFO read end (O_NONBLOCK) BEFORE
|
||||
// the wait is registered (§16Y #10 race fix): unlink+recreate, chmod 0666
|
||||
// AFTER mkfifo (umask never applies), then O_RDONLY|O_NONBLOCK.
|
||||
func openFifoReader(operationID string) (int, error) {
|
||||
fifo := fifoPath(operationID)
|
||||
os.MkdirAll(filepath.Dir(fifo), 0o755)
|
||||
os.Remove(fifo)
|
||||
if err := syscall.Mkfifo(fifo, 0o666); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if err := os.Chmod(fifo, 0o666); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return syscall.Open(fifo, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
|
||||
}
|
||||
|
||||
// notifierWriterSrc — the per-wait resume_cmd writer program (verbatim).
|
||||
const notifierWriterSrc = "import sys,json,time,os;" +
|
||||
"fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);" +
|
||||
"os.write(fd, json.dumps({'wait_id': sys.argv[2]," +
|
||||
" 'resumed_at': time.time()}).encode());os.close(fd)"
|
||||
|
||||
// registerProcessWait — the durable PROCESS_EVENT record shape. The
|
||||
// waitstore load/save seams stay Python; this is the pure record transform
|
||||
// (kwargs → full new_wait kwargs + ext), verified against the oracle's
|
||||
// shaped records.
|
||||
func registerProcessWait(operationID string, kw map[string]any, env map[string]string) map[string]any {
|
||||
get := func(key, def string) string {
|
||||
if v, ok := kw[key].(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
provider := get("provider", "builder")
|
||||
eventType := get("event_type", "builder_exit")
|
||||
sha := get("sha", "")
|
||||
project := get("project", "")
|
||||
objective := get("objective", "")
|
||||
watchedFor := get("watched_for", "")
|
||||
continuationState := get("continuation_state", "WAITING_EXTERNAL")
|
||||
opType := get("op_type", "")
|
||||
authority := get("authority", "")
|
||||
taskID := get("task_id", "")
|
||||
resumeMode := get("resume_mode", "fifo")
|
||||
|
||||
watchdogSecs := 0
|
||||
switch v := kw["watchdog_secs"].(type) {
|
||||
case json.Number:
|
||||
n, _ := v.Int64()
|
||||
watchdogSecs = int(n)
|
||||
case float64:
|
||||
watchdogSecs = int(v)
|
||||
}
|
||||
if watchdogSecs == 0 { // Python `watchdog_secs or int(env or 600)`
|
||||
envSecs := "600"
|
||||
if e := env["WR_WATCH_SECS"]; e != "" {
|
||||
envSecs = e
|
||||
}
|
||||
fmt.Sscanf(envSecs, "%d", &watchdogSecs)
|
||||
}
|
||||
|
||||
fifo := fifoPath(operationID)
|
||||
os.MkdirAll(filepath.Dir(fifo), 0o755)
|
||||
if _, err := os.Stat(fifo); os.IsNotExist(err) {
|
||||
syscall.Mkfifo(fifo, 0o666)
|
||||
os.Chmod(fifo, 0o666)
|
||||
}
|
||||
|
||||
var resumeCmd any
|
||||
if resumeMode == "self" {
|
||||
resumeCmd = nil
|
||||
} else {
|
||||
resumeCmd = []any{"python3", "-c", notifierWriterSrc, fifo, "{wait_id}"}
|
||||
}
|
||||
|
||||
repo := project
|
||||
if repo == "" {
|
||||
repo = "construction"
|
||||
}
|
||||
obj := objective
|
||||
if obj == "" {
|
||||
obj = "builder operation " + operationID
|
||||
}
|
||||
wf := watchedFor
|
||||
if wf == "" {
|
||||
wf = "builder process exit event"
|
||||
}
|
||||
|
||||
rec := map[string]any{
|
||||
"wait_id": "wr-fake-0000",
|
||||
"session_id": "controller-" + operationID,
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"provider": provider,
|
||||
"event_type": eventType,
|
||||
"sha": sha,
|
||||
"repo": repo,
|
||||
"run_ref": operationID,
|
||||
"expected": "success",
|
||||
"wait_kind": "PROCESS_EVENT",
|
||||
"target_kind": "headless",
|
||||
"objective": obj,
|
||||
"waited_for": wf,
|
||||
"verify_when_woken": "reload canonical state; exit canary; independent verification",
|
||||
"on_success": "continue programme continuation canary",
|
||||
"on_failure": "classify failure; recovery or block",
|
||||
"resume_cmd": resumeCmd,
|
||||
"ext": map[string]any{
|
||||
"contract_id": get("contract_id", ""),
|
||||
"operation_id": operationID,
|
||||
"fifo": fifo,
|
||||
"watchdog_secs": watchdogSecs,
|
||||
"continuation_state": continuationState,
|
||||
"op_type": opType,
|
||||
"authority": authority,
|
||||
"task_id": taskID,
|
||||
"resume_mode": resumeMode,
|
||||
},
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// pgroupAlive — at least one NON-ZOMBIE member of the process group.
|
||||
func pgroupAlive(pgid int) bool {
|
||||
if err := syscall.Kill(-pgid, syscall.Signal(0)); err != nil {
|
||||
// ESRCH (gone) and EPERM (foreign) both classify NOT-alive,
|
||||
// matching the oracle's ProcessLookupError/PermissionError arm.
|
||||
return false
|
||||
}
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !isDigits(e.Name()) {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join("/proc", e.Name(), "stat"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
state, procPgid, ok := parseStat(string(raw))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if procPgid == pgid && state != "Z" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cmdlineAlive — any live process whose cmdline contains the needle.
|
||||
func cmdlineAlive(needle string) bool {
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !isDigits(e.Name()) {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join("/proc", e.Name(), "cmdline"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
statRaw, err := os.ReadFile(filepath.Join("/proc", e.Name(), "stat"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
state, _, ok := parseStat(string(statRaw))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(decodeReplace(raw), needle) && state != "Z" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// liveness — pgid membership first, cmdline evidence as the
|
||||
// regroup-tolerant fallback. pgid 0/None (Python falsy) skips the pgid arm.
|
||||
func liveness(operationID string, pgid int) bool {
|
||||
if pgid != 0 && pgroupAlive(pgid) {
|
||||
return true
|
||||
}
|
||||
return cmdlineAlive("op:" + operationID)
|
||||
}
|
||||
|
||||
func isDigits(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// parseStat — comm (field 2) may contain spaces/parens: split after the
|
||||
// LAST ')' then fields are state, ppid, pgid, ...
|
||||
func parseStat(stat string) (state string, pgid int, ok bool) {
|
||||
i := strings.LastIndex(stat, ")")
|
||||
if i < 0 || i+1 >= len(stat) {
|
||||
return "", 0, false
|
||||
}
|
||||
fields := strings.Fields(stat[i+1:])
|
||||
if len(fields) < 3 {
|
||||
return "", 0, false
|
||||
}
|
||||
n, err := parseInt(fields[2])
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
return fields[0], n, true
|
||||
}
|
||||
|
||||
func parseInt(s string) (int, error) {
|
||||
n := 0
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("empty")
|
||||
}
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, fmt.Errorf("not a number: %q", s)
|
||||
}
|
||||
n = n*10 + int(c-'0')
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// decodeReplace — errors="replace" semantics for the ASCII needles used here.
|
||||
func decodeReplace(b []byte) string {
|
||||
return strings.ToValidUTF8(string(b), "\uFFFD")
|
||||
}
|
||||
|
||||
// operationWait — find the durable record carrying this operation_id.
|
||||
func operationWait(waitsDir, operationID string) map[string]any {
|
||||
entries, err := os.ReadDir(waitsDir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, e := range entries {
|
||||
if filepath.Ext(e.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(waitsDir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var rec map[string]any
|
||||
if json.Unmarshal(raw, &rec) != nil {
|
||||
continue
|
||||
}
|
||||
ext, _ := rec["ext"].(map[string]any)
|
||||
if ext == nil {
|
||||
continue
|
||||
}
|
||||
if op, _ := ext["operation_id"].(string); op == operationID {
|
||||
return rec
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resumeOrRecover — Python-identical: the durable record is the authority.
|
||||
func resumeOrRecover(rec map[string]any) map[string]any {
|
||||
if rec == nil {
|
||||
@@ -35,6 +370,7 @@ func resumeOrRecover(rec map[string]any) map[string]any {
|
||||
}
|
||||
|
||||
// findMatchingEvent — newest mtime wins; identity = provider+event_type+run_ref.
|
||||
// run_ref coercion is Python's str(): missing/null → "", numbers stringify.
|
||||
func findMatchingEvent(eventsDir, provider, eventType, runRef string) map[string]any {
|
||||
entries, err := os.ReadDir(eventsDir)
|
||||
if err != nil {
|
||||
@@ -42,7 +378,7 @@ func findMatchingEvent(eventsDir, provider, eventType, runRef string) map[string
|
||||
}
|
||||
type cand struct {
|
||||
path string
|
||||
mtime int64
|
||||
mtime time.Time
|
||||
}
|
||||
var files []cand
|
||||
for _, e := range entries {
|
||||
@@ -53,16 +389,16 @@ func findMatchingEvent(eventsDir, provider, eventType, runRef string) map[string
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, cand{filepath.Join(eventsDir, e.Name()), info.ModTime().UnixNano()})
|
||||
files = append(files, cand{filepath.Join(eventsDir, e.Name()), info.ModTime()})
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool { return files[i].mtime > files[j].mtime })
|
||||
sort.SliceStable(files, func(i, j int) bool { return files[i].mtime.After(files[j].mtime) })
|
||||
for _, f := range files {
|
||||
raw, err := os.ReadFile(f.path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var rec map[string]any
|
||||
if err := json.Unmarshal(raw, &rec); err != nil {
|
||||
if json.Unmarshal(raw, &rec) != nil {
|
||||
continue
|
||||
}
|
||||
ev, _ := rec["event"].(map[string]any)
|
||||
@@ -71,7 +407,10 @@ func findMatchingEvent(eventsDir, provider, eventType, runRef string) map[string
|
||||
}
|
||||
p, _ := ev["provider"].(string)
|
||||
et, _ := ev["event_type"].(string)
|
||||
rr := fmt.Sprintf("%v", ev["run_ref"])
|
||||
rr := ""
|
||||
if v, has := ev["run_ref"]; has && v != nil {
|
||||
rr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
if p == provider && et == eventType && rr == runRef {
|
||||
return ev
|
||||
}
|
||||
@@ -79,95 +418,27 @@ func findMatchingEvent(eventsDir, provider, eventType, runRef string) map[string
|
||||
return nil
|
||||
}
|
||||
|
||||
// canonicalJSON — sorted-key compact dump for exact comparisons (Go marshals
|
||||
// map keys sorted; HTML escaping disabled to match Python's json.dumps).
|
||||
func canonicalJSON(v any) string {
|
||||
var sb strings.Builder
|
||||
enc := json.NewEncoder(&sb)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return fmt.Sprintf("<unmarshalable: %v>", err)
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: event-wait replay <truth.json>")
|
||||
os.Exit(2)
|
||||
}
|
||||
raw, err := os.ReadFile(os.Args[2])
|
||||
n, err := runReplay(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED (%d cases): %v\n", n, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var cases []map[string]any
|
||||
if err := json.Unmarshal(raw, &cases); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// reconstruct the oracle's fixture state deterministically
|
||||
base, err := os.MkdirTemp("", "ew_truth_")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.RemoveAll(base)
|
||||
waits := filepath.Join(base, "waits")
|
||||
os.MkdirAll(waits, 0o755)
|
||||
statuses := []string{"COMPLETED", "FAILED", "READY_TO_RESUME", "READY_TO_RETRY",
|
||||
"RESUMING", "RESUME_UNVERIFIED", "WAITING_EXTERNAL"}
|
||||
for _, st := range statuses {
|
||||
rec := map[string]any{
|
||||
"id": "w-" + st, "status": st,
|
||||
"ext": map[string]any{"operation_id": "op-" + st},
|
||||
}
|
||||
if st == "RESUME_UNVERIFIED" {
|
||||
rec["process_event_evidence"] = map[string]any{"verdict": "success"}
|
||||
}
|
||||
b, _ := json.Marshal(rec)
|
||||
os.WriteFile(filepath.Join(waits, "w-"+st+".json"), b, 0o644)
|
||||
}
|
||||
events := filepath.Join(base, "events")
|
||||
os.MkdirAll(events, 0o755)
|
||||
writeEv := func(name string, ev map[string]any) {
|
||||
b, _ := json.Marshal(map[string]any{"event": ev})
|
||||
os.WriteFile(filepath.Join(events, name), b, 0o644)
|
||||
}
|
||||
writeEv("a.json", map[string]any{"provider": "gitea", "event_type": "ci", "run_ref": "123", "verdict": "old"})
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
writeEv("b.json", map[string]any{"provider": "gitea", "event_type": "ci", "run_ref": "123", "verdict": "new"})
|
||||
writeEv("c.json", map[string]any{"provider": "gitea", "event_type": "deploy", "run_ref": "123"})
|
||||
|
||||
fails := 0
|
||||
for i, c := range cases {
|
||||
op, _ := c["op"].(string)
|
||||
switch op {
|
||||
case "recover":
|
||||
st, _ := c["status"].(string)
|
||||
wantKind, _ := c["want_kind"].(string)
|
||||
wantRec, _ := c["want_recovered"].(bool)
|
||||
rawRec, _ := os.ReadFile(filepath.Join(waits, "w-"+st+".json"))
|
||||
var rec map[string]any
|
||||
json.Unmarshal(rawRec, &rec)
|
||||
got := resumeOrRecover(rec)
|
||||
kind, _ := got["kind"].(string)
|
||||
rec_, _ := got["recovered"].(bool)
|
||||
if kind != wantKind || rec_ != wantRec {
|
||||
fails++
|
||||
fmt.Printf("case %d recover %s: got %v/%v want %v/%v\n", i, st, kind, rec_, wantKind, wantRec)
|
||||
}
|
||||
case "recover_unknown":
|
||||
got := resumeOrRecover(nil)
|
||||
if got["kind"] != "unknown_operation" {
|
||||
fails++
|
||||
fmt.Printf("case %d recover_unknown: got %v\n", i, got["kind"])
|
||||
}
|
||||
case "match":
|
||||
got := findMatchingEvent(events, "gitea", "ci", "123")
|
||||
v, _ := got["verdict"].(string)
|
||||
if v != "new" {
|
||||
fails++
|
||||
fmt.Printf("case %d match: got %v (want newest)\n", i, got)
|
||||
}
|
||||
case "match_none":
|
||||
if got := findMatchingEvent(events, "gitea", "ci", "999"); got != nil {
|
||||
fails++
|
||||
fmt.Printf("case %d match_none: got %v\n", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
if fails > 0 {
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", fails)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases exact-equal vs the Python oracle\n", len(cases))
|
||||
fmt.Printf("PARITY OK: %d cases exact-equal vs the Python oracle\n", n)
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package main
|
||||
|
||||
// replay.go — the oracle-first truth battery: replays the frozen truth
|
||||
// (generated from the live Python oracle by /tmp/ew_truth_gen.py) against
|
||||
// THIS Go port, case by case. Every case must match exactly.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// runReplay executes the frozen truth against the Go port. Every case must
|
||||
// match; any divergence is printed and makes the run exit non-zero.
|
||||
func runReplay(truthPath string) (int, error) {
|
||||
raw, err := os.ReadFile(truthPath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
||||
dec.UseNumber()
|
||||
var parsed struct {
|
||||
Module string `json:"module"`
|
||||
BaseRoot string `json:"base_root"`
|
||||
Cases []map[string]any `json:"cases"`
|
||||
}
|
||||
if err := dec.Decode(&parsed); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
base, err := os.MkdirTemp("", "ew_replay_")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer os.RemoveAll(base)
|
||||
os.Setenv("RUNE_WR_ROOT", base)
|
||||
defer os.Unsetenv("RUNE_WR_ROOT")
|
||||
|
||||
fails := 0
|
||||
for i, c := range parsed.Cases {
|
||||
// base-root substitution: the truth froze the generator's temp
|
||||
// root; the replay rebuilds the same state under its own root.
|
||||
cj := strings.ReplaceAll(canonicalJSON(c), parsed.BaseRoot, base)
|
||||
var caseMap map[string]any
|
||||
d2 := json.NewDecoder(strings.NewReader(cj))
|
||||
d2.UseNumber()
|
||||
if err := d2.Decode(&caseMap); err != nil {
|
||||
return len(parsed.Cases), fmt.Errorf("case %d: %w", i, err)
|
||||
}
|
||||
if msg := runCase(caseMap, base); msg != "" {
|
||||
fails++
|
||||
fmt.Printf("case %d (%s %s): %s\n", i, str(caseMap, "fn"), str(caseMap, "name"), msg)
|
||||
}
|
||||
}
|
||||
if fails > 0 {
|
||||
return len(parsed.Cases), fmt.Errorf("%d divergence(s)", fails)
|
||||
}
|
||||
return len(parsed.Cases), nil
|
||||
}
|
||||
|
||||
func str(m map[string]any, k string) string {
|
||||
v, _ := m[k].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func runCase(c map[string]any, base string) string {
|
||||
switch str(c, "fn") {
|
||||
case "wr_root":
|
||||
saved := os.Getenv("RUNE_WR_ROOT")
|
||||
defer os.Setenv("RUNE_WR_ROOT", saved)
|
||||
os.Unsetenv("RUNE_WR_ROOT")
|
||||
if env, ok := c["env"].(map[string]any); ok {
|
||||
for k, v := range env {
|
||||
os.Setenv(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
if got := wrRoot(); got != str(c, "want") {
|
||||
return fmt.Sprintf("wr_root got %q want %q", got, str(c, "want"))
|
||||
}
|
||||
return ""
|
||||
|
||||
case "fifo_path":
|
||||
if got := fifoPath(str(c, "op")); got != str(c, "want") {
|
||||
return fmt.Sprintf("fifo_path got %q want %q", got, str(c, "want"))
|
||||
}
|
||||
return ""
|
||||
|
||||
case "open_fifo_reader":
|
||||
op := str(c, "op")
|
||||
fd, err := openFifoReader(op)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("open_fifo_reader error: %v", err)
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
if fd < 0 {
|
||||
return "open_fifo_reader fd not positive"
|
||||
}
|
||||
info, err := os.Stat(fifoPath(op))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("stat fifo: %v", err)
|
||||
}
|
||||
if info.Mode()&os.ModeNamedPipe == 0 {
|
||||
return "path is not a FIFO"
|
||||
}
|
||||
if info.Mode().Perm() != 0o666 {
|
||||
return fmt.Sprintf("fifo mode %o want 666", info.Mode().Perm())
|
||||
}
|
||||
return ""
|
||||
|
||||
case "register_process_wait":
|
||||
kw, _ := c["kw"].(map[string]any)
|
||||
env := map[string]string{}
|
||||
if em, ok := c["env"].(map[string]any); ok {
|
||||
for k, v := range em {
|
||||
env[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
got := registerProcessWait(str(c, "op"), kw, env)
|
||||
if canonicalJSON(got) != canonicalJSON(c["want_rec"]) {
|
||||
return fmt.Sprintf("record shape\n got %s\nwant %s",
|
||||
canonicalJSON(got), canonicalJSON(c["want_rec"]))
|
||||
}
|
||||
return ""
|
||||
|
||||
case "resume_or_recover":
|
||||
var rec map[string]any
|
||||
if c["status"] != nil {
|
||||
rec = map[string]any{"status": str(c, "status")}
|
||||
if ev, _ := c["evidence"].(bool); ev {
|
||||
rec["process_event_evidence"] = map[string]any{"verdict": "success"}
|
||||
}
|
||||
}
|
||||
got := resumeOrRecover(rec)
|
||||
if kind, _ := got["kind"].(string); kind != str(c, "want_kind") {
|
||||
return fmt.Sprintf("kind %q want %q", kind, str(c, "want_kind"))
|
||||
}
|
||||
recd, _ := got["recovered"].(bool)
|
||||
wantRec, _ := c["want_recovered"].(bool)
|
||||
if recd != wantRec {
|
||||
return fmt.Sprintf("recovered %v want %v", recd, wantRec)
|
||||
}
|
||||
if w, ok := got["wait"].(map[string]any); ok {
|
||||
if st, _ := w["status"].(string); st != str(c, "want_status") {
|
||||
return fmt.Sprintf("wait status %q want %q", st, str(c, "want_status"))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
case "operation_wait":
|
||||
waits := filepath.Join(base, "waits")
|
||||
os.MkdirAll(waits, 0o755)
|
||||
one := map[string]any{
|
||||
"wait_id": "wr-1", "status": "WAITING_EXTERNAL",
|
||||
"ext": map[string]any{"operation_id": "op-a"},
|
||||
}
|
||||
b, _ := json.Marshal(one)
|
||||
os.WriteFile(filepath.Join(waits, "wr-1.json"), b, 0o644)
|
||||
got := operationWait(waits, str(c, "op"))
|
||||
found := got != nil
|
||||
wantFound, _ := c["want_found"].(bool)
|
||||
if found != wantFound {
|
||||
return fmt.Sprintf("found %v want %v", found, wantFound)
|
||||
}
|
||||
if found {
|
||||
ext, _ := got["ext"].(map[string]any)
|
||||
op, _ := ext["operation_id"].(string)
|
||||
st, _ := got["status"].(string)
|
||||
if op != str(c, "want_op") || st != str(c, "want_status") {
|
||||
return fmt.Sprintf("rec op/status %q/%q", op, st)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
case "find_matching_event":
|
||||
events := filepath.Join(base, "events")
|
||||
os.MkdirAll(events, 0o755)
|
||||
writeEv := func(name string, ev map[string]any) {
|
||||
b, _ := json.Marshal(map[string]any{"event": ev})
|
||||
os.WriteFile(filepath.Join(events, name), b, 0o644)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
writeEv("e_old.json", map[string]any{"provider": "gitea", "event_type": "ci", "run_ref": "123", "verdict": "old"})
|
||||
writeEv("e_new.json", map[string]any{"provider": "gitea", "event_type": "ci", "run_ref": "123", "verdict": "new"})
|
||||
writeEv("e_other.json", map[string]any{"provider": "gitea", "event_type": "deploy", "run_ref": "124", "verdict": "deploy"})
|
||||
writeEv("e_int.json", map[string]any{"provider": "gitea", "event_type": "ci", "run_ref": json.Number("123"), "verdict": "int-new"})
|
||||
writeEv("e_noref.json", map[string]any{"provider": "gitea", "event_type": "ci", "verdict": "noref-new"})
|
||||
os.WriteFile(filepath.Join(events, "e_bad.json"), []byte("{not json"), 0o644)
|
||||
os.WriteFile(filepath.Join(events, "e_notjson.txt"), []byte("nope"), 0o644)
|
||||
got := findMatchingEvent(events, str(c, "provider"), str(c, "event_type"), str(c, "run_ref"))
|
||||
wantNone, _ := c["want_none"].(bool)
|
||||
if wantNone {
|
||||
if got != nil {
|
||||
return fmt.Sprintf("expected no match, got %v", got)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if got == nil {
|
||||
return "expected a match, got none"
|
||||
}
|
||||
if v, _ := got["verdict"].(string); v != str(c, "want_verdict") {
|
||||
return fmt.Sprintf("verdict %q want %q", v, str(c, "want_verdict"))
|
||||
}
|
||||
return ""
|
||||
|
||||
case "pgroup_alive":
|
||||
want, _ := c["want"].(bool)
|
||||
if got := runPgroupProbe(str(c, "probe")); got != want {
|
||||
return fmt.Sprintf("pgroup_alive(%s) got %v want %v", str(c, "probe"), got, want)
|
||||
}
|
||||
return ""
|
||||
|
||||
case "cmdline_alive":
|
||||
needle := str(c, "needle")
|
||||
if needle == "__SELF__" {
|
||||
needle = selfPath()
|
||||
}
|
||||
want, _ := c["want"].(bool)
|
||||
if got := cmdlineAlive(needle); got != want {
|
||||
return fmt.Sprintf("cmdline_alive(%q) got %v want %v", needle, got, want)
|
||||
}
|
||||
return ""
|
||||
|
||||
case "liveness":
|
||||
want, _ := c["want"].(bool)
|
||||
pgid := 0
|
||||
switch str(c, "pgid_probe") {
|
||||
case "own":
|
||||
pgid = syscall.Getpgrp()
|
||||
case "absent":
|
||||
pgid = 999999999
|
||||
}
|
||||
if got := liveness(str(c, "op"), pgid); got != want {
|
||||
return fmt.Sprintf("liveness(%s) got %v want %v", str(c, "pgid_probe"), got, want)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("unknown fn %q", str(c, "fn"))
|
||||
}
|
||||
|
||||
// runPgroupProbe rebuilds the oracle's live process state per probe.
|
||||
func runPgroupProbe(probe string) bool {
|
||||
switch probe {
|
||||
case "own":
|
||||
return pgroupAlive(syscall.Getpgrp())
|
||||
case "child-alive":
|
||||
proc := spawnSleeper()
|
||||
defer killReap(proc)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return pgroupAlive(proc.Pid)
|
||||
case "child-dead":
|
||||
proc := spawnSleeper()
|
||||
killReap(proc)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return pgroupAlive(proc.Pid)
|
||||
case "absent":
|
||||
return pgroupAlive(999999999)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func spawnSleeper() *os.Process {
|
||||
cmd := exec.Command("/bin/sh", "-c", "sleep 30")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return cmd.Process
|
||||
}
|
||||
|
||||
func killReap(proc *os.Process) {
|
||||
syscall.Kill(-proc.Pid, syscall.SIGKILL)
|
||||
proc.Wait()
|
||||
}
|
||||
|
||||
// selfPath — the __SELF__ probe: this process's own argv[0], matching the
|
||||
// oracle's script-path needle.
|
||||
func selfPath() string {
|
||||
p, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
return os.Args[0]
|
||||
}
|
||||
return p
|
||||
}
|
||||
+448
-56
@@ -1,56 +1,448 @@
|
||||
[
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "COMPLETED",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true
|
||||
},
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "FAILED",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true
|
||||
},
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "READY_TO_RESUME",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true
|
||||
},
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "READY_TO_RETRY",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true
|
||||
},
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "RESUMING",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true
|
||||
},
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "RESUME_UNVERIFIED",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true
|
||||
},
|
||||
{
|
||||
"op": "recover",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"want_kind": "still_waiting",
|
||||
"want_recovered": false
|
||||
},
|
||||
{
|
||||
"op": "recover_unknown",
|
||||
"want_kind": "unknown_operation"
|
||||
},
|
||||
{
|
||||
"op": "match",
|
||||
"want_verdict": "new"
|
||||
},
|
||||
{
|
||||
"op": "match_none",
|
||||
"want": true
|
||||
}
|
||||
]
|
||||
{
|
||||
"base_root": "/tmp/ew_truth_kavaxsjd",
|
||||
"cases": [
|
||||
{
|
||||
"env": {
|
||||
"RUNE_WR_ROOT": "/tmp/ew_truth_kavaxsjd"
|
||||
},
|
||||
"fn": "wr_root",
|
||||
"name": "env-pin",
|
||||
"want": "/tmp/ew_truth_kavaxsjd"
|
||||
},
|
||||
{
|
||||
"env": {},
|
||||
"fn": "wr_root",
|
||||
"name": "canonical-live",
|
||||
"want": "/mnt/workdrive/wr-bridge"
|
||||
},
|
||||
{
|
||||
"fn": "fifo_path",
|
||||
"op": "op-x",
|
||||
"want": "/tmp/ew_truth_kavaxsjd/resumes/op-x.fifo"
|
||||
},
|
||||
{
|
||||
"fn": "open_fifo_reader",
|
||||
"op": "op-open",
|
||||
"want_fd_positive": true,
|
||||
"want_isfifo": true,
|
||||
"want_mode": "0o666"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "COMPLETED",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true,
|
||||
"want_status": "COMPLETED"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "FAILED",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true,
|
||||
"want_status": "FAILED"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "READY_TO_RESUME",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true,
|
||||
"want_status": "READY_TO_RESUME"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "READY_TO_RETRY",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true,
|
||||
"want_status": "READY_TO_RETRY"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "RESUMING",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true,
|
||||
"want_status": "RESUMING"
|
||||
},
|
||||
{
|
||||
"evidence": true,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "RESUME_UNVERIFIED",
|
||||
"want_kind": "event",
|
||||
"want_recovered": true,
|
||||
"want_status": "RESUME_UNVERIFIED"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "RESUME_UNVERIFIED",
|
||||
"want_kind": "still_waiting",
|
||||
"want_recovered": false,
|
||||
"want_status": "RESUME_UNVERIFIED"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"want_kind": "still_waiting",
|
||||
"want_recovered": false,
|
||||
"want_status": "WAITING_EXTERNAL"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "RUNNABLE",
|
||||
"want_kind": "still_waiting",
|
||||
"want_recovered": false,
|
||||
"want_status": "RUNNABLE"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": "TIMED_OUT",
|
||||
"want_kind": "still_waiting",
|
||||
"want_recovered": false,
|
||||
"want_status": "TIMED_OUT"
|
||||
},
|
||||
{
|
||||
"evidence": false,
|
||||
"fn": "resume_or_recover",
|
||||
"status": null,
|
||||
"want_kind": "unknown_operation",
|
||||
"want_recovered": false,
|
||||
"want_status": null
|
||||
},
|
||||
{
|
||||
"fn": "operation_wait",
|
||||
"op": "op-a",
|
||||
"want_found": true,
|
||||
"want_op": "op-a",
|
||||
"want_status": "WAITING_EXTERNAL"
|
||||
},
|
||||
{
|
||||
"fn": "operation_wait",
|
||||
"op": "op-missing",
|
||||
"want_found": false,
|
||||
"want_op": null,
|
||||
"want_status": null
|
||||
},
|
||||
{
|
||||
"event_type": "ci",
|
||||
"fn": "find_matching_event",
|
||||
"provider": "gitea",
|
||||
"run_ref": "123",
|
||||
"want_verdict": "int-new"
|
||||
},
|
||||
{
|
||||
"event_type": "ci",
|
||||
"fn": "find_matching_event",
|
||||
"provider": "gitea",
|
||||
"run_ref": "999",
|
||||
"want_none": true
|
||||
},
|
||||
{
|
||||
"event_type": "deploy",
|
||||
"fn": "find_matching_event",
|
||||
"provider": "gitea",
|
||||
"run_ref": "124",
|
||||
"want_verdict": "deploy"
|
||||
},
|
||||
{
|
||||
"event_type": "ci",
|
||||
"fn": "find_matching_event",
|
||||
"provider": "gitea",
|
||||
"run_ref": "",
|
||||
"want_none": false,
|
||||
"want_verdict": "noref-new"
|
||||
},
|
||||
{
|
||||
"fn": "pgroup_alive",
|
||||
"probe": "own",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "pgroup_alive",
|
||||
"probe": "child-alive",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "pgroup_alive",
|
||||
"probe": "child-dead",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "pgroup_alive",
|
||||
"probe": "absent",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "cmdline_alive",
|
||||
"needle": "__SELF__",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "cmdline_alive",
|
||||
"needle": "op:ew-truth-absent-op",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "liveness",
|
||||
"op": "ew-truth-absent-op",
|
||||
"pgid_probe": "none",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "liveness",
|
||||
"op": "ew-truth-absent-op",
|
||||
"pgid_probe": "zero",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "liveness",
|
||||
"op": "ew-truth-absent-op",
|
||||
"pgid_probe": "own",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "liveness",
|
||||
"op": "ew-truth-absent-op",
|
||||
"pgid_probe": "absent",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"env": {},
|
||||
"fn": "register_process_wait",
|
||||
"kw": {},
|
||||
"name": "defaults",
|
||||
"op": "op-reg-default",
|
||||
"want_rec": {
|
||||
"event_type": "builder_exit",
|
||||
"expected": "success",
|
||||
"ext": {
|
||||
"authority": "",
|
||||
"continuation_state": "WAITING_EXTERNAL",
|
||||
"contract_id": "",
|
||||
"fifo": "/tmp/ew_truth_kavaxsjd/resumes/op-reg-default.fifo",
|
||||
"op_type": "",
|
||||
"operation_id": "op-reg-default",
|
||||
"resume_mode": "fifo",
|
||||
"task_id": "",
|
||||
"watchdog_secs": 600
|
||||
},
|
||||
"objective": "builder operation op-reg-default",
|
||||
"on_failure": "classify failure; recovery or block",
|
||||
"on_success": "continue programme continuation canary",
|
||||
"provider": "builder",
|
||||
"repo": "construction",
|
||||
"resume_cmd": [
|
||||
"python3",
|
||||
"-c",
|
||||
"import sys,json,time,os;fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);os.write(fd, json.dumps({'wait_id': sys.argv[2], 'resumed_at': time.time()}).encode());os.close(fd)",
|
||||
"/tmp/ew_truth_kavaxsjd/resumes/op-reg-default.fifo",
|
||||
"{wait_id}"
|
||||
],
|
||||
"run_ref": "op-reg-default",
|
||||
"session_id": "controller-op-reg-default",
|
||||
"sha": "",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"target_kind": "headless",
|
||||
"verify_when_woken": "reload canonical state; exit canary; independent verification",
|
||||
"wait_id": "wr-fake-0000",
|
||||
"wait_kind": "PROCESS_EVENT",
|
||||
"waited_for": "builder process exit event"
|
||||
}
|
||||
},
|
||||
{
|
||||
"env": {},
|
||||
"fn": "register_process_wait",
|
||||
"kw": {
|
||||
"resume_mode": "self"
|
||||
},
|
||||
"name": "self-mode",
|
||||
"op": "op-reg-self",
|
||||
"want_rec": {
|
||||
"event_type": "builder_exit",
|
||||
"expected": "success",
|
||||
"ext": {
|
||||
"authority": "",
|
||||
"continuation_state": "WAITING_EXTERNAL",
|
||||
"contract_id": "",
|
||||
"fifo": "/tmp/ew_truth_kavaxsjd/resumes/op-reg-self.fifo",
|
||||
"op_type": "",
|
||||
"operation_id": "op-reg-self",
|
||||
"resume_mode": "self",
|
||||
"task_id": "",
|
||||
"watchdog_secs": 600
|
||||
},
|
||||
"objective": "builder operation op-reg-self",
|
||||
"on_failure": "classify failure; recovery or block",
|
||||
"on_success": "continue programme continuation canary",
|
||||
"provider": "builder",
|
||||
"repo": "construction",
|
||||
"resume_cmd": null,
|
||||
"run_ref": "op-reg-self",
|
||||
"session_id": "controller-op-reg-self",
|
||||
"sha": "",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"target_kind": "headless",
|
||||
"verify_when_woken": "reload canonical state; exit canary; independent verification",
|
||||
"wait_id": "wr-fake-0000",
|
||||
"wait_kind": "PROCESS_EVENT",
|
||||
"waited_for": "builder process exit event"
|
||||
}
|
||||
},
|
||||
{
|
||||
"env": {},
|
||||
"fn": "register_process_wait",
|
||||
"kw": {
|
||||
"authority": "spine",
|
||||
"continuation_state": "RESUMABLE",
|
||||
"contract_id": "contract:t15",
|
||||
"event_type": "job_done",
|
||||
"objective": "latency proof",
|
||||
"op_type": "task",
|
||||
"project": "t15-fixture",
|
||||
"provider": "ci-adapter",
|
||||
"resume_mode": "fifo",
|
||||
"sha": "abc123",
|
||||
"task_id": "T9",
|
||||
"watchdog_secs": 120,
|
||||
"watched_for": "watched thing"
|
||||
},
|
||||
"name": "explicit",
|
||||
"op": "op-reg-custom",
|
||||
"want_rec": {
|
||||
"event_type": "job_done",
|
||||
"expected": "success",
|
||||
"ext": {
|
||||
"authority": "spine",
|
||||
"continuation_state": "RESUMABLE",
|
||||
"contract_id": "contract:t15",
|
||||
"fifo": "/tmp/ew_truth_kavaxsjd/resumes/op-reg-custom.fifo",
|
||||
"op_type": "task",
|
||||
"operation_id": "op-reg-custom",
|
||||
"resume_mode": "fifo",
|
||||
"task_id": "T9",
|
||||
"watchdog_secs": 120
|
||||
},
|
||||
"objective": "latency proof",
|
||||
"on_failure": "classify failure; recovery or block",
|
||||
"on_success": "continue programme continuation canary",
|
||||
"provider": "ci-adapter",
|
||||
"repo": "t15-fixture",
|
||||
"resume_cmd": [
|
||||
"python3",
|
||||
"-c",
|
||||
"import sys,json,time,os;fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);os.write(fd, json.dumps({'wait_id': sys.argv[2], 'resumed_at': time.time()}).encode());os.close(fd)",
|
||||
"/tmp/ew_truth_kavaxsjd/resumes/op-reg-custom.fifo",
|
||||
"{wait_id}"
|
||||
],
|
||||
"run_ref": "op-reg-custom",
|
||||
"session_id": "controller-op-reg-custom",
|
||||
"sha": "abc123",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"target_kind": "headless",
|
||||
"verify_when_woken": "reload canonical state; exit canary; independent verification",
|
||||
"wait_id": "wr-fake-0000",
|
||||
"wait_kind": "PROCESS_EVENT",
|
||||
"waited_for": "watched thing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"env": {
|
||||
"WR_WATCH_SECS": "777"
|
||||
},
|
||||
"fn": "register_process_wait",
|
||||
"kw": {},
|
||||
"name": "watchdog-env",
|
||||
"op": "op-reg-watchdog-env",
|
||||
"want_rec": {
|
||||
"event_type": "builder_exit",
|
||||
"expected": "success",
|
||||
"ext": {
|
||||
"authority": "",
|
||||
"continuation_state": "WAITING_EXTERNAL",
|
||||
"contract_id": "",
|
||||
"fifo": "/tmp/ew_truth_kavaxsjd/resumes/op-reg-watchdog-env.fifo",
|
||||
"op_type": "",
|
||||
"operation_id": "op-reg-watchdog-env",
|
||||
"resume_mode": "fifo",
|
||||
"task_id": "",
|
||||
"watchdog_secs": 777
|
||||
},
|
||||
"objective": "builder operation op-reg-watchdog-env",
|
||||
"on_failure": "classify failure; recovery or block",
|
||||
"on_success": "continue programme continuation canary",
|
||||
"provider": "builder",
|
||||
"repo": "construction",
|
||||
"resume_cmd": [
|
||||
"python3",
|
||||
"-c",
|
||||
"import sys,json,time,os;fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);os.write(fd, json.dumps({'wait_id': sys.argv[2], 'resumed_at': time.time()}).encode());os.close(fd)",
|
||||
"/tmp/ew_truth_kavaxsjd/resumes/op-reg-watchdog-env.fifo",
|
||||
"{wait_id}"
|
||||
],
|
||||
"run_ref": "op-reg-watchdog-env",
|
||||
"session_id": "controller-op-reg-watchdog-env",
|
||||
"sha": "",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"target_kind": "headless",
|
||||
"verify_when_woken": "reload canonical state; exit canary; independent verification",
|
||||
"wait_id": "wr-fake-0000",
|
||||
"wait_kind": "PROCESS_EVENT",
|
||||
"waited_for": "builder process exit event"
|
||||
}
|
||||
},
|
||||
{
|
||||
"env": {},
|
||||
"fn": "register_process_wait",
|
||||
"kw": {
|
||||
"watchdog_secs": 42
|
||||
},
|
||||
"name": "watchdog-explicit",
|
||||
"op": "op-reg-watchdog-explicit",
|
||||
"want_rec": {
|
||||
"event_type": "builder_exit",
|
||||
"expected": "success",
|
||||
"ext": {
|
||||
"authority": "",
|
||||
"continuation_state": "WAITING_EXTERNAL",
|
||||
"contract_id": "",
|
||||
"fifo": "/tmp/ew_truth_kavaxsjd/resumes/op-reg-watchdog-explicit.fifo",
|
||||
"op_type": "",
|
||||
"operation_id": "op-reg-watchdog-explicit",
|
||||
"resume_mode": "fifo",
|
||||
"task_id": "",
|
||||
"watchdog_secs": 42
|
||||
},
|
||||
"objective": "builder operation op-reg-watchdog-explicit",
|
||||
"on_failure": "classify failure; recovery or block",
|
||||
"on_success": "continue programme continuation canary",
|
||||
"provider": "builder",
|
||||
"repo": "construction",
|
||||
"resume_cmd": [
|
||||
"python3",
|
||||
"-c",
|
||||
"import sys,json,time,os;fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);os.write(fd, json.dumps({'wait_id': sys.argv[2], 'resumed_at': time.time()}).encode());os.close(fd)",
|
||||
"/tmp/ew_truth_kavaxsjd/resumes/op-reg-watchdog-explicit.fifo",
|
||||
"{wait_id}"
|
||||
],
|
||||
"run_ref": "op-reg-watchdog-explicit",
|
||||
"session_id": "controller-op-reg-watchdog-explicit",
|
||||
"sha": "",
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"target_kind": "headless",
|
||||
"verify_when_woken": "reload canonical state; exit canary; independent verification",
|
||||
"wait_id": "wr-fake-0000",
|
||||
"wait_kind": "PROCESS_EVENT",
|
||||
"waited_for": "builder process exit event"
|
||||
}
|
||||
}
|
||||
],
|
||||
"generated_by": "/tmp/ew_truth_gen.py",
|
||||
"module": "event_wait",
|
||||
"oracle": "src/rune/event_wait.py"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
// truth_test.go — the HARD parity gate: replays the frozen oracle truth
|
||||
// (generated from the live Python oracle) against this port. Any
|
||||
// divergence fails the test; the truth file is embedded via the repo copy
|
||||
// so the gate holds in CI without /tmp.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruthParity(t *testing.T) {
|
||||
const path = "truth.json"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
n, err := runReplay(path)
|
||||
if err != nil {
|
||||
t.Fatalf("parity replay diverged after %d cases: %v", n, err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("truth.json contained no cases")
|
||||
}
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", n)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// CLI — exact port of goal.py main(): same argv parsing, same printed
|
||||
// lines (including the ✓ characters), byte-for-byte stdout parity.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RunCLI executes goal.py main() against the given argv (including
|
||||
// program name at index 0) and writes the exact stdout bytes.
|
||||
func RunCLI(argv []string, w io.Writer) {
|
||||
managerMu.Lock()
|
||||
if globalManager == nil {
|
||||
globalManager = NewGoalManager(homeGoalsPath())
|
||||
globalManager.Load()
|
||||
}
|
||||
managerMu.Unlock()
|
||||
|
||||
if len(argv) < 2 {
|
||||
printUsage(w)
|
||||
return
|
||||
}
|
||||
|
||||
command := argv[1]
|
||||
|
||||
switch command {
|
||||
case "set":
|
||||
if len(argv) < 3 {
|
||||
fmt.Fprintln(w, "Usage: goal.py set <goal> [--conditions <cond1,cond2>] [--verify <method>]")
|
||||
return
|
||||
}
|
||||
description := argv[2]
|
||||
var stopConditions []string
|
||||
verificationMethod := "manual"
|
||||
|
||||
i := 3
|
||||
for i < len(argv) {
|
||||
if argv[i] == "--conditions" && i+1 < len(argv) {
|
||||
stopConditions = strings.Split(argv[i+1], ",")
|
||||
i += 2
|
||||
} else if argv[i] == "--verify" && i+1 < len(argv) {
|
||||
verificationMethod = argv[i+1]
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
result := setGoal(description, stopConditions, verificationMethod)
|
||||
desc, _ := result.Get("description")
|
||||
fmt.Fprintf(w, "✓ Goal set: %s\n", pyStr(desc))
|
||||
if len(stopConditions) > 0 {
|
||||
fmt.Fprintf(w, " Stop conditions: %s\n", strings.Join(stopConditions, ", "))
|
||||
}
|
||||
|
||||
case "check":
|
||||
var evidence *string
|
||||
if len(argv) > 2 && argv[2] == "--evidence" {
|
||||
if len(argv) > 3 {
|
||||
evidence = &argv[3]
|
||||
}
|
||||
}
|
||||
result := checkGoal(evidence)
|
||||
status, _ := result.Get("status")
|
||||
message, _ := result.Get("message")
|
||||
fmt.Fprintf(w, "Status: %s\n", pyStr(status))
|
||||
fmt.Fprintf(w, "Message: %s\n", pyStr(message))
|
||||
if goalV, ok := result.Get("goal"); ok && goalV != nil {
|
||||
goal := goalV.(*OrderedMap)
|
||||
descV, _ := goal.Get("description")
|
||||
fmt.Fprintf(w, "Goal: %s\n", pyStr(descV))
|
||||
if metV, ok := result.Get("conditions_met"); ok {
|
||||
totalV, _ := result.Get("conditions_total")
|
||||
fmt.Fprintf(w, "Conditions: %s/%s\n", pyStr(metV), pyStr(totalV))
|
||||
}
|
||||
}
|
||||
|
||||
case "achieve":
|
||||
var evidence *string
|
||||
if len(argv) > 2 && argv[2] == "--evidence" {
|
||||
if len(argv) > 3 {
|
||||
evidence = &argv[3]
|
||||
}
|
||||
}
|
||||
result := achieveGoal(evidence)
|
||||
message, _ := result.Get("message")
|
||||
fmt.Fprintf(w, "✓ %s\n", pyStr(message))
|
||||
|
||||
case "clear":
|
||||
result := clearGoal()
|
||||
message, _ := result.Get("message")
|
||||
fmt.Fprintf(w, "✓ %s\n", pyStr(message))
|
||||
|
||||
case "status":
|
||||
result := checkGoal(nil)
|
||||
status, _ := result.Get("status")
|
||||
if status == "no_goal" {
|
||||
fmt.Fprintln(w, "No active goal")
|
||||
} else {
|
||||
goalV, _ := result.Get("goal")
|
||||
goal := goalV.(*OrderedMap)
|
||||
descV, _ := goal.Get("description")
|
||||
statusV, _ := goal.Get("status")
|
||||
condsV, _ := goal.Get("stop_conditions")
|
||||
evidV, _ := goal.Get("evidence")
|
||||
conds, _ := condsV.([]any)
|
||||
evid, _ := evidV.([]any)
|
||||
fmt.Fprintf(w, "Goal: %s\n", pyStr(descV))
|
||||
fmt.Fprintf(w, "Status: %s\n", pyStr(statusV))
|
||||
fmt.Fprintf(w, "Conditions: %d\n", len(conds))
|
||||
fmt.Fprintf(w, "Evidence: %d\n", len(evid))
|
||||
}
|
||||
|
||||
case "list":
|
||||
goals := listGoals()
|
||||
if len(goals) == 0 {
|
||||
fmt.Fprintln(w, "No goals set")
|
||||
} else {
|
||||
fmt.Fprintf(w, "Goals: %d\n", len(goals))
|
||||
for _, g := range goals {
|
||||
goal := g.(*OrderedMap)
|
||||
statusV, _ := goal.Get("status")
|
||||
descV, _ := goal.Get("description")
|
||||
fmt.Fprintf(w, " [%s] %s\n", pyStr(statusV), pyStr(descV))
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Fprintf(w, "Unknown command: %s\n", command)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "Usage:")
|
||||
fmt.Fprintln(w, " goal.py set <goal> [--conditions <cond1,cond2>] [--verify <method>]")
|
||||
fmt.Fprintln(w, " goal.py check [--evidence <text>]")
|
||||
fmt.Fprintln(w, " goal.py achieve [--evidence <text>]")
|
||||
fmt.Fprintln(w, " goal.py clear")
|
||||
fmt.Fprintln(w, " goal.py status")
|
||||
fmt.Fprintln(w, " goal.py list")
|
||||
}
|
||||
|
||||
func homeGoalsPath() string {
|
||||
return filepath.Join(HomeDir(), ".rune", "goals.json")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/goal
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,725 @@
|
||||
// Package main — the FULL GO port of src/rune/goal.py (#2479 dichotomy
|
||||
// tranche, GO side verdict). Every public Python function has a
|
||||
// name-preserving counterpart; the goal state machine + keyword
|
||||
// verification are pure transforms, and IO (storage file, LLM endpoint,
|
||||
// clock, uuid) stays behind injectable seams so the frozen truth battery
|
||||
// replays deterministically.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Goal — Python-identical field semantics (goal.Goal dataclass).
|
||||
type Goal struct {
|
||||
GoalID string
|
||||
Description string
|
||||
StopConditions []string
|
||||
VerificationMethod string
|
||||
Status string // "active", "achieved", "failed", "abandoned"
|
||||
CreatedAt string
|
||||
CompletedAt *string
|
||||
Evidence []string
|
||||
}
|
||||
|
||||
// toDict — Goal.to_dict, key order preserved (goal_id, description,
|
||||
// stop_conditions, verification_method, status, created_at, completed_at,
|
||||
// evidence). Nil lists emit [] like Python's default_factory.
|
||||
func (g *Goal) toDict() *OrderedMap {
|
||||
conds := g.StopConditions
|
||||
if conds == nil {
|
||||
conds = []string{}
|
||||
}
|
||||
ev := g.Evidence
|
||||
if ev == nil {
|
||||
ev = []string{}
|
||||
}
|
||||
var completed any
|
||||
if g.CompletedAt != nil {
|
||||
completed = *g.CompletedAt
|
||||
} else {
|
||||
completed = nil
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"goal_id", g.GoalID},
|
||||
KV{"description", g.Description},
|
||||
KV{"stop_conditions", toAnySlice(conds)},
|
||||
KV{"verification_method", g.VerificationMethod},
|
||||
KV{"status", g.Status},
|
||||
KV{"created_at", g.CreatedAt},
|
||||
KV{"completed_at", completed},
|
||||
KV{"evidence", toAnySlice(ev)},
|
||||
)
|
||||
}
|
||||
|
||||
// goalFromDict rebuilds a Goal from a parsed JSON object (missing optional
|
||||
// fields behave like the Python dataclass defaults).
|
||||
func goalFromDict(om *OrderedMap) *Goal {
|
||||
g := &Goal{
|
||||
VerificationMethod: "manual",
|
||||
Status: "active",
|
||||
}
|
||||
if v, ok := om.Get("goal_id"); ok {
|
||||
g.GoalID, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("description"); ok {
|
||||
g.Description, _ = v.(string)
|
||||
}
|
||||
if v, ok := om.Get("stop_conditions"); ok {
|
||||
g.StopConditions = stringSlice(v)
|
||||
}
|
||||
if v, ok := om.Get("verification_method"); ok {
|
||||
if s, ok := v.(string); ok {
|
||||
g.VerificationMethod = s
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("status"); ok {
|
||||
if s, ok := v.(string); ok {
|
||||
g.Status = s
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("created_at"); ok {
|
||||
if s, ok := v.(string); ok {
|
||||
g.CreatedAt = s
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("completed_at"); ok {
|
||||
if s, ok := v.(string); ok {
|
||||
g.CompletedAt = &s
|
||||
}
|
||||
}
|
||||
if v, ok := om.Get("evidence"); ok {
|
||||
g.Evidence = stringSlice(v)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// ---------- seams (frozen by the truth battery) ----------
|
||||
|
||||
// ClockNow — datetime.now(timezone.utc).isoformat().
|
||||
var ClockNow = func() string {
|
||||
return time.Now().UTC().Format("2006-01-02T15:04:05.000000") + "+00:00"
|
||||
}
|
||||
|
||||
// NewGoalID — str(uuid.uuid4())[:8].
|
||||
var NewGoalID = func() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
s := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
return s[:8]
|
||||
}
|
||||
|
||||
// HomeDir — os.path.expanduser("~") seam (default storage path).
|
||||
var HomeDir = func() string {
|
||||
h, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// LLMPost — urllib.request.urlopen seam: POSTs the payload, returns the
|
||||
// response body. Any error triggers the keyword-verification fallback,
|
||||
// exactly like the Python except-path.
|
||||
var LLMPost = func(url string, payload []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", url, bytesReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := make([]byte, 0, 4096)
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(tmp)
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// ---------- GoalManager ----------
|
||||
|
||||
// GoalManager manages goals and stop conditions (goal.GoalManager).
|
||||
type GoalManager struct {
|
||||
StoragePath string
|
||||
Goals *OrderedMap // goal_id -> *Goal, insertion-ordered
|
||||
}
|
||||
|
||||
// NewGoalManager — GoalManager(storage_path=...).
|
||||
func NewGoalManager(storagePath string) *GoalManager {
|
||||
return &GoalManager{StoragePath: storagePath, Goals: NewOrderedMap()}
|
||||
}
|
||||
|
||||
// Load — GoalManager._load: missing file and any parse/validation error are
|
||||
// suppressed to an empty (or partially-filled) store, like the Python
|
||||
// try/except around the whole loop.
|
||||
func (m *GoalManager) Load() {
|
||||
if _, err := os.Stat(m.StoragePath); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(m.StoragePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dec := json.NewDecoder(bytesReader(data))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return // not an object — Python .items() AttributeError → suppressed
|
||||
}
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
goalData, ok := val.(*OrderedMap)
|
||||
if !ok {
|
||||
return // Goal(**goal_data) TypeError → loop aborts, prior kept
|
||||
}
|
||||
if _, ok := goalData.Get("goal_id"); !ok {
|
||||
return // required field missing → TypeError → abort
|
||||
}
|
||||
if _, ok := goalData.Get("description"); !ok {
|
||||
return
|
||||
}
|
||||
// unknown keys also raise TypeError(**kwargs) → abort mid-loop
|
||||
for _, k := range goalData.Keys {
|
||||
switch k {
|
||||
case "goal_id", "description", "stop_conditions", "verification_method",
|
||||
"status", "created_at", "completed_at", "evidence":
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
m.Goals.Set(key, goalFromDict(goalData))
|
||||
}
|
||||
}
|
||||
|
||||
// Save — GoalManager._save: makedirs + json.dump(data, f, indent=2), no
|
||||
// trailing newline, ensure_ascii escaping.
|
||||
func (m *GoalManager) Save() {
|
||||
os.MkdirAll(filepath.Dir(m.StoragePath), 0o755)
|
||||
data := NewOrderedMap()
|
||||
for _, kv := range m.Goals.Entries() {
|
||||
data.Set(kv.Key, kv.Value.(*Goal).toDict())
|
||||
}
|
||||
os.WriteFile(m.StoragePath, []byte(encodePythonJSON(data, 0)), 0o644)
|
||||
}
|
||||
|
||||
// SetGoal — GoalManager.set_goal.
|
||||
func (m *GoalManager) SetGoal(description string, stopConditions []string, verificationMethod string) *Goal {
|
||||
if stopConditions == nil {
|
||||
stopConditions = []string{}
|
||||
}
|
||||
goal := &Goal{
|
||||
GoalID: NewGoalID(),
|
||||
Description: description,
|
||||
StopConditions: stopConditions,
|
||||
VerificationMethod: verificationMethod,
|
||||
Status: "active",
|
||||
CreatedAt: ClockNow(),
|
||||
}
|
||||
m.Goals.Set(goal.GoalID, goal)
|
||||
m.Save()
|
||||
return goal
|
||||
}
|
||||
|
||||
// GetActiveGoal — GoalManager.get_active_goal.
|
||||
func (m *GoalManager) GetActiveGoal() *Goal {
|
||||
for _, kv := range m.Goals.Entries() {
|
||||
g := kv.Value.(*Goal)
|
||||
if g.Status == "active" {
|
||||
return g
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckGoal — GoalManager.check_goal.
|
||||
func (m *GoalManager) CheckGoal(evidence *string) *OrderedMap {
|
||||
goal := m.GetActiveGoal()
|
||||
if goal == nil {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "no_goal"},
|
||||
KV{"message", "No active goal set"},
|
||||
)
|
||||
}
|
||||
if evidence != nil {
|
||||
goal.Evidence = append(goal.Evidence, *evidence)
|
||||
}
|
||||
if len(goal.StopConditions) > 0 {
|
||||
verification := m.VerifyWithLlm(goal)
|
||||
if pyTruthy(omGet2(verification, "achieved")) {
|
||||
goal.Status = "achieved"
|
||||
now := ClockNow()
|
||||
goal.CompletedAt = &now
|
||||
goal.Evidence = append(goal.Evidence, "LLM verification: "+pyStr(dictDefault(verification, "reason", "")))
|
||||
m.Save()
|
||||
return NewOrderedMap(
|
||||
KV{"status", "achieved"},
|
||||
KV{"message", dictDefault(verification, "reason", "Goal achieved")},
|
||||
KV{"goal", goal.toDict()},
|
||||
KV{"verification", verification},
|
||||
)
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "in_progress"},
|
||||
KV{"message", dictDefault(verification, "reason", "Goal not yet achieved")},
|
||||
KV{"goal", goal.toDict()},
|
||||
KV{"verification", verification},
|
||||
)
|
||||
}
|
||||
met := 0
|
||||
for _, c := range goal.StopConditions {
|
||||
for _, e := range goal.Evidence {
|
||||
if strings.Contains(strings.ToLower(e), strings.ToLower(c)) {
|
||||
met++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "in_progress"},
|
||||
KV{"message", "Goal not yet achieved"},
|
||||
KV{"goal", goal.toDict()},
|
||||
KV{"conditions_met", met},
|
||||
KV{"conditions_total", len(goal.StopConditions)},
|
||||
)
|
||||
}
|
||||
|
||||
// VerifyWithLlm — GoalManager._verify_with_llm: builds the judge prompt,
|
||||
// POSTs it, extracts the JSON verdict, and falls back to
|
||||
// VerifyWithKeywords on any parse or transport failure.
|
||||
func (m *GoalManager) VerifyWithLlm(goal *Goal) *OrderedMap {
|
||||
ollamaURL := os.Getenv("OLLAMA_URL")
|
||||
if ollamaURL == "" {
|
||||
ollamaURL = "http://localhost:11434"
|
||||
}
|
||||
verifyModel := os.Getenv("RUNE_VERIFY_MODEL")
|
||||
if verifyModel == "" {
|
||||
verifyModel = "qwen2.5:3b"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("You are a goal verification judge. Your job is to determine if a goal has been achieved based on the evidence provided.\n\n")
|
||||
sb.WriteString("GOAL: " + goal.Description + "\n\n")
|
||||
sb.WriteString("STOP CONDITIONS:\n")
|
||||
for i, c := range goal.StopConditions {
|
||||
if i > 0 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("- " + c)
|
||||
}
|
||||
sb.WriteString("\n\nEVIDENCE:\n")
|
||||
if len(goal.Evidence) == 0 {
|
||||
sb.WriteString("No evidence provided yet.")
|
||||
} else {
|
||||
for i, e := range goal.Evidence {
|
||||
if i > 0 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("- " + e)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n\nBased on the goal, stop conditions, and evidence, determine:\n")
|
||||
sb.WriteString("1. Has the goal been achieved? (yes/no)\n")
|
||||
sb.WriteString("2. What is your reasoning?\n")
|
||||
sb.WriteString("3. Which conditions are met and which are not?\n\n")
|
||||
sb.WriteString(`Respond in JSON format:
|
||||
{"achieved": true/false, "reason": "your reasoning", "conditions_met": ["condition1"], "conditions_unmet": ["condition2"]}`)
|
||||
prompt := sb.String()
|
||||
|
||||
payload := map[string]any{
|
||||
"model": verifyModel,
|
||||
"prompt": prompt,
|
||||
"stream": false,
|
||||
"options": map[string]any{
|
||||
"temperature": 0.1,
|
||||
"num_predict": 200,
|
||||
"num_ctx": CtxBudget(verifyModel),
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return m.VerifyWithKeywords(goal)
|
||||
}
|
||||
lastRequest.Store(string(body))
|
||||
respBody, err := LLMPost(ollamaURL+"/api/generate", body)
|
||||
if err != nil {
|
||||
return m.VerifyWithKeywords(goal)
|
||||
}
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return m.VerifyWithKeywords(goal)
|
||||
}
|
||||
response, ok := result["response"].(string)
|
||||
if !ok {
|
||||
return m.VerifyWithKeywords(goal) // .strip() on non-str raises → fallback
|
||||
}
|
||||
response = strings.TrimSpace(response)
|
||||
re := regexp.MustCompile(`(?s)\{.*\}`)
|
||||
match := re.FindString(response)
|
||||
if match == "" {
|
||||
return m.VerifyWithKeywords(goal)
|
||||
}
|
||||
var verification map[string]any
|
||||
if err := json.Unmarshal([]byte(match), &verification); err != nil {
|
||||
return m.VerifyWithKeywords(goal)
|
||||
}
|
||||
return OrderedMapFromMap(verification)
|
||||
}
|
||||
|
||||
// lastRequest records the most recent LLM payload (truth battery captures
|
||||
// the request the Python oracle sent).
|
||||
var lastRequest atomicString
|
||||
|
||||
// lastURL records the most recent LLM endpoint URL (truth battery capture).
|
||||
var lastURL atomicString
|
||||
|
||||
// VerifyWithKeywords — GoalManager._verify_with_keywords.
|
||||
func (m *GoalManager) VerifyWithKeywords(goal *Goal) *OrderedMap {
|
||||
if len(goal.StopConditions) == 0 {
|
||||
return NewOrderedMap(
|
||||
KV{"achieved", false},
|
||||
KV{"reason", "No stop conditions defined"},
|
||||
)
|
||||
}
|
||||
met := []any{}
|
||||
unmet := []any{}
|
||||
for _, condition := range goal.StopConditions {
|
||||
condMet := false
|
||||
for _, ev := range goal.Evidence {
|
||||
if strings.Contains(strings.ToLower(ev), strings.ToLower(condition)) {
|
||||
condMet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if condMet {
|
||||
met = append(met, condition)
|
||||
} else {
|
||||
unmet = append(unmet, condition)
|
||||
}
|
||||
}
|
||||
achieved := len(unmet) == 0
|
||||
reason := fmt.Sprintf("Unmet: %s", strings.Join(toStrings(unmet), ", "))
|
||||
if achieved {
|
||||
reason = "All conditions met"
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"achieved", achieved},
|
||||
KV{"reason", reason},
|
||||
KV{"conditions_met", met},
|
||||
KV{"conditions_unmet", unmet},
|
||||
)
|
||||
}
|
||||
|
||||
// AchieveGoal — GoalManager.achieve_goal.
|
||||
func (m *GoalManager) AchieveGoal(evidence *string) *OrderedMap {
|
||||
goal := m.GetActiveGoal()
|
||||
if goal == nil {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "no_goal"},
|
||||
KV{"message", "No active goal set"},
|
||||
)
|
||||
}
|
||||
if evidence != nil {
|
||||
goal.Evidence = append(goal.Evidence, *evidence)
|
||||
}
|
||||
goal.Status = "achieved"
|
||||
now := ClockNow()
|
||||
goal.CompletedAt = &now
|
||||
m.Save()
|
||||
return NewOrderedMap(
|
||||
KV{"status", "achieved"},
|
||||
KV{"message", "Goal marked as achieved"},
|
||||
KV{"goal", goal.toDict()},
|
||||
)
|
||||
}
|
||||
|
||||
// FailGoal — GoalManager.fail_goal.
|
||||
func (m *GoalManager) FailGoal(reason *string) *OrderedMap {
|
||||
goal := m.GetActiveGoal()
|
||||
if goal == nil {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "no_goal"},
|
||||
KV{"message", "No active goal set"},
|
||||
)
|
||||
}
|
||||
goal.Status = "failed"
|
||||
if reason != nil {
|
||||
goal.Evidence = append(goal.Evidence, "Failed: "+*reason)
|
||||
}
|
||||
m.Save()
|
||||
return NewOrderedMap(
|
||||
KV{"status", "failed"},
|
||||
KV{"message", "Goal marked as failed"},
|
||||
KV{"goal", goal.toDict()},
|
||||
)
|
||||
}
|
||||
|
||||
// ClearGoal — GoalManager.clear_goal.
|
||||
func (m *GoalManager) ClearGoal() *OrderedMap {
|
||||
goal := m.GetActiveGoal()
|
||||
if goal == nil {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "no_goal"},
|
||||
KV{"message", "No active goal set"},
|
||||
)
|
||||
}
|
||||
goal.Status = "abandoned"
|
||||
m.Save()
|
||||
return NewOrderedMap(
|
||||
KV{"status", "abandoned"},
|
||||
KV{"message", "Goal cleared"},
|
||||
)
|
||||
}
|
||||
|
||||
// ListGoals — GoalManager.list_goals.
|
||||
func (m *GoalManager) ListGoals() []any {
|
||||
out := []any{}
|
||||
for _, kv := range m.Goals.Entries() {
|
||||
out = append(out, kv.Value.(*Goal).toDict())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------- module-level surface (global manager singleton) ----------
|
||||
|
||||
var (
|
||||
managerMu sync.Mutex
|
||||
globalManager *GoalManager
|
||||
)
|
||||
|
||||
// getGoalManager — module get_goal_manager: lazily-built global manager on
|
||||
// the default storage path (~/.rune/goals.json).
|
||||
func getGoalManager() *GoalManager {
|
||||
managerMu.Lock()
|
||||
defer managerMu.Unlock()
|
||||
if globalManager == nil {
|
||||
globalManager = NewGoalManager(filepath.Join(HomeDir(), ".rune", "goals.json"))
|
||||
globalManager.Load()
|
||||
}
|
||||
return globalManager
|
||||
}
|
||||
|
||||
// setGoal — module set_goal.
|
||||
func setGoal(description string, stopConditions []string, verificationMethod string) *OrderedMap {
|
||||
return getGoalManager().SetGoal(description, stopConditions, verificationMethod).toDict()
|
||||
}
|
||||
|
||||
// checkGoal — module check_goal.
|
||||
func checkGoal(evidence *string) *OrderedMap {
|
||||
return getGoalManager().CheckGoal(evidence)
|
||||
}
|
||||
|
||||
// achieveGoal — module achieve_goal.
|
||||
func achieveGoal(evidence *string) *OrderedMap {
|
||||
return getGoalManager().AchieveGoal(evidence)
|
||||
}
|
||||
|
||||
// clearGoal — module clear_goal.
|
||||
func clearGoal() *OrderedMap {
|
||||
return getGoalManager().ClearGoal()
|
||||
}
|
||||
|
||||
// listGoals — module list_goals.
|
||||
func listGoals() []any {
|
||||
return getGoalManager().ListGoals()
|
||||
}
|
||||
|
||||
// ---------- _ctx_budget: model-aware num_ctx ----------
|
||||
|
||||
// defaultProfiles — frozen snapshot of model_registry.DEFAULT_PROFILES
|
||||
// (id -> context_window, local): the only profile fields resolve_context_budget
|
||||
// consults for _ctx_budget(model_id). Pinned by the truth battery.
|
||||
var defaultProfiles = map[string]struct {
|
||||
window int64
|
||||
local bool
|
||||
}{
|
||||
"deterministic": {0, true},
|
||||
"qwen2.5:3b": {32768, true},
|
||||
"qwen3:14b": {32768, true},
|
||||
"qwen3.6:27b": {32768, true},
|
||||
"mimo-v2.5-pro": {128000, false},
|
||||
}
|
||||
|
||||
// outputReserve — model_registry.OUTPUT_RESERVE (task_kind defaults to
|
||||
// "default" in _ctx_budget).
|
||||
var outputReserve = map[string]int64{
|
||||
"default": 2048,
|
||||
"verify": 512,
|
||||
"summarize": 1024,
|
||||
"embed": 0,
|
||||
}
|
||||
|
||||
const toolResultReserve = 1024
|
||||
|
||||
// CtxBudget — _ctx_budget(model_id): resolve_context_budget(model_id,
|
||||
// task_kind="default")["num_ctx"] against the frozen default registry.
|
||||
func CtxBudget(modelID string) int64 {
|
||||
window := int64(8192)
|
||||
local := true
|
||||
if p, ok := defaultProfiles[modelID]; ok {
|
||||
window = p.window
|
||||
local = p.local
|
||||
}
|
||||
if window == 0 {
|
||||
window = 8192
|
||||
}
|
||||
reserve := outputReserve["default"]
|
||||
toolReserve := int64(0)
|
||||
if !local {
|
||||
toolReserve = toolResultReserve
|
||||
}
|
||||
numCtx := window - reserve - toolReserve
|
||||
if numCtx < 1024 {
|
||||
numCtx = 1024
|
||||
}
|
||||
return numCtx
|
||||
}
|
||||
|
||||
// ---------- Python semantics helpers ----------
|
||||
|
||||
// pyTruthy — Python bool(v) for the JSON-decoded verdict "achieved".
|
||||
func pyTruthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
case float64:
|
||||
return t != 0
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f != 0
|
||||
case string:
|
||||
return t != ""
|
||||
case []any:
|
||||
return len(t) > 0
|
||||
case *OrderedMap:
|
||||
return len(t.Keys) > 0
|
||||
case map[string]any:
|
||||
return len(t) > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// pyStr — Python str() for f-string interpolation of verdict fields.
|
||||
func pyStr(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case string:
|
||||
return t
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// pyStrOr — dict.get(key, default) with str() semantics for f-strings.
|
||||
func pyStrOr(m *OrderedMap, key, def string) string {
|
||||
v, ok := m.Get(key)
|
||||
if !ok || v == nil {
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
return pyStr(v)
|
||||
}
|
||||
return pyStr(v)
|
||||
}
|
||||
|
||||
// dictDefault — dict.get(key, default) value-preserving (None stays None).
|
||||
func dictDefault(m *OrderedMap, key string, def any) any {
|
||||
if v, ok := m.Get(key); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func toAnySlice(s []string) []any {
|
||||
out := make([]any, len(s))
|
||||
for i, v := range s {
|
||||
out[i] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toStrings(s []any) []string {
|
||||
out := make([]string, len(s))
|
||||
for i, v := range s {
|
||||
out[i], _ = v.(string)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringSlice(v any) []string {
|
||||
arr, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return toStrings(arr)
|
||||
}
|
||||
|
||||
// atomicString — tiny mutex-guarded string cell.
|
||||
type atomicString struct {
|
||||
mu sync.Mutex
|
||||
v string
|
||||
}
|
||||
|
||||
func (a *atomicString) Store(s string) {
|
||||
a.mu.Lock()
|
||||
a.v = s
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
func (a *atomicString) Load() string {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.v
|
||||
}
|
||||
|
||||
// omGet2 returns the value at key (nil when absent).
|
||||
func omGet2(om *OrderedMap, key string) any {
|
||||
v, _ := om.Get(key)
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Entry point: CLI (goal.py main() port) + truth replay subcommand for the
|
||||
// HARD CI parity gate.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// default seams (goal.go swaps these per truth row).
|
||||
var (
|
||||
defaultClockNow = func() string {
|
||||
return time.Now().UTC().Format("2006-01-02T15:04:05.000000") + "+00:00"
|
||||
}
|
||||
defaultNewGoalID = func() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
s := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
return s[:8]
|
||||
}
|
||||
defaultHomeDir = func() string {
|
||||
h, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return h
|
||||
}
|
||||
defaultLLMPost = func(url string, payload []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(string(payload)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := make([]byte, 0, 4096)
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(tmp)
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
)
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
RunCLI(os.Args, os.Stdout)
|
||||
}
|
||||
|
||||
func truthRowCount() int {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent, ensure_ascii) so _save/_load and CLI output are
|
||||
// byte-parity with the oracle.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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 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(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr for the values in play.
|
||||
func pythonFloat(f float64) string {
|
||||
if f == float64(int64(f)) {
|
||||
return strconv.FormatInt(int64(f), 10)
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// bytesReader — small io.Reader helper for byte slices.
|
||||
func bytesReader(b []byte) io.Reader {
|
||||
return strings.NewReader(string(b))
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// /tmp/gen_goal_truth.py against the live Python oracle) and re-executes
|
||||
// every row through this Go port, comparing results. Exact-string rows
|
||||
// (save file content, CLI stdout) compare byte-for-byte; everything else
|
||||
// compares canonically (sorted-key JSON).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
FrozenID string `json:"frozen_id"`
|
||||
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 {
|
||||
got, exact, err := execRow(&doc, &row)
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("%s: error: %v", row.Name, err))
|
||||
continue
|
||||
}
|
||||
var wantStr, gotStr string
|
||||
if exact {
|
||||
var w string
|
||||
if err := json.Unmarshal(row.Want, &w); err != nil {
|
||||
fails = append(fails, fmt.Sprintf("%s: want not a string: %v", row.Name, err))
|
||||
continue
|
||||
}
|
||||
wantStr, gotStr = w, got.(string)
|
||||
} else {
|
||||
wantStr, err = canonicalFromRaw(row.Want)
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("%s: canonicalize want: %v", row.Name, err))
|
||||
continue
|
||||
}
|
||||
gotStr, err = canonicalFromRaw([]byte(mustJSON(got)))
|
||||
if err != nil {
|
||||
fails = append(fails, fmt.Sprintf("%s: canonicalize got: %v", row.Name, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
if wantStr != gotStr {
|
||||
fails = append(fails, fmt.Sprintf("%s:\n want: %s\n got: %s", row.Name, wantStr, gotStr))
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
// 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(strings.NewReader(string(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
|
||||
defer func() {
|
||||
ClockNow = defaultClockNow
|
||||
NewGoalID = defaultNewGoalID
|
||||
HomeDir = defaultHomeDir
|
||||
LLMPost = defaultLLMPost
|
||||
}()
|
||||
ClockNow = func() string { return doc.FrozenTS }
|
||||
NewGoalID = func() string { return doc.FrozenID }
|
||||
|
||||
name := row.Name
|
||||
switch {
|
||||
case name == "to_dict:full" || name == "to_dict:achieved":
|
||||
g := goalFromDict(asOrdered(omGet(setup, "goal")))
|
||||
return g.toDict(), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "ctx_budget:"):
|
||||
mid, _ := setup.Get2("model_id").(string)
|
||||
return CtxBudget(mid), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "load:"):
|
||||
path := filepath.Join(os.TempDir(), "goal_truth_load_test.json")
|
||||
fileV, hasFile := setup.Get("file")
|
||||
if hasFile && fileV != nil {
|
||||
content, _ := fileV.(string)
|
||||
os.WriteFile(path, []byte(content), 0o644)
|
||||
} else {
|
||||
os.Remove(path)
|
||||
}
|
||||
defer os.Remove(path)
|
||||
m := NewGoalManager(path)
|
||||
m.Load()
|
||||
out := NewOrderedMap()
|
||||
for _, kv := range m.Goals.Entries() {
|
||||
c := canonical(kv.Value.(*Goal).toDict())
|
||||
out.Set(kv.Key, c)
|
||||
}
|
||||
return out, false, nil
|
||||
|
||||
case strings.HasPrefix(name, "save:"):
|
||||
path := filepath.Join(os.TempDir(), "goal_truth_save_test.json")
|
||||
defer os.Remove(path)
|
||||
m := NewGoalManager(path)
|
||||
goals := asOrdered(omGet(setup, "goals"))
|
||||
for _, kv := range goals.Entries() {
|
||||
m.Goals.Set(kv.Key, goalFromDict(asOrdered(kv.Value)))
|
||||
}
|
||||
m.Save()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", true, err
|
||||
}
|
||||
return string(data), true, nil
|
||||
|
||||
case strings.HasPrefix(name, "verify_kw:"):
|
||||
g := goalFromDict(asOrdered(omGet(setup, "goal")))
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
return m.VerifyWithKeywords(g), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "set_goal:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
desc, _ := setup.Get2("description").(string)
|
||||
method, _ := setup.Get2("verification_method").(string)
|
||||
conds := stringSlice(omGet(setup, "stop_conditions"))
|
||||
return m.SetGoal(desc, conds, method).toDict(), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "active:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStore(m, setup)
|
||||
g := m.GetActiveGoal()
|
||||
if g == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
return g.toDict(), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "check_llm:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStoreWith(m, setup, "goal")
|
||||
var evidence *string
|
||||
if ev, ok := setup.Get2("evidence").(string); ok {
|
||||
evidence = &ev
|
||||
}
|
||||
resp := asOrdered(setup.Get2("response"))
|
||||
if errMsgV, ok := resp.Get("error"); ok {
|
||||
LLMPost = func(url string, payload []byte) ([]byte, error) {
|
||||
lastRequest.Store(string(payload))
|
||||
lastURL.Store(url)
|
||||
return nil, fmt.Errorf("%v", errMsgV)
|
||||
}
|
||||
} else {
|
||||
body, _ := resp.Get2("body").(string)
|
||||
LLMPost = func(url string, payload []byte) ([]byte, error) {
|
||||
lastRequest.Store(string(payload))
|
||||
lastURL.Store(url)
|
||||
return []byte(body), nil
|
||||
}
|
||||
}
|
||||
result := m.CheckGoal(evidence)
|
||||
out := NewOrderedMap()
|
||||
for _, kv := range result.Entries() {
|
||||
out.Set(kv.Key, kv.Value)
|
||||
}
|
||||
var req any
|
||||
if raw := lastRequest.Load(); raw != "" {
|
||||
json.Unmarshal([]byte(raw), &req)
|
||||
}
|
||||
out.Set("captured", NewOrderedMap(
|
||||
KV{"llm_request", req},
|
||||
KV{"llm_url", lastURL.Load()},
|
||||
))
|
||||
return out, false, nil
|
||||
|
||||
case strings.HasPrefix(name, "check:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStoreWith(m, setup, "goal")
|
||||
var evidence *string
|
||||
if ev, ok := setup.Get("evidence"); ok {
|
||||
if s, ok2 := ev.(string); ok2 {
|
||||
evidence = &s
|
||||
}
|
||||
}
|
||||
return m.CheckGoal(evidence), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "check_manual:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStoreWith(m, setup, "goal")
|
||||
var evidence *string
|
||||
if ev, ok := setup.Get("evidence"); ok {
|
||||
if s, ok2 := ev.(string); ok2 {
|
||||
evidence = &s
|
||||
}
|
||||
}
|
||||
return m.CheckGoal(evidence), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "achieve:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStoreWith(m, setup, "goal")
|
||||
var evidence *string
|
||||
if ev, ok := setup.Get("evidence"); ok {
|
||||
if s, ok2 := ev.(string); ok2 {
|
||||
evidence = &s
|
||||
}
|
||||
}
|
||||
return m.AchieveGoal(evidence), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "fail:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStoreWith(m, setup, "goal")
|
||||
var reason *string
|
||||
if r, ok := setup.Get("reason"); ok {
|
||||
if s, ok2 := r.(string); ok2 {
|
||||
reason = &s
|
||||
}
|
||||
}
|
||||
return m.FailGoal(reason), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "clear:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStoreWith(m, setup, "goal")
|
||||
return m.ClearGoal(), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "list:"):
|
||||
m := NewGoalManager(filepath.Join(os.TempDir(), "goal_truth_unused.json"))
|
||||
fillStore(m, setup)
|
||||
return m.ListGoals(), false, nil
|
||||
|
||||
case strings.HasPrefix(name, "mod:"):
|
||||
tmpStore := filepath.Join(os.TempDir(), "goal_truth_mod_store.json")
|
||||
os.Remove(tmpStore)
|
||||
m := NewGoalManager(tmpStore)
|
||||
managerMu.Lock()
|
||||
globalManager = m
|
||||
managerMu.Unlock()
|
||||
defer func() {
|
||||
managerMu.Lock()
|
||||
globalManager = nil
|
||||
managerMu.Unlock()
|
||||
os.Remove(tmpStore)
|
||||
}()
|
||||
switch name {
|
||||
case "mod:set_goal":
|
||||
desc, _ := setup.Get2("description").(string)
|
||||
conds := stringSlice(omGet(setup, "stop_conditions"))
|
||||
method, _ := setup.Get2("verification_method").(string)
|
||||
return setGoal(desc, conds, method), false, nil
|
||||
case "mod:check_no_goal":
|
||||
return checkGoal(nil), false, nil
|
||||
case "mod:achieve_no_goal":
|
||||
var ev *string
|
||||
if s, ok := setup.Get2("evidence").(string); ok {
|
||||
ev = &s
|
||||
}
|
||||
return achieveGoal(ev), false, nil
|
||||
case "mod:clear_no_goal":
|
||||
return clearGoal(), false, nil
|
||||
case "mod:list_empty":
|
||||
return listGoals(), false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("unknown mod row %s", name)
|
||||
|
||||
case strings.HasPrefix(name, "cli:"):
|
||||
argvAny, _ := setup.Get2("argv").([]any)
|
||||
argv := []string{"goal.py"}
|
||||
for _, a := range argvAny {
|
||||
s, _ := a.(string)
|
||||
argv = append(argv, s)
|
||||
}
|
||||
tmpStore := filepath.Join(os.TempDir(), "goal_truth_cli_store.json")
|
||||
os.Remove(tmpStore)
|
||||
m := NewGoalManager(tmpStore)
|
||||
fillStore(m, setup)
|
||||
managerMu.Lock()
|
||||
globalManager = m
|
||||
managerMu.Unlock()
|
||||
defer func() {
|
||||
managerMu.Lock()
|
||||
globalManager = nil
|
||||
managerMu.Unlock()
|
||||
os.Remove(tmpStore)
|
||||
}()
|
||||
var sb strings.Builder
|
||||
RunCLI(argv, &sb)
|
||||
return sb.String(), true, nil
|
||||
|
||||
case name == "singleton:default_path":
|
||||
// seed the home seam from the frozen path so the row is
|
||||
// environment-independent; the assertion still checks the
|
||||
// singleton identity and the expanduser-joined path shape
|
||||
wantPath := ""
|
||||
var want map[string]any
|
||||
json.Unmarshal(row.Want, &want)
|
||||
if p, ok := want["path"].(string); ok {
|
||||
wantPath = p
|
||||
}
|
||||
HomeDir = func() string {
|
||||
return strings.TrimSuffix(wantPath, filepath.Join(".rune", "goals.json"))
|
||||
}
|
||||
managerMu.Lock()
|
||||
globalManager = nil
|
||||
managerMu.Unlock()
|
||||
defer func() {
|
||||
managerMu.Lock()
|
||||
globalManager = nil
|
||||
managerMu.Unlock()
|
||||
}()
|
||||
m1 := getGoalManager()
|
||||
m2 := getGoalManager()
|
||||
return NewOrderedMap(
|
||||
KV{"same", m1 == m2},
|
||||
KV{"path", m1.StoragePath},
|
||||
), false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("unknown row %s", name)
|
||||
}
|
||||
|
||||
// fillStore populates m from setup["store"] (goal_id -> goal dict).
|
||||
func fillStore(m *GoalManager, setup *OrderedMap) {
|
||||
storeV, ok := setup.Get("store")
|
||||
if !ok || storeV == nil {
|
||||
return
|
||||
}
|
||||
store := asOrdered(storeV)
|
||||
for _, kv := range store.Entries() {
|
||||
m.Goals.Set(kv.Key, goalFromDict(asOrdered(kv.Value)))
|
||||
}
|
||||
}
|
||||
|
||||
// fillStoreWith handles rows that carry a single "goal" dict (stored under
|
||||
// k1 like the oracle harness did) or a "store" map.
|
||||
func fillStoreWith(m *GoalManager, setup *OrderedMap, key string) {
|
||||
if g, ok := setup.Get(key); ok && g != nil {
|
||||
m.Goals.Set("k1", goalFromDict(asOrdered(g)))
|
||||
return
|
||||
}
|
||||
fillStore(m, setup)
|
||||
}
|
||||
|
||||
func asOrdered(v any) *OrderedMap {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
return t
|
||||
case map[string]any:
|
||||
return OrderedMapFromMap(t)
|
||||
default:
|
||||
return NewOrderedMap()
|
||||
}
|
||||
}
|
||||
|
||||
// canonicalFromRaw — canonical string of a raw JSON document.
|
||||
func canonicalFromRaw(raw []byte) (string, error) {
|
||||
var v any
|
||||
dec := json.NewDecoder(strings.NewReader(string(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, ", ") + "}"
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// omGet returns the value at key (nil when absent).
|
||||
func omGet(om *OrderedMap, key string) any {
|
||||
v, _ := om.Get(key)
|
||||
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,31 @@
|
||||
// Frozen-truth battery: replays every row generated from the live Python
|
||||
// oracle (src/rune/goal.py) through this Go port. 100% match required.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruthParity(t *testing.T) {
|
||||
fails := RunTruth("truth.json")
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
t.Error(f)
|
||||
}
|
||||
return
|
||||
}
|
||||
rows := truthRowCount()
|
||||
if rows <= 0 {
|
||||
t.Fatalf("truth.json unreadable or empty")
|
||||
}
|
||||
t.Logf("PARITY OK: %d truth rows exact-equal vs the Python oracle", rows)
|
||||
}
|
||||
|
||||
func TestTruthRowCount(t *testing.T) {
|
||||
rows := truthRowCount()
|
||||
if rows < 59 {
|
||||
t.Fatalf("expected >= 59 truth rows, got %d", rows)
|
||||
}
|
||||
fmt.Printf("truth rows: %d\n", rows)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"_doc": "FROZEN ORACLE (response_adapter).",
|
||||
"rows": [
|
||||
{
|
||||
"input": {
|
||||
"content": "hello",
|
||||
"model": "qwen"
|
||||
},
|
||||
"want": {
|
||||
"user_text": "hello",
|
||||
"work_events": [],
|
||||
"tool_events": [],
|
||||
"status_updates": [],
|
||||
"warnings": [],
|
||||
"debug_refs": [],
|
||||
"provenance": {
|
||||
"provider": "unknown",
|
||||
"model": "qwen"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"response": "mimo text",
|
||||
"model": "mimo"
|
||||
},
|
||||
"want": {
|
||||
"user_text": "",
|
||||
"work_events": [],
|
||||
"tool_events": [],
|
||||
"status_updates": [],
|
||||
"warnings": [],
|
||||
"debug_refs": [],
|
||||
"provenance": {
|
||||
"provider": "unknown",
|
||||
"model": "mimo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"message": {
|
||||
"content": "local"
|
||||
},
|
||||
"model": "local"
|
||||
},
|
||||
"want": {
|
||||
"user_text": "",
|
||||
"work_events": [],
|
||||
"tool_events": [],
|
||||
"status_updates": [],
|
||||
"warnings": [],
|
||||
"debug_refs": [],
|
||||
"provenance": {
|
||||
"provider": "unknown",
|
||||
"model": "local"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"text": "plain"
|
||||
},
|
||||
"want": {
|
||||
"user_text": "",
|
||||
"work_events": [],
|
||||
"tool_events": [],
|
||||
"status_updates": [],
|
||||
"warnings": [],
|
||||
"debug_refs": [],
|
||||
"provenance": {
|
||||
"provider": "unknown",
|
||||
"model": "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {},
|
||||
"want": {
|
||||
"user_text": "",
|
||||
"work_events": [],
|
||||
"tool_events": [],
|
||||
"status_updates": [],
|
||||
"warnings": [],
|
||||
"debug_refs": [],
|
||||
"provenance": {
|
||||
"provider": "unknown",
|
||||
"model": "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// frozen_response_test.go — #2479 response_adapter parity gate (frozen oracle).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResponseAdapterParity(t *testing.T) {
|
||||
raw, err := os.ReadFile("frozen_response.json")
|
||||
if err != nil {
|
||||
t.Fatalf("frozen_response.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Rows []struct {
|
||||
Input map[string]any `json:"input"`
|
||||
Want map[string]any `json:"want"`
|
||||
} `json:"rows"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for i, c := range doc.Rows {
|
||||
got := parseResponse(c.Input, "")
|
||||
gb, _ := json.Marshal(got)
|
||||
var gm map[string]any
|
||||
_ = json.Unmarshal(gb, &gm)
|
||||
wb, _ := json.Marshal(c.Want)
|
||||
var wm map[string]any
|
||||
_ = json.Unmarshal(wb, &wm)
|
||||
// provenance may carry timestamps in future; compare stable fields
|
||||
for _, k := range []string{"user_text", "work_events", "tool_events", "status_updates", "warnings", "debug_refs"} {
|
||||
gj, _ := json.Marshal(gm[k])
|
||||
wj, _ := json.Marshal(wm[k])
|
||||
if string(gj) != string(wj) {
|
||||
t.Errorf("row %d %s: got %s want %s", i, k, gj, wj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,9 @@ func parseQwen(resp map[string]any) RuneResponse {
|
||||
content, _ := resp["content"].(string)
|
||||
matches := thinkPattern.FindAllStringSubmatch(content, -1)
|
||||
visible := strings.TrimSpace(thinkPattern.ReplaceAllString(content, ""))
|
||||
r := RuneResponse{UserText: visible}
|
||||
r := RuneResponse{UserText: visible,
|
||||
WorkEvents: []any{}, ToolEvents: []any{}, StatusUpdates: []any{},
|
||||
Warnings: []any{}, DebugRefs: []any{}}
|
||||
words := 0
|
||||
for _, m := range matches {
|
||||
r.DebugRefs = append(r.DebugRefs, map[string]any{"type": "think", "content": m[1]})
|
||||
@@ -49,7 +51,9 @@ func parseQwen(resp map[string]any) RuneResponse {
|
||||
func parseMimo(resp map[string]any) RuneResponse {
|
||||
reasoning, _ := resp["reasoning_content"].(string)
|
||||
content, _ := resp["content"].(string)
|
||||
r := RuneResponse{UserText: content}
|
||||
r := RuneResponse{UserText: content,
|
||||
WorkEvents: []any{}, ToolEvents: []any{}, StatusUpdates: []any{},
|
||||
Warnings: []any{}, DebugRefs: []any{}}
|
||||
if reasoning != "" {
|
||||
r.DebugRefs = []any{map[string]any{"type": "reasoning", "content": reasoning}}
|
||||
}
|
||||
@@ -79,7 +83,33 @@ func parsePlain(resp map[string]any) RuneResponse {
|
||||
Provenance: map[string]any{"provider": resp["provider"], "model": resp["model"]}}
|
||||
}
|
||||
|
||||
func normalizeEmpty(r RuneResponse) RuneResponse {
|
||||
if r.WorkEvents == nil {
|
||||
r.WorkEvents = []any{}
|
||||
}
|
||||
if r.ToolEvents == nil {
|
||||
r.ToolEvents = []any{}
|
||||
}
|
||||
if r.StatusUpdates == nil {
|
||||
r.StatusUpdates = []any{}
|
||||
}
|
||||
if r.Warnings == nil {
|
||||
r.Warnings = []any{}
|
||||
}
|
||||
if r.DebugRefs == nil {
|
||||
r.DebugRefs = []any{}
|
||||
}
|
||||
if r.Provenance == nil {
|
||||
r.Provenance = map[string]any{}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func parseResponse(resp map[string]any, provider string) RuneResponse {
|
||||
return normalizeEmpty(parseResponseInner(resp, provider))
|
||||
}
|
||||
|
||||
func parseResponseInner(resp map[string]any, provider string) RuneResponse {
|
||||
switch provider {
|
||||
case "qwen":
|
||||
return parseQwen(resp)
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.183.0"
|
||||
__version__ = "0.203.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust capability_broker selection core (dichotomy
|
||||
debt queue batch 1, #2479 tranche). Mirrors cs_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cs_select_for_job({tools, description, need_tools}) -> selection dict
|
||||
* cs_find_capability({tools, cards, query, top_k}) -> results list
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with BROKER_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "capability_broker"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("BROKER_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcapability_broker.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class BrokerUnavailable(FileNotFoundError):
|
||||
"""The Rust selection core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise BrokerUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cs_select_for_job.argtypes = [ctypes.c_void_p]
|
||||
lib.cs_select_for_job.restype = ctypes.c_void_p
|
||||
lib.cs_find_capability.argtypes = [ctypes.c_void_p]
|
||||
lib.cs_find_capability.restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def select_for_job(tools: list, description: str, need_tools: bool) -> dict:
|
||||
return _call("cs_select_for_job", {
|
||||
"tools": tools, "description": description, "need_tools": need_tools,
|
||||
})
|
||||
|
||||
|
||||
def find_capability(tools: list, cards: list, query: str, top_k: int = 5) -> list:
|
||||
return _call("cs_find_capability", {
|
||||
"tools": tools, "cards": cards, "query": query, "top_k": top_k,
|
||||
})
|
||||
Loaded 100 of 320 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user