Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b3ee1db5 | ||
|
|
2016fe0433 | ||
|
|
2a3526db1e | ||
|
|
8b51a6c824 | ||
|
|
bf9750ea5c | ||
|
|
976609b0b9 | ||
|
|
08757105e7 | ||
|
|
3e16ece001 | ||
|
|
f5dfd4e6b9 | ||
|
|
f82fed98fb | ||
|
|
55ad089c3f | ||
|
|
c8426bdddb | ||
|
|
27716e0243 | ||
|
|
caeaab3c1a | ||
|
|
a95c6ce129 | ||
|
|
9efb7d8697 | ||
|
|
031e054268 | ||
|
|
1ce62e2730 | ||
|
|
fffce59894 | ||
|
|
33692e090b | ||
|
|
28dc24b481 | ||
|
|
57290880ad | ||
|
|
40a1bd8f6a | ||
|
|
c31886823a |
No files matched your search
@@ -237,6 +237,9 @@ 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: 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.192.0**
|
||||
**Current release: v0.202.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 |
|
||||
@@ -39598,3 +39598,190 @@
|
||||
{"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"}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.192.0"
|
||||
version = "0.202.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -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,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,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)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.192.0"
|
||||
__version__ = "0.202.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
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.192.0"' in rune_version,
|
||||
'__version__ = "0.202.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust cognitive_budgeter core (dichotomy debt queue
|
||||
batch 5, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cb_calculate({salience, context}) -> budget dict
|
||||
|
||||
Override the .so with CB_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "cognitive_budgeter"
|
||||
_LIB_PATH = Path(os.environ.get("CB_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcognitive_budgeter.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class BudgeterUnavailable(FileNotFoundError):
|
||||
"""The Rust budget core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise BudgeterUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cb_calculate.argtypes = [ctypes.c_void_p]
|
||||
lib.cb_calculate.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def calculate(salience: dict, context: dict | None = None) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps({"salience": salience, "context": context or {}}).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.cb_calculate(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("cb_calculate returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "cb_calculate failed"))
|
||||
return doc["value"]
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_intent core (dichotomy wave,
|
||||
#2479 tranche). Mirrors cm_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* ci_resolve({text, origin_context_id?, context}) ->
|
||||
{"intent": {...ConstructionIntent dict, intent_id ""}}
|
||||
|
||||
The context passed is the POST-mutation state (the facade advances
|
||||
next_turn/record_turn before delegating); the kernel is pure.
|
||||
|
||||
Override the .so with CI_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "construction_intent"
|
||||
_LIB_PATH = Path(os.environ.get("CI_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_intent.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ConstructionIntentCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust intent 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 ConstructionIntentCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.ci_resolve.argtypes = [ctypes.c_void_p]
|
||||
lib.ci_resolve.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.ci_resolve(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("ci_resolve returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "ci_resolve failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def resolve(text: str, context, origin_context_id: str | None = None) -> dict:
|
||||
"""Run the Rust kernel on the POST-mutation context. `context` is the
|
||||
live ConversationContext (already advanced by the facade)."""
|
||||
payload = {
|
||||
"text": text,
|
||||
"origin_context_id": origin_context_id,
|
||||
"context": {
|
||||
"turn": context.turn,
|
||||
"recent_turns": [dict(t) for t in context.recent_turns],
|
||||
"pending_proposals": [
|
||||
{"proposal_id": p.proposal_id, "summary": p.summary,
|
||||
"created_turn": p.created_turn, "superseded": p.superseded}
|
||||
for p in context.pending_proposals],
|
||||
"active_project": context.active_project,
|
||||
"active_task_id": context.active_task_id,
|
||||
},
|
||||
}
|
||||
return _call(payload)["intent"]
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_mode classify core (dichotomy
|
||||
debt queue batch 2, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cm_classify({objective, target_project?, project_exists?,
|
||||
affected_surface?, explicit_override?,
|
||||
override_authority?, model_proposal?}) -> decision dict
|
||||
|
||||
Override the .so with CM_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "construction_mode"
|
||||
_LIB_PATH = Path(os.environ.get("CM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_mode.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ConstructionModeUnavailable(FileNotFoundError):
|
||||
"""The Rust classify core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise ConstructionModeUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cm_classify.argtypes = [ctypes.c_void_p]
|
||||
lib.cm_classify.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = lib.cm_classify(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError("cm_classify returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
# free via any sibling free symbol already in-process (capability_scoring)
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass # leak-on-fallback only if capability_scoring absent; bounded
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", "cm_classify failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
_VALID_MODES = ("NOT_CONSTRUCTION", "LOCAL_CHANGE", "EXISTING_PROJECT_CHANGE", "PROJECT_BUILD", "UNKNOWN")
|
||||
|
||||
def classify(objective: str, **kwargs) -> dict:
|
||||
"""kwargs: target_project, project_exists, affected_surface,
|
||||
explicit_override, override_authority, model_proposal"""
|
||||
ov = kwargs.get("explicit_override")
|
||||
if ov is not None and ov not in _VALID_MODES:
|
||||
raise ValueError(f"override must be one of {_VALID_MODES}, got {ov!r}")
|
||||
payload = {"objective": objective}
|
||||
for k, v in kwargs.items():
|
||||
if v is not None:
|
||||
payload[k] = v
|
||||
return _call(payload)
|
||||
@@ -60,7 +60,20 @@ TIERS = {
|
||||
|
||||
|
||||
def calculate_budget(salience: dict, context: dict = None) -> CognitiveBudget:
|
||||
"""Calculate cognitive budget based on salience and context."""
|
||||
"""Calculate cognitive budget based on salience and context.
|
||||
|
||||
Dichotomy batch 5: delegates to the Rust budget core when built
|
||||
(RUNE_CB_CORE=python pins the Python oracle). The Python implementation
|
||||
below is the strangler fallback and the parity authority.
|
||||
"""
|
||||
if os.environ.get("RUNE_CB_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import cb_loader
|
||||
if cb_loader.available():
|
||||
out = cb_loader.calculate(salience, context or {})
|
||||
return CognitiveBudget(**out)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ incantations.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
@@ -257,6 +258,30 @@ def resolve_construction_intent(
|
||||
"""
|
||||
context.next_turn()
|
||||
context.record_turn("user", text)
|
||||
if os.environ.get("RUNE_CI_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import ci_loader
|
||||
if ci_loader.available():
|
||||
out = ci_loader.resolve(text, context,
|
||||
origin_context_id=origin_context_id)
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=out["objective"],
|
||||
requested_effect=out["requested_effect"],
|
||||
resolved_target=out["resolved_target"],
|
||||
resolved_referent=out["resolved_referent"],
|
||||
origin_context_id=out["origin_context_id"],
|
||||
action_intent=out["action_intent"],
|
||||
approval_state=out["approval_state"],
|
||||
confidence=out["confidence"],
|
||||
ambiguity=out["ambiguity"],
|
||||
consequence_level=out["consequence_level"],
|
||||
relevant_constraints=out["relevant_constraints"],
|
||||
classification_evidence=out["classification_evidence"],
|
||||
probe_questions=out["probe_questions"],
|
||||
)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
t = text.strip()
|
||||
low = t.lower()
|
||||
ev: list[str] = []
|
||||
|
||||
@@ -39,6 +39,7 @@ Model interpretation may be PROPOSED (recorded, PENDING — never authority,
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@@ -215,7 +216,23 @@ def classify_mode(
|
||||
`explicit_override` — operator/builder authority feed (§11/§23):
|
||||
validated against known modes; provenance recorded. Override feeds the
|
||||
SAME machinery — it never bypasses the evidence record.
|
||||
|
||||
Dichotomy batch 2: delegates to the Rust classify core when built
|
||||
(RUNE_CM_CORE=python pins the Python oracle). The Python implementation
|
||||
below is the strangler fallback and the parity authority.
|
||||
"""
|
||||
if os.environ.get("RUNE_CM_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import cm_loader
|
||||
if cm_loader.available():
|
||||
out = cm_loader.classify(objective, target_project=target_project,
|
||||
project_exists=project_exists, affected_surface=affected_surface,
|
||||
explicit_override=explicit_override,
|
||||
override_authority=override_authority, model_proposal=model_proposal)
|
||||
from rune.construction_mode import ConstructionMode as CM
|
||||
return CM.from_dict(out)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
ev: list[str] = []
|
||||
obj = objective or ""
|
||||
mode = None
|
||||
|
||||
+116
-2
@@ -19,6 +19,27 @@ logger = logging.getLogger(__name__)
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
|
||||
try:
|
||||
from . import contracts_loader
|
||||
except ImportError: # direct script execution (canary `import contracts`)
|
||||
try:
|
||||
import contracts_loader # type: ignore[no-redef]
|
||||
except ImportError: # noqa: S110 — no loader on path → Python oracle below
|
||||
contracts_loader = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _ct_core():
|
||||
"""The Rust runner-core probe (strangler law). RUNE_CONTRACTS_CORE=python
|
||||
pins the Python fallback — the pin is honored inside
|
||||
contracts_loader.core so every consumer gets it. Returns the loader
|
||||
MODULE (never the raw CDLL)."""
|
||||
if contracts_loader is None:
|
||||
return None
|
||||
try:
|
||||
return contracts_loader if contracts_loader.core() is not None else None
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
return None
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CONTRACT DEFINITION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -430,8 +451,93 @@ class ContractResult:
|
||||
actual_value: str = ""
|
||||
|
||||
|
||||
def _project(contract: Contract, result) -> dict:
|
||||
"""JSON-safe projection of a call/attr result for the Rust verdict
|
||||
core — computed with the exact oracle predicates (isinstance,
|
||||
hasattr __len__, len, keys) so subclass semantics stay Python."""
|
||||
return {
|
||||
"type": type(result).__name__,
|
||||
"none": result is None,
|
||||
"type_ok": (isinstance(result, contract.expect_type)
|
||||
if contract.expect_type is not None else None),
|
||||
"is_dict": isinstance(result, dict),
|
||||
"keys": list(result.keys()) if isinstance(result, dict) else None,
|
||||
"sized": hasattr(result, "__len__"),
|
||||
"len": len(result) if hasattr(result, "__len__") else None,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_and_project(contract: Contract):
|
||||
"""The resolution half of run_contract (IO side, stays Python):
|
||||
dynamic import, getattr, the call. Returns
|
||||
(phase, phase_error, callable_flag, projection|None)."""
|
||||
try:
|
||||
mod = __import__(contract.module)
|
||||
except ImportError as e:
|
||||
return ("import_error", str(e), False, None)
|
||||
except Exception as e: # noqa: BLE001 — mirrors the oracle's outer catch
|
||||
return ("crash", f"{type(e).__name__}: {e}", False, None)
|
||||
func = getattr(mod, contract.function, None)
|
||||
if func is None:
|
||||
return ("not_found", "", False, None)
|
||||
callable_flag = callable(func)
|
||||
if callable_flag and not contract.expect_callable:
|
||||
try:
|
||||
result = func(*contract.args, **contract.kwargs)
|
||||
except Exception as e: # noqa: BLE001 — oracle outer catch
|
||||
return ("crash", f"{type(e).__name__}: {e}", callable_flag, None)
|
||||
try:
|
||||
return ("found", "", callable_flag, _project(contract, result))
|
||||
except Exception as e: # noqa: BLE001 — projection failure = crash
|
||||
return ("crash", f"{type(e).__name__}: {e}", callable_flag, None)
|
||||
if contract.expect_callable:
|
||||
return ("found", "", callable_flag, None)
|
||||
try:
|
||||
result = func # plain attribute value (non-callable)
|
||||
return ("found", "", callable_flag, _project(contract, result))
|
||||
except Exception as e: # noqa: BLE001
|
||||
return ("crash", f"{type(e).__name__}: {e}", callable_flag, None)
|
||||
|
||||
|
||||
def run_contract(contract: Contract) -> ContractResult:
|
||||
"""Run a single contract test."""
|
||||
"""Run a single contract test. Delegates the VERDICT to the Rust
|
||||
runner core (dichotomy wave); the resolution (import/getattr/call)
|
||||
stays Python. Falls back to the Python oracle when the core is
|
||||
unavailable or fails."""
|
||||
core = _ct_core()
|
||||
if core is not None:
|
||||
try:
|
||||
phase, err, callable_flag, proj = _resolve_and_project(contract)
|
||||
out = core.verdict({
|
||||
"name": contract.name,
|
||||
"module": contract.module,
|
||||
"function": contract.function,
|
||||
"expect_type": (contract.expect_type.__name__
|
||||
if contract.expect_type is not None else None),
|
||||
"expect_keys": contract.expect_keys,
|
||||
"expect_len": contract.expect_len,
|
||||
"expect_not_none": contract.expect_not_none,
|
||||
"expect_callable": contract.expect_callable,
|
||||
"skip_on_import_error": contract.skip_on_import_error,
|
||||
"phase": phase,
|
||||
"phase_error": err,
|
||||
"callable": callable_flag,
|
||||
"projection": proj,
|
||||
})
|
||||
return ContractResult(
|
||||
name=out["name"],
|
||||
passed=out["passed"],
|
||||
error=out["error"],
|
||||
actual_type=out["actual_type"],
|
||||
actual_value=out["actual_value"],
|
||||
)
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
logger.debug("rust verdict core failed; using python oracle", exc_info=True)
|
||||
return _run_contract_python(contract)
|
||||
|
||||
|
||||
def _run_contract_python(contract: Contract) -> ContractResult:
|
||||
"""Run a single contract test (Python oracle)."""
|
||||
try:
|
||||
# Import module
|
||||
try:
|
||||
@@ -554,9 +660,17 @@ def load_signatures() -> dict:
|
||||
|
||||
|
||||
def check_signature_drift() -> list[str]:
|
||||
"""Check if any function signatures have changed since last save."""
|
||||
"""Check if any function signatures have changed since last save.
|
||||
The compare delegates to the Rust core (ct_drift); capture and load
|
||||
stay Python. Falls back to the inline compare on kernel failure."""
|
||||
current = capture_signatures()
|
||||
saved = load_signatures()
|
||||
core = _ct_core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.drift(current, saved)
|
||||
except Exception: # noqa: BLE001 — kernel failure → compare below
|
||||
logger.debug("rust drift core failed; using python compare", exc_info=True)
|
||||
drifts = []
|
||||
for key, sig in current.items():
|
||||
if key in saved and saved[key] != sig:
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust contracts runner core (dichotomy wave, #2479
|
||||
tranche). Mirrors mse_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ct_verdict({name, module, function, expect_*, skip_on_import_error,
|
||||
phase, phase_error, callable, projection}) -> ContractResult fields
|
||||
* ct_drift({current, saved}) -> drift lines
|
||||
* ct_registry() -> the 42 frozen contract specs
|
||||
|
||||
The IO (dynamic module import + call, signature capture via inspect, the
|
||||
signatures file) stays in contracts.py; this core owns the verdict branch
|
||||
tree and the drift compare. Override the .so with CONTRACTS_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" / "contracts"
|
||||
_LIB_PATH = Path(os.environ.get("CONTRACTS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcontracts.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
_SYMBOLS = (
|
||||
"ct_verdict",
|
||||
"ct_drift",
|
||||
"ct_registry",
|
||||
)
|
||||
|
||||
|
||||
class ContractsCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust runner core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def core():
|
||||
"""The shared kernel probe (strangler law): the delegation entry the
|
||||
facade uses. RUNE_CONTRACTS_CORE=python pins the fallback — THIS is
|
||||
where the pin is honored, so every consumer gets it."""
|
||||
if os.environ.get("RUNE_CONTRACTS_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
return _load()
|
||||
except Exception: # noqa: BLE001 — build/loader failures mean fallback
|
||||
return None
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise ContractsCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
# FFI segfault law: explicit argtypes/restype (c_void_p for returned
|
||||
# char* — the default c_int truncates 64-bit pointers).
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
_free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
"""Doc-envelope free via capability_scoring's cs_free_string — NEVER
|
||||
re-export a free symbol from this crate (duplicate symbols break the
|
||||
cdylib link)."""
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string.restype = None
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def verdict(payload: dict) -> dict:
|
||||
return _call("ct_verdict", payload)
|
||||
|
||||
|
||||
def drift(current: dict, saved: dict) -> list:
|
||||
return _call("ct_drift", {"current": current, "saved": saved})
|
||||
|
||||
|
||||
def registry() -> list:
|
||||
return _call("ct_registry", {})
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust drift_ledger core (dichotomy debt queue
|
||||
batch 3, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* dl_sha({text}) -> hex
|
||||
* dl_diff({ref, cur}) -> summary string
|
||||
* dl_check({label, prev_size, cur_size, anchor_size?, anchor_sha?, cur_sha?}) -> {alert, pct, limit}
|
||||
* dl_auto_fix({label, anchored, has_last_good}) -> {action, reason}
|
||||
|
||||
Override the .so with DL_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "drift_ledger"
|
||||
_LIB_PATH = Path(os.environ.get("DL_RUST_LIB", _CRATE_DIR / "target" / "release" / "libdrift_ledger.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class DriftLedgerUnavailable(FileNotFoundError):
|
||||
"""The Rust drift core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise DriftLedgerUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("dl_sha", "dl_diff", "dl_check", "dl_auto_fix"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def sha(text: str) -> str:
|
||||
return _call("dl_sha", {"text": text})
|
||||
|
||||
|
||||
def diff_blocks(ref: str, cur: str) -> str:
|
||||
return _call("dl_diff", {"ref": ref, "cur": cur})
|
||||
@@ -64,6 +64,14 @@ BLOCKS = ("constitution", "persona", "human", "operating", "project")
|
||||
|
||||
|
||||
def _sha(text):
|
||||
# Dichotomy batch 3: Rust core first (RUNE_DL_CORE=python pins fallback).
|
||||
if os.environ.get("RUNE_DL_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.sha(text)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
@@ -305,6 +313,13 @@ def auto_fix(label, ledger, strict):
|
||||
def diff_blocks(ref, cur):
|
||||
"""Root-cause: what changed between the last-good content and now.
|
||||
Returns a short list of added/removed lines (the drift's cause)."""
|
||||
if os.environ.get("RUNE_DL_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import dl_loader
|
||||
if dl_loader.available():
|
||||
return dl_loader.diff_blocks(ref, cur)
|
||||
except Exception:
|
||||
pass # fallback below
|
||||
if not ref:
|
||||
return "no prior content (first drift from empty baseline)"
|
||||
try:
|
||||
|
||||
@@ -27,6 +27,8 @@ observation to contract. No adapter → UNKNOWN; UNKNOWN is never PASS (§23).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from rune.structural_store import add_entity, entity, now, relationships, resolve_status
|
||||
|
||||
__all__ = [
|
||||
@@ -228,7 +230,25 @@ def check_layout(doc: dict, observed: dict) -> dict:
|
||||
UNKNOWN — no observation available for a locked contract (no
|
||||
adapter evidence) — NEVER converted to PASS
|
||||
Flexible components may be reorganized by builders via amendment, but a
|
||||
live observation contradicting any contract still fails the gate."""
|
||||
live observation contradicting any contract still fails the gate.
|
||||
|
||||
Dichotomy batch 4: delegates to the Rust check core when built
|
||||
(RUNE_LC_CORE=python pins the Python oracle). The Python implementation
|
||||
below is the strangler fallback and the parity authority.
|
||||
"""
|
||||
if os.environ.get("RUNE_LC_CORE", "auto") != "python":
|
||||
try:
|
||||
from rune import lc_loader
|
||||
if lc_loader.available():
|
||||
contracts = [
|
||||
{"kind": e.get("kind"), "status": e.get("status"),
|
||||
"identities": dict(e.get("identities", {}))}
|
||||
for e in doc["entities"]
|
||||
if e.get("kind") == "layout_contract"
|
||||
]
|
||||
return lc_loader.check_layout(contracts, observed)
|
||||
except Exception:
|
||||
pass # fallback below; never crash the caller
|
||||
results = []
|
||||
verdict = "PASS"
|
||||
for e in doc["entities"]:
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust layout_contract core (dichotomy debt queue
|
||||
batch 4, #2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* lc_check({contracts, observed}) -> {verdict, checks}
|
||||
* lc_drift({contracts, checks}) -> {drifted_locked, reactivated}
|
||||
|
||||
Override the .so with LC_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "layout_contract"
|
||||
_LIB_PATH = Path(os.environ.get("LC_RUST_LIB", _CRATE_DIR / "target" / "release" / "liblayout_contract.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class LayoutContractUnavailable(FileNotFoundError):
|
||||
"""The Rust layout core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise LayoutContractUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("lc_check", "lc_drift"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def check_layout(contracts: list, observed: dict) -> dict:
|
||||
return _call("lc_check", {"contracts": contracts, "observed": observed})
|
||||
|
||||
|
||||
def drift_decisions(contracts: list, checks: list) -> dict:
|
||||
return _call("lc_drift", {"contracts": contracts, "checks": checks})
|
||||
@@ -49,6 +49,20 @@ except ImportError: # direct script execution
|
||||
|
||||
LOG = logging.getLogger("rune.memory_selector")
|
||||
|
||||
|
||||
def _mse_core():
|
||||
"""The Rust selection-core probe (strangler law). RUNE_MSE_CORE=python
|
||||
pins the Python fallback — the pin is honored inside mse_loader.core so
|
||||
every consumer gets it. Returns the loader MODULE (never the raw CDLL:
|
||||
dlsym on the cdylib handle would resolve short names like `select`
|
||||
against libc instead of the mse_* exports)."""
|
||||
try:
|
||||
from rune import mse_loader
|
||||
|
||||
return mse_loader if mse_loader.core() is not None else None
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
return None
|
||||
|
||||
# Candidate cap BEFORE budgeting (plan DD 5: the selector caps its own
|
||||
# candidate list before returning).
|
||||
MAX_CANDIDATES = 24
|
||||
@@ -167,6 +181,32 @@ class MemorySelector:
|
||||
candidates.append(entry)
|
||||
candidates = candidates[:MAX_CANDIDATES]
|
||||
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
out = _core.select(candidates, budget, self.min_score, types_queried)
|
||||
blocks = [
|
||||
ContributionBlock(
|
||||
b["name"],
|
||||
b["priority"],
|
||||
b["content"],
|
||||
b["tokens"],
|
||||
b["source"],
|
||||
evictable=b["evictable"],
|
||||
)
|
||||
for b in out["blocks"]
|
||||
]
|
||||
if out["abstained"]:
|
||||
LOG.info(
|
||||
"memory abstain: task=%r candidates=%d floor=%.2f types=%s",
|
||||
query[:60],
|
||||
out["candidates_evaluated"],
|
||||
self.min_score,
|
||||
types_queried or "all",
|
||||
)
|
||||
return SelectionResult(
|
||||
blocks, out["candidates_evaluated"], out["selected_count"], out["abstained"], types_queried
|
||||
)
|
||||
|
||||
# ONE floor for EVERY source: the store source was already floored by
|
||||
# the same constant inside recall() — which applies min_score in
|
||||
# EVERY mode (lexical-only, dense-only, hybrid; P1-03 #26 discharge)
|
||||
@@ -285,6 +325,9 @@ def _store_recall(
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
return _core.store_project(results)
|
||||
out: list[dict] = []
|
||||
seen_ids: set = set()
|
||||
for row in results:
|
||||
@@ -348,6 +391,9 @@ def _neurons_recall(query: str, types: list[str] | None, budget: int, min_score:
|
||||
import rune.warm_neuron_store as warm_neuron_store # type: ignore[no-redef]
|
||||
|
||||
neurons = warm_neuron_store.list_neurons()
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
return _core.neurons_decision(neurons, query, budget, min_score)
|
||||
task_words = {w.lower().strip(".,;:!?()[]{}\"'") for w in query.split()}
|
||||
out: list[dict] = []
|
||||
for neuron in neurons:
|
||||
@@ -385,6 +431,7 @@ def _graphs_recall(query: str, types: list[str] | None, budget: int, min_score:
|
||||
out: list[dict] = []
|
||||
try:
|
||||
task_words = list(dict.fromkeys(w.lower().strip(".,;:!?()[]{}\"'") for w in query.split()))[:5]
|
||||
texts: list[str] = []
|
||||
for word in task_words:
|
||||
result = graph_memory.query(db, word, hops=1)
|
||||
if not result:
|
||||
@@ -393,6 +440,11 @@ def _graphs_recall(query: str, types: list[str] | None, budget: int, min_score:
|
||||
facts = result.get("facts", []) if isinstance(result, dict) else []
|
||||
lines = [f"{f.get('subject')} {f.get('predicate', '-')} {f.get('object')}" for f in facts]
|
||||
text = "; ".join(lines) if lines else str(entity)
|
||||
texts.append(text)
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
return _core.graphs_decision(texts, query, len(task_words), budget, min_score)
|
||||
for text in texts:
|
||||
score = _lex_overlap_score(text, query) or (1.0 / max(len(task_words), 1))
|
||||
if score < min_score:
|
||||
continue
|
||||
@@ -427,8 +479,11 @@ def _abstraction_recall(query: str, types: list[str] | None, budget: int, min_sc
|
||||
f"SELECT what FROM facts WHERE state='active' AND ({placeholders}) LIMIT {int(budget) * 3}",
|
||||
params,
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
text = str(row[0])
|
||||
texts = [str(row[0]) for row in rows]
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
return _core.abstraction_decision(texts, query, budget, min_score)
|
||||
for text in texts:
|
||||
score = _lex_overlap_score(text, query)
|
||||
if score < min_score:
|
||||
continue
|
||||
@@ -464,6 +519,13 @@ def _consolidated_recall(query: str, types: list[str] | None, budget: int, min_s
|
||||
content = (getattr(item, "content", "") or "").strip()
|
||||
if not content:
|
||||
continue
|
||||
out.append(content)
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
return _core.consolidated_decision(out, query, budget, min_score)
|
||||
contents = out
|
||||
out = []
|
||||
for content in contents:
|
||||
score = _lex_overlap_score(content, query)
|
||||
if score < min_score:
|
||||
continue # below floor -> abstain (was: flat relevance injection)
|
||||
@@ -491,12 +553,16 @@ def _persona_recall(
|
||||
PINNED store (``db_path``) — never the module-default store that a bare
|
||||
``memory_store.connect()`` would open.
|
||||
"""
|
||||
wanted = {t for t in (types or []) if t in ("self", "personality")}
|
||||
if not wanted:
|
||||
_core = _mse_core()
|
||||
if _core is not None:
|
||||
wanted_list = _core.persona_wanted(types)
|
||||
else:
|
||||
wanted_list = sorted({t for t in (types or []) if t in ("self", "personality")})
|
||||
if not wanted_list:
|
||||
return []
|
||||
db = memory_store.connect(db_path)
|
||||
try:
|
||||
rows = memory_store.recall(db, query, budget=budget, min_score=min_score, memory_types=sorted(wanted))
|
||||
rows = memory_store.recall(db, query, budget=budget, min_score=min_score, memory_types=wanted_list)
|
||||
finally:
|
||||
db.close()
|
||||
out: list[dict] = []
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust memory_selector core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* mse_select({candidates, budget, min_score, types}) -> selection outcome
|
||||
* mse_neurons_decision({neurons, query, budget, min_score}) -> rows
|
||||
* mse_graphs_decision({texts, query, n, budget, min_score}) -> rows
|
||||
* mse_abstraction_decision({whats, query, budget, min_score}) -> rows
|
||||
* mse_consolidated_decision({contents, query, budget, min_score}) -> rows
|
||||
* mse_persona_wanted({types}) -> sorted wanted types
|
||||
* mse_store_project({rows}) -> deduped projected rows
|
||||
|
||||
The IO (store recall, list_neurons, graph query, LIKE scan, consolidation
|
||||
read) stays in memory_selector.py; this core owns the post-gather decision
|
||||
math. Override the .so with MSE_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" / "memory_selector"
|
||||
_LIB_PATH = Path(os.environ.get("MSE_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmemory_selector.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
_SYMBOLS = (
|
||||
"mse_select",
|
||||
"mse_neurons_decision",
|
||||
"mse_graphs_decision",
|
||||
"mse_abstraction_decision",
|
||||
"mse_consolidated_decision",
|
||||
"mse_persona_wanted",
|
||||
"mse_store_project",
|
||||
)
|
||||
|
||||
|
||||
class SelectorCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust selection core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def core():
|
||||
"""The shared kernel probe (strangler law): the delegation entry the
|
||||
facade uses. RUNE_MSE_CORE=python pins the fallback — THIS is where the
|
||||
pin is honored, so every consumer gets it (a module-local probe that
|
||||
ignores the pin silently defeats it)."""
|
||||
if os.environ.get("RUNE_MSE_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
return _load()
|
||||
except Exception: # noqa: BLE001 — build/loader failures mean fallback
|
||||
return None
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise SelectorCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
# FFI segfault law: explicit argtypes/restype (c_void_p for returned
|
||||
# char* — the default c_int truncates 64-bit pointers).
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
_free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
"""Doc-envelope free via capability_scoring's cs_free_string — NEVER
|
||||
re-export a free symbol from this crate (duplicate symbols break the
|
||||
cdylib link)."""
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string.restype = None
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def select(candidates: list, budget: int, min_score: float, types: list | None) -> dict:
|
||||
return _call("mse_select", {
|
||||
"candidates": candidates,
|
||||
"budget": budget,
|
||||
"min_score": min_score,
|
||||
"types": list(types or []),
|
||||
})
|
||||
|
||||
|
||||
def neurons_decision(neurons: list, query: str, budget: int, min_score: float) -> list:
|
||||
return _call("mse_neurons_decision", {
|
||||
"neurons": neurons, "query": query,
|
||||
"budget": budget, "min_score": min_score,
|
||||
})
|
||||
|
||||
|
||||
def graphs_decision(texts: list, query: str, n: int, budget: int, min_score: float) -> list:
|
||||
return _call("mse_graphs_decision", {
|
||||
"texts": texts, "query": query, "n": n,
|
||||
"budget": budget, "min_score": min_score,
|
||||
})
|
||||
|
||||
|
||||
def abstraction_decision(whats: list, query: str, budget: int, min_score: float) -> list:
|
||||
return _call("mse_abstraction_decision", {
|
||||
"whats": whats, "query": query,
|
||||
"budget": budget, "min_score": min_score,
|
||||
})
|
||||
|
||||
|
||||
def consolidated_decision(contents: list, query: str, budget: int, min_score: float) -> list:
|
||||
return _call("mse_consolidated_decision", {
|
||||
"contents": contents, "query": query,
|
||||
"budget": budget, "min_score": min_score,
|
||||
})
|
||||
|
||||
|
||||
def persona_wanted(types: list | None) -> list:
|
||||
return _call("mse_persona_wanted", {"types": list(types or [])})
|
||||
|
||||
|
||||
def store_project(rows: list) -> list:
|
||||
return _call("mse_store_project", {"rows": rows})
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.192.0
|
||||
Version: v0.202.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -2037,7 +2037,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.192.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.202.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
+86
-8
@@ -115,11 +115,22 @@ def now() -> str:
|
||||
|
||||
|
||||
def _sorted(obj):
|
||||
"""Deterministic normalization: sort everything sortable, recursively."""
|
||||
"""Deterministic normalization: sort everything sortable, recursively.
|
||||
Dichotomy wave (issue #2479): delegates to the Rust core when built;
|
||||
RUNE_SPINE_CORE=python pins the Python oracle below (strangler
|
||||
fallback, parity authority)."""
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
return core.sorted_kernel(obj)
|
||||
return _py_sorted(obj)
|
||||
|
||||
|
||||
def _py_sorted(obj):
|
||||
"""Python oracle for _sorted (strangler fallback)."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: _sorted(obj[k]) for k in sorted(obj)}
|
||||
return {k: _py_sorted(obj[k]) for k in sorted(obj)}
|
||||
if isinstance(obj, list):
|
||||
return sorted((_sorted(v) for v in obj), key=lambda v: json.dumps(v, sort_keys=True))
|
||||
return sorted((_py_sorted(v) for v in obj), key=lambda v: json.dumps(v, sort_keys=True))
|
||||
return obj
|
||||
|
||||
|
||||
@@ -137,13 +148,21 @@ def load(path: Path | None = None) -> dict | None:
|
||||
|
||||
|
||||
def save(doc: dict, path: Path | str | None = None) -> Path:
|
||||
"""Canonical store write (dichotomy wave: the serialization core is
|
||||
Rust; schema validation raises ValueError here — the caller contract).
|
||||
RUNE_SPINE_CORE=python pins the Python oracle (strangler fallback)."""
|
||||
incoming = doc.get("schema_version", SCHEMA_VERSION)
|
||||
if incoming != SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version {incoming} != {SCHEMA_VERSION}")
|
||||
doc["schema_version"] = SCHEMA_VERSION
|
||||
p = Path(path) if path else CURRENT # str paths are legitimate (CLI callers)
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
text = core.save_text(doc)
|
||||
else:
|
||||
text = json.dumps(_py_sorted(doc), indent=1, sort_keys=True) + "\n"
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(_sorted(doc), indent=1, sort_keys=True) + "\n")
|
||||
p.write_text(text)
|
||||
return p
|
||||
|
||||
|
||||
@@ -195,11 +214,21 @@ WORKFLOWS_DIR = ROOT / ".gitea" / "workflows"
|
||||
def _declared_surfaces() -> dict:
|
||||
"""Umbrella-declared non-module surfaces: rust crates, Go surfaces,
|
||||
configs. The manifest (config/umbrellas.json) is the declaration
|
||||
authority — same law as modules, new kinds."""
|
||||
authority — same law as modules, new kinds. Dichotomy wave: the file
|
||||
read stays here (IO law), the manifest parse delegates to the Rust
|
||||
core when built (RUNE_SPINE_CORE=python pins the Python oracle)."""
|
||||
try:
|
||||
manifest = json.loads((ROOT / "config" / "umbrellas.json").read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {"rust_crates": [], "go_surfaces": [], "configs": []}
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
return core.declared_surfaces(manifest)
|
||||
return _py_declared_surfaces(manifest)
|
||||
|
||||
|
||||
def _py_declared_surfaces(manifest: dict) -> dict:
|
||||
"""Python oracle for the _declared_surfaces parse core (fallback)."""
|
||||
out = {"rust_crates": [], "go_surfaces": [], "configs": []}
|
||||
for name, u in manifest.get("umbrellas", {}).items():
|
||||
for crate in u.get("rust_crates", []):
|
||||
@@ -222,10 +251,23 @@ def project_meta_surfaces(doc: dict, inputs: dict | None = None) -> int:
|
||||
H3-spine: when `inputs` is given (the gather-driven path), every
|
||||
filesystem probe comes from the gathered dict so the Rust core and the
|
||||
Python fallback consume byte-identical scans. The no-inputs path gathers
|
||||
on the spot (backward compatibility for direct callers)."""
|
||||
count = 0
|
||||
on the spot (backward compatibility for direct callers). Dichotomy
|
||||
wave: the core delegates to Rust when built (RUNE_SPINE_CORE=python
|
||||
pins the Python oracle); the doc is mutated in place either way."""
|
||||
if inputs is None:
|
||||
inputs = _gather_meta_inputs()
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
out = core.project_meta_surfaces(doc, inputs, now())
|
||||
doc.clear()
|
||||
doc.update(out["doc"]) # preserve the in-place mutation contract
|
||||
return out["count"]
|
||||
return _py_project_meta_surfaces(doc, inputs)
|
||||
|
||||
|
||||
def _py_project_meta_surfaces(doc: dict, inputs: dict) -> int:
|
||||
"""Python oracle for the project_meta_surfaces core (fallback)."""
|
||||
count = 0
|
||||
manifest = inputs.get("config_manifest") or {}
|
||||
declared = {"rust_crates": [], "go_surfaces": [], "configs": []}
|
||||
for name, u in manifest.get("umbrellas", {}).items():
|
||||
@@ -472,7 +514,27 @@ def _py_build_from_inputs(inputs: dict) -> dict:
|
||||
def _ingest_umbrellas_data(doc: dict, data: dict) -> int:
|
||||
"""The original ingest_umbrellas with the file read lifted out (H3-spine
|
||||
gather boundary). Raises ValueError on a Spine-generated view, exactly
|
||||
like the file-driven original."""
|
||||
like the file-driven original. Dichotomy wave: the happy path delegates
|
||||
to the Rust core when built (RUNE_SPINE_CORE=python pins the Python
|
||||
oracle); the refusal check stays here — ValueError is the caller
|
||||
contract and the core carries the identical message (battery-pinned)."""
|
||||
if GENERATED_VIEW_MARKER in data:
|
||||
raise ValueError(
|
||||
"the manifest is a Spine-generated compatibility view; refusing to "
|
||||
"ingest it (Phase A reads the manual authority exactly once). "
|
||||
"Structural membership now lives in the Spine store."
|
||||
)
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
out = core.ingest_umbrellas_data(doc, data, now())
|
||||
doc.clear()
|
||||
doc.update(out["doc"]) # preserve the in-place mutation contract
|
||||
return out["count"]
|
||||
return _py_ingest_umbrellas_data(doc, data)
|
||||
|
||||
|
||||
def _py_ingest_umbrellas_data(doc: dict, data: dict) -> int:
|
||||
"""Python oracle for the ingest core (strangler fallback)."""
|
||||
if GENERATED_VIEW_MARKER in data:
|
||||
raise ValueError(
|
||||
"the manifest is a Spine-generated compatibility view; refusing to "
|
||||
@@ -706,6 +768,22 @@ REL_STATUS = {"UNKNOWN", "CURRENT", "STALE", "CONFLICTING", "DEGRADED"}
|
||||
|
||||
|
||||
def entity(doc: dict, id: str) -> dict | None:
|
||||
"""Resolve an entity (rename-resilient via aliases). Dichotomy wave:
|
||||
delegates to the Rust core when built; RUNE_SPINE_CORE=python pins the
|
||||
Python oracle below (strangler fallback, parity authority). The core
|
||||
owns the RESOLUTION verdict; the facade returns the LIVE dict so the
|
||||
pre-port mutation-through-entity contract is preserved (JSON FFI gives
|
||||
a detached copy — callers mutate docs through this reference)."""
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
if core.entity(doc, id) is None:
|
||||
return None
|
||||
return _py_entity(doc, id)
|
||||
return _py_entity(doc, id)
|
||||
|
||||
|
||||
def _py_entity(doc: dict, id: str) -> dict | None:
|
||||
"""Python oracle for entity (strangler fallback)."""
|
||||
doc_ents = doc.get("entities", [])
|
||||
for e in doc_ents:
|
||||
if e["id"] == id:
|
||||
|
||||
@@ -60,7 +60,12 @@ def _load() -> ctypes.CDLL:
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.sp_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.sp_free_string.restype = None
|
||||
for sym in ("sp_project", "sp_drift", "sp_query", "sp_export"):
|
||||
for sym in (
|
||||
"sp_project", "sp_drift", "sp_query", "sp_export",
|
||||
# dichotomy-wave facade kernels (issue #2479 tranche)
|
||||
"sf_sorted", "sf_save_text", "sf_entity", "sf_declared_surfaces",
|
||||
"sf_project_meta_surfaces", "sf_ingest_umbrellas_data",
|
||||
):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
@@ -103,3 +108,37 @@ def query(doc: dict, entity_id: str) -> dict:
|
||||
def export_umbrellas(doc: dict, now: str) -> dict:
|
||||
"""Legacy umbrella compatibility view (output-only)."""
|
||||
return _call("sp_export", {"doc": doc, "now": now})
|
||||
|
||||
|
||||
# ── dichotomy-wave facade kernels (issue #2479 tranche) ────────────────────
|
||||
|
||||
def sorted_kernel(v):
|
||||
"""spine._sorted — recursive deterministic normalization."""
|
||||
return _call("sf_sorted", {"v": v})
|
||||
|
||||
|
||||
def save_text(doc: dict) -> str:
|
||||
"""The canonical text spine.save writes (validation inside the core)."""
|
||||
return _call("sf_save_text", {"doc": doc})
|
||||
|
||||
|
||||
def entity(doc: dict, entity_id: str):
|
||||
"""Rename-resilient entity resolution (None when absent)."""
|
||||
return _call("sf_entity", {"doc": doc, "id": entity_id})
|
||||
|
||||
|
||||
def declared_surfaces(config_manifest: dict) -> dict:
|
||||
"""The manifest-parse core of spine._declared_surfaces."""
|
||||
return _call("sf_declared_surfaces", {"config_manifest": config_manifest})
|
||||
|
||||
|
||||
def project_meta_surfaces(doc: dict, inputs: dict, now: str) -> dict:
|
||||
"""Meta-scaffold projector core. Returns {"count", "doc"} — the doc is
|
||||
the mutated copy; the facade copies it back to preserve the in-place
|
||||
contract."""
|
||||
return _call("sf_project_meta_surfaces", {"doc": doc, "inputs": inputs, "now": now})
|
||||
|
||||
|
||||
def ingest_umbrellas_data(doc: dict, data: dict, now: str) -> dict:
|
||||
"""Umbrella-manifest ingest core. Returns {"count", "doc"}."""
|
||||
return _call("sf_ingest_umbrellas_data", {"doc": doc, "data": data, "now": now})
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust system_registry core (dichotomy wave, #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* sr_load_settings({settings_file}) -> settings object
|
||||
* sr_save_settings({settings_file, settings}) -> null
|
||||
* sr_is_dev_mode({settings_file}) -> bool
|
||||
* sr_file_hash({path}) -> hex16 | null
|
||||
* sr_file_modified({path}) -> iso | null
|
||||
* sr_discover_files({env}) -> file rows
|
||||
* sr_load_registry({registry_file}) -> registry object
|
||||
* sr_save_registry({registry_file, registry}) -> null
|
||||
* sr_scan_and_register({env, registry_file, now}) -> changes
|
||||
* sr_get_file_status({registry_file, name}) -> entry | null
|
||||
* sr_diff_since_last_scan({env, registry_file}) -> changes
|
||||
* sr_generate_report({env, registry_file, settings_file}) -> report text
|
||||
* sr_handle_hook({event}) -> {status, event, handler}
|
||||
|
||||
The oracle's import-time path roots (memory_env, module globals) stay in
|
||||
system_registry.py; this core owns the pure decision math plus the file
|
||||
reads it needs. Override the .so with SR_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" / "system_registry"
|
||||
_LIB_PATH = Path(os.environ.get("SR_RUST_LIB", _CRATE_DIR / "target" / "release" / "libsystem_registry.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
_SYMBOLS = (
|
||||
"sr_load_settings",
|
||||
"sr_save_settings",
|
||||
"sr_is_dev_mode",
|
||||
"sr_file_hash",
|
||||
"sr_file_modified",
|
||||
"sr_discover_files",
|
||||
"sr_load_registry",
|
||||
"sr_save_registry",
|
||||
"sr_scan_and_register",
|
||||
"sr_get_file_status",
|
||||
"sr_diff_since_last_scan",
|
||||
"sr_generate_report",
|
||||
"sr_handle_hook",
|
||||
)
|
||||
|
||||
|
||||
class RegistryCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust registry core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def core():
|
||||
"""The shared kernel probe (strangler law): the delegation entry the
|
||||
facade uses. RUNE_SYSTEM_REGISTRY_CORE=python pins the fallback — THIS
|
||||
is where the pin is honored, so every consumer gets it (a module-local
|
||||
probe that ignores the pin silently defeats it)."""
|
||||
if os.environ.get("RUNE_SYSTEM_REGISTRY_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
return _load()
|
||||
except Exception: # noqa: BLE001 — build/loader failures mean fallback
|
||||
return None
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise RegistryCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
# FFI segfault law: explicit argtypes/restype (c_void_p for returned
|
||||
# char* — the default c_int truncates 64-bit pointers).
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
_free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
"""Doc-envelope free via capability_scoring's cs_free_string — NEVER
|
||||
re-export a free symbol from this crate (duplicate symbols break the
|
||||
cdylib link)."""
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string.restype = None
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def load_settings(settings_file: str) -> dict:
|
||||
return _call("sr_load_settings", {"settings_file": settings_file})
|
||||
|
||||
|
||||
def save_settings(settings_file: str, settings: dict) -> None:
|
||||
_call("sr_save_settings", {"settings_file": settings_file, "settings": settings})
|
||||
|
||||
|
||||
def is_dev_mode(settings_file: str) -> bool:
|
||||
return _call("sr_is_dev_mode", {"settings_file": settings_file})
|
||||
|
||||
|
||||
def file_hash(path: str):
|
||||
return _call("sr_file_hash", {"path": path})
|
||||
|
||||
|
||||
def file_modified(path: str):
|
||||
return _call("sr_file_modified", {"path": path})
|
||||
|
||||
|
||||
def discover_files(env: dict) -> list:
|
||||
return _call("sr_discover_files", {"env": env})
|
||||
|
||||
|
||||
def load_registry(registry_file: str) -> dict:
|
||||
return _call("sr_load_registry", {"registry_file": registry_file})
|
||||
|
||||
|
||||
def save_registry(registry_file: str, registry: dict) -> None:
|
||||
_call("sr_save_registry", {"registry_file": registry_file, "registry": registry})
|
||||
|
||||
|
||||
def scan_and_register(env: dict, registry_file: str, now: str) -> list:
|
||||
return _call("sr_scan_and_register", {"env": env, "registry_file": registry_file, "now": now})
|
||||
|
||||
|
||||
def get_file_status(registry_file: str, name: str):
|
||||
return _call("sr_get_file_status", {"registry_file": registry_file, "name": name})
|
||||
|
||||
|
||||
def diff_since_last_scan(env: dict, registry_file: str) -> list:
|
||||
return _call("sr_diff_since_last_scan", {"env": env, "registry_file": registry_file})
|
||||
|
||||
|
||||
def generate_report(env: dict, registry_file: str, settings_file: str) -> str:
|
||||
return _call("sr_generate_report", {
|
||||
"env": env, "registry_file": registry_file, "settings_file": settings_file,
|
||||
})
|
||||
|
||||
|
||||
def handle_hook(event: str) -> dict:
|
||||
return _call("sr_handle_hook", {"event": event})
|
||||
@@ -70,8 +70,35 @@ STATUS_EXPERIMENTAL = "experimental"
|
||||
STATUS_INFRA = "infrastructural"
|
||||
|
||||
|
||||
def _sr_core():
|
||||
"""The Rust registry kernel probe (strangler law): delegates to
|
||||
sr_loader.core so RUNE_SYSTEM_REGISTRY_CORE=python pins the fallback —
|
||||
the pin is honored inside the loader, so every consumer gets it."""
|
||||
try:
|
||||
from rune import sr_loader
|
||||
except ImportError: # pragma: no cover — direct-script invocation
|
||||
import sr_loader # type: ignore[no-redef]
|
||||
return sr_loader.core()
|
||||
|
||||
|
||||
def _sr_env():
|
||||
"""The five discovery roots the Rust core takes as parameters."""
|
||||
return {
|
||||
"scripts_dir": SCRIPTS_DIR,
|
||||
"plugin_dir": PLUGIN_DIR,
|
||||
"systemd_dir": SYSTEMD_DIR,
|
||||
"config_dir": memory_env.config_dir(),
|
||||
"memory_dir": memory_env.memory_dir(),
|
||||
}
|
||||
|
||||
|
||||
def load_settings():
|
||||
"""Load system settings."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.load_settings(SETTINGS_FILE)
|
||||
defaults = {
|
||||
"mode": "dev",
|
||||
"auto_scan": True,
|
||||
@@ -93,6 +120,12 @@ def load_settings():
|
||||
|
||||
def save_settings(settings):
|
||||
"""Save system settings."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
_sr.save_settings(SETTINGS_FILE, settings)
|
||||
return
|
||||
os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True)
|
||||
with open(SETTINGS_FILE, "w") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
@@ -100,12 +133,22 @@ def save_settings(settings):
|
||||
|
||||
def is_dev_mode():
|
||||
"""Check if we're in dev mode."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.is_dev_mode(SETTINGS_FILE)
|
||||
settings = load_settings()
|
||||
return settings.get("mode", "dev") == "dev"
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
"""SHA256 of file contents."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.file_hash(path)
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
return hashlib.sha256(f.read()).hexdigest()[:16]
|
||||
@@ -115,6 +158,11 @@ def file_hash(path):
|
||||
|
||||
def file_modified(path):
|
||||
"""Last modified timestamp."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.file_modified(path)
|
||||
try:
|
||||
return datetime.fromtimestamp(os.path.getmtime(path), tz=timezone.utc).isoformat()
|
||||
except Exception: # noqa: BLE001 — deliberate: best-effort degrade (logged or continued); crash would be worse
|
||||
@@ -123,6 +171,11 @@ def file_modified(path):
|
||||
|
||||
def discover_files():
|
||||
"""Discover all system files and their metadata."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.discover_files(_sr_env())
|
||||
files = []
|
||||
settings = load_settings()
|
||||
|
||||
@@ -237,6 +290,11 @@ def discover_files():
|
||||
|
||||
def load_registry():
|
||||
"""Load the file registry."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.load_registry(REGISTRY_FILE)
|
||||
if os.path.exists(REGISTRY_FILE):
|
||||
try:
|
||||
with open(REGISTRY_FILE) as f:
|
||||
@@ -248,6 +306,12 @@ def load_registry():
|
||||
|
||||
def save_registry(registry):
|
||||
"""Save the file registry."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
_sr.save_registry(REGISTRY_FILE, registry)
|
||||
return
|
||||
os.makedirs(os.path.dirname(REGISTRY_FILE), exist_ok=True)
|
||||
with open(REGISTRY_FILE, "w") as f:
|
||||
json.dump(registry, f, indent=2)
|
||||
@@ -255,6 +319,20 @@ def save_registry(registry):
|
||||
|
||||
def scan_and_register():
|
||||
"""Scan files and update registry. Returns changes detected."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
changes = _sr.scan_and_register(_sr_env(), REGISTRY_FILE, now)
|
||||
settings = load_settings()
|
||||
if settings.get("verbose_logging") and changes:
|
||||
for change in changes:
|
||||
if change["type"] == "modified":
|
||||
print(f" CHANGED: {change['file']}")
|
||||
elif change["type"] == "new":
|
||||
print(f" NEW: {change['file']}")
|
||||
return changes
|
||||
files = discover_files()
|
||||
registry = load_registry()
|
||||
settings = load_settings()
|
||||
@@ -311,12 +389,22 @@ def scan_and_register():
|
||||
|
||||
def get_file_status(name):
|
||||
"""Get status of a specific file."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.get_file_status(REGISTRY_FILE, name)
|
||||
registry = load_registry()
|
||||
return registry.get(name)
|
||||
|
||||
|
||||
def diff_since_last_scan():
|
||||
"""Show files changed since last scan."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.diff_since_last_scan(_sr_env(), REGISTRY_FILE)
|
||||
registry = load_registry()
|
||||
current = discover_files()
|
||||
changes = []
|
||||
@@ -349,6 +437,12 @@ def diff_since_last_scan():
|
||||
|
||||
def generate_report():
|
||||
"""Generate full system report."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
print(_sr.generate_report(_sr_env(), REGISTRY_FILE, SETTINGS_FILE), end="")
|
||||
return
|
||||
files = discover_files()
|
||||
registry = load_registry()
|
||||
settings = load_settings()
|
||||
@@ -399,6 +493,11 @@ def generate_report():
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
"""Hook handler for system_registry events."""
|
||||
core = _sr_core()
|
||||
if core is not None:
|
||||
from rune import sr_loader as _sr
|
||||
|
||||
return _sr.handle_hook(event)
|
||||
return {"status": "ok", "event": event, "handler": "system_registry"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
//! cognitive_budgeter branch battery — parity vs frozen Python-oracle
|
||||
//! truth (/tmp/cb_truth.json: 14 calculate cases, frozen 2026-10-11).
|
||||
|
||||
use crate::calculate_budget;
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"calculate": [
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.9,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.2,
|
||||
"minimum_tier": "T1",
|
||||
"flags": []
|
||||
},
|
||||
"context": {},
|
||||
"out": {
|
||||
"start_tier": "T1",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.7,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 800
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.5,
|
||||
"minimum_tier": "T2",
|
||||
"flags": [
|
||||
"REPEATED_RETRY"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"risk": "high",
|
||||
"verification_available": false
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T2",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 5,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.5,
|
||||
"minimum_tier": "T0",
|
||||
"flags": [
|
||||
"TASK_FAILURE"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"risk": "high",
|
||||
"verification_available": true
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T3",
|
||||
"max_attempts": 4,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"channel": "work"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 8192,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": true,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"channel": "conversation"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"channel": "other"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 4096,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": true,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"urgency": "critical"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 10.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"urgency": "high"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 20.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.4,
|
||||
"minimum_tier": "T0",
|
||||
"flags": [
|
||||
"HIGH_RISK_IRREVERSIBLE"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"verification_available": false
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.65,
|
||||
"minimum_tier": "T3",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"tool_allowed": false,
|
||||
"cloud_allowed": false
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T3",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": false,
|
||||
"verification_required": true,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 800
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.0,
|
||||
"minimum_tier": "T0",
|
||||
"flags": []
|
||||
},
|
||||
"context": {
|
||||
"previous_failures": 3
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T0",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 3,
|
||||
"max_tokens": 2048,
|
||||
"max_latency": 30.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": false,
|
||||
"tool_allowed": false,
|
||||
"attention_budget": 400
|
||||
}
|
||||
},
|
||||
{
|
||||
"salience": {
|
||||
"score": 0.85,
|
||||
"minimum_tier": "T1",
|
||||
"flags": [
|
||||
"TASK_FAILURE",
|
||||
"REPEATED_RETRY"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"risk": "high",
|
||||
"channel": "work",
|
||||
"urgency": "critical"
|
||||
},
|
||||
"out": {
|
||||
"start_tier": "T1",
|
||||
"max_tier": "T4",
|
||||
"max_attempts": 5,
|
||||
"max_tokens": 8192,
|
||||
"max_latency": 10.0,
|
||||
"cloud_allowed": true,
|
||||
"verification_required": true,
|
||||
"tool_allowed": true,
|
||||
"attention_budget": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
#[test]
|
||||
fn calculate_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["calculate"].as_array().expect("calculate array").iter().enumerate() {
|
||||
let salience = case.get("salience").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let context = case.get("context").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let got = calculate_budget(&salience, &context);
|
||||
assert_eq!(got, case["out"], "calculate case {}", idx);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,65 @@
|
||||
//! cognitive_budgeter — cognitive budget calculation (W6).
|
||||
//! PARITY port of src/rune/cognitive_budgeter.py.
|
||||
//! cognitive_budgeter — budget kernels (dichotomy debt queue batch 5).
|
||||
//! PARITY port of src/rune/cognitive_budgeter.py calculate_budget (GO
|
||||
//! verdict, deterministic decision tree; salience/context stay Python).
|
||||
|
||||
pub fn calculate_budget(
|
||||
salience: &serde_json::Value,
|
||||
context: &serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
let score = salience
|
||||
.get("score")
|
||||
.and_then(|s| s.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let min_tier = salience
|
||||
.get("minimum_tier")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("T0")
|
||||
.to_string();
|
||||
let flags: Vec<String> = salience
|
||||
.get("flags")
|
||||
.and_then(|f| f.as_array())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
let risk = context
|
||||
.get("risk")
|
||||
.and_then(|r| r.as_str())
|
||||
.unwrap_or("normal");
|
||||
let verification = context
|
||||
.get("verification_available")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let channel = context
|
||||
.get("channel")
|
||||
.and_then(|c| c.as_str())
|
||||
.unwrap_or("conversation");
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
let max_tier = if risk == "high" && !verification {
|
||||
fn s(v: Option<&Value>) -> &str {
|
||||
match v {
|
||||
Some(Value::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn f64_of(v: Option<&Value>, dflt: f64) -> f64 {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_f64().unwrap_or(dflt),
|
||||
_ => dflt,
|
||||
}
|
||||
}
|
||||
|
||||
fn i64_of(v: Option<&Value>, dflt: i64) -> i64 {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_i64().unwrap_or(dflt),
|
||||
_ => dflt,
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_of(v: Option<&Value>, dflt: bool) -> bool {
|
||||
match v {
|
||||
Some(Value::Bool(b)) => *b,
|
||||
_ => dflt,
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_of(v: Option<&Value>) -> Vec<String> {
|
||||
match v {
|
||||
Some(Value::Array(a)) => a.iter().filter_map(|i| i.as_str().map(String::from)).collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// calculate_budget port. salience={score, minimum_tier, flags},
|
||||
/// context={risk, verification_available, previous_failures, channel,
|
||||
/// urgency, cloud_allowed, tool_allowed}.
|
||||
pub fn calculate_budget(salience: &Value, context: &Value) -> Value {
|
||||
let score = f64_of(salience.get("score"), 0.0);
|
||||
let start_tier = s(salience.get("minimum_tier"));
|
||||
let start_tier = if start_tier.is_empty() { "T0" } else { start_tier };
|
||||
let flags = flags_of(salience.get("flags"));
|
||||
|
||||
let risk = s(context.get("risk"));
|
||||
let risk = if risk.is_empty() { "normal" } else { risk };
|
||||
let verification_available = bool_of(context.get("verification_available"), false);
|
||||
let _previous_failures = i64_of(context.get("previous_failures"), 0);
|
||||
|
||||
let max_tier = if risk == "high" && !verification_available {
|
||||
"T4"
|
||||
} else if risk == "high" {
|
||||
"T3"
|
||||
@@ -53,61 +75,87 @@ pub fn calculate_budget(
|
||||
3
|
||||
};
|
||||
|
||||
let max_tokens = match channel {
|
||||
"work" => 8192,
|
||||
"conversation" => 2048,
|
||||
_ => 2048,
|
||||
let channel = s(context.get("channel"));
|
||||
let channel = if channel.is_empty() { "conversation" } else { channel };
|
||||
let max_tokens = if channel == "work" {
|
||||
8192
|
||||
} else if channel == "conversation" {
|
||||
2048
|
||||
} else {
|
||||
4096
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"start_tier": min_tier,
|
||||
let urgency = s(context.get("urgency"));
|
||||
let urgency = if urgency.is_empty() { "normal" } else { urgency };
|
||||
let max_latency = if urgency == "critical" {
|
||||
10.0
|
||||
} else if urgency == "high" {
|
||||
20.0
|
||||
} else {
|
||||
30.0
|
||||
};
|
||||
|
||||
let mut cloud_allowed = bool_of(context.get("cloud_allowed"), true);
|
||||
if flags.iter().any(|f| f == "HIGH_RISK_IRREVERSIBLE") && !verification_available {
|
||||
cloud_allowed = true;
|
||||
}
|
||||
|
||||
let verification_required = score >= 0.6 || flags.iter().any(|f| f == "TASK_FAILURE");
|
||||
|
||||
let mut tool_allowed = bool_of(context.get("tool_allowed"), true);
|
||||
if channel == "conversation" {
|
||||
tool_allowed = false;
|
||||
}
|
||||
|
||||
let attention_budget = if score >= 0.8 {
|
||||
1000
|
||||
} else if score >= 0.6 {
|
||||
800
|
||||
} else {
|
||||
400
|
||||
};
|
||||
|
||||
json!({
|
||||
"start_tier": start_tier,
|
||||
"max_tier": max_tier,
|
||||
"max_attempts": max_attempts,
|
||||
"max_tokens": max_tokens,
|
||||
"score": score,
|
||||
"max_latency": max_latency,
|
||||
"cloud_allowed": cloud_allowed,
|
||||
"verification_required": verification_required,
|
||||
"tool_allowed": tool_allowed,
|
||||
"attention_budget": attention_budget,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_basic_budget() {
|
||||
let salience = json!({"score": 0.5, "minimum_tier": "T2", "flags": []});
|
||||
let context = json!({});
|
||||
let b = calculate_budget(&salience, &context);
|
||||
assert_eq!(b["max_attempts"], 3);
|
||||
assert_eq!(b["max_tokens"], 2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_budget() {
|
||||
let salience = json!({"score": 0.8, "minimum_tier": "T3", "flags": ["REPEATED_RETRY"]});
|
||||
let b = calculate_budget(&salience, &json!({}));
|
||||
assert_eq!(b["max_attempts"], 5);
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn budget_with_salience() {
|
||||
let salience =
|
||||
serde_json::json!({"score": 0.8, "minimum_tier": "coding", "flags": ["DIRECTIVE"]});
|
||||
let context = serde_json::json!({"window": 32768});
|
||||
let b = calculate_budget(&salience, &context);
|
||||
assert!(b.is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_zero_salience() {
|
||||
let salience = serde_json::json!({"score": 0.0, "minimum_tier": "quick", "flags": []});
|
||||
let context = serde_json::json!({"window": 8192});
|
||||
let b = calculate_budget(&salience, &context);
|
||||
assert!(b.is_object());
|
||||
}
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// cb_calculate({"salience": {...}, "context": {...}})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cb_calculate(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let salience = a.get("salience").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
let context = a.get("context").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
calculate_budget(&salience, &context)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
// the whole process — duplicate symbols break the cdylib link).
|
||||
Generated
+39
@@ -2,10 +2,20 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "construction_intent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -39,6 +49,35 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
|
||||
@@ -8,3 +8,4 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
regex = "1"
|
||||
@@ -0,0 +1,76 @@
|
||||
//! construction_intent branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (/tmp/ci_truth.json, 48 resolve cases + 10 is_question + 8
|
||||
//! imperative_verb across every branch, frozen 2026-10-11). Embedded
|
||||
//! VERBATIM — never transcribed.
|
||||
|
||||
use crate::{imperative_verb, is_question, resolve_construction_intent};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_ci_truth.json");
|
||||
|
||||
/// The facade advances next_turn + record_turn before delegating; the
|
||||
/// kernel receives the POST-mutation context. This helper replicates that
|
||||
/// contract and asserts it against the oracle's recorded post-state.
|
||||
fn advance(input: &Value) -> Value {
|
||||
let mut ctx = input["context"].clone();
|
||||
let t0 = ctx["turn"].as_i64().unwrap();
|
||||
ctx["turn"] = json!(t0 + 2);
|
||||
let mut turns = ctx["recent_turns"].as_array().cloned().unwrap_or_default();
|
||||
turns.push(json!({"role": "user", "text": input["text"], "turn": t0 + 1}));
|
||||
ctx["recent_turns"] = Value::Array(turns);
|
||||
ctx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_parity_battery() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for case in truth["resolve"].as_array().unwrap() {
|
||||
let input = &case["input"];
|
||||
let ctx = advance(input);
|
||||
assert_eq!(
|
||||
&ctx, &case["post_context"],
|
||||
"case '{}': facade mutation contract (turn+2, recorded user turn)",
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
let out = resolve_construction_intent(
|
||||
input["text"].as_str().unwrap(),
|
||||
&ctx,
|
||||
input["origin_context_id"].as_str(),
|
||||
);
|
||||
let mut got = out;
|
||||
got.as_object_mut().unwrap().remove("intent_id"); // minted by the facade
|
||||
assert_eq!(
|
||||
&got, &case["expected"],
|
||||
"case '{}' text {:?}",
|
||||
case["name"].as_str().unwrap(),
|
||||
input["text"].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_question_parity_battery() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for case in truth["is_question"].as_array().unwrap() {
|
||||
assert_eq!(
|
||||
is_question(case[0].as_str().unwrap()),
|
||||
case[1].as_bool().unwrap(),
|
||||
"is_question({:?})",
|
||||
case[0].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imperative_verb_parity_battery() {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
for case in truth["imperative_verb"].as_array().unwrap() {
|
||||
// null → None; the kernel returns the canonical &'static str verb
|
||||
assert_eq!(
|
||||
imperative_verb(case[0].as_str().unwrap()),
|
||||
case[1].as_str(),
|
||||
"imperative_verb({:?})",
|
||||
case[0].as_str().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,200 +1,700 @@
|
||||
//! construction_intent — construction intent classification (W6).
|
||||
pub fn classify_intent(text: &str) -> serde_json::Value {
|
||||
let lower = text.to_lowercase();
|
||||
if lower.contains("fix") || lower.contains("bug") || lower.contains("repair") {
|
||||
serde_json::json!({"intent": "fix", "text": text})
|
||||
} else if lower.contains("add") || lower.contains("create") || lower.contains("implement") {
|
||||
serde_json::json!({"intent": "feature", "text": text})
|
||||
} else if lower.contains("test") || lower.contains("verify") {
|
||||
serde_json::json!({"intent": "verify", "text": text})
|
||||
} else if lower.contains("design") || lower.contains("architect") {
|
||||
serde_json::json!({"intent": "design", "text": text})
|
||||
} else {
|
||||
serde_json::json!({"intent": "unknown", "text": text})
|
||||
//! construction_intent — the conversational interpretation layer (§17J–§17N).
|
||||
//!
|
||||
//! FULL parity port of src/rune/construction_intent.py (dichotomy wave,
|
||||
//! #2479 tranche). This layer NEVER deepens or reclassifies — it resolves
|
||||
//! what the user MEANT and whether they AUTHORIZED it. Scaffolding
|
||||
//! (classify_mode) owns classification/depth/contracts.
|
||||
//!
|
||||
//! FFI (doc-envelope law): `ci_resolve({text, origin_context_id?, context})`
|
||||
//! → {"ok": true, "value": {"intent": {...ConstructionIntent dict}}}.
|
||||
//! The context passed in is the POST-mutation state (the Python facade
|
||||
//! advances next_turn/record_turn before delegating); the kernel is pure.
|
||||
//!
|
||||
//! intent_id is minted by the Python facade (uuid4) — the kernel returns "".
|
||||
//!
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the decision lattices
|
||||
//! are enums with exhaustive matches at the JSON boundary, tokenization on
|
||||
//! the hot path is zero-copy over the lowered input, imperative verbs come
|
||||
//! back as canonical `&'static str`, and the referent machinery borrows the
|
||||
//! context Value (`Prop<'a>`) instead of cloning proposal strings.
|
||||
|
||||
use regex::Regex;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const STALE_GAP_TURNS: i64 = 2;
|
||||
|
||||
const CONSTRUCTION_VERBS: &[&str] = &[
|
||||
"build", "implement", "add", "replace", "fix", "create", "make", "write", "port", "migrate",
|
||||
"refactor", "integrate", "remove", "extend", "wire", "ship",
|
||||
];
|
||||
const CREATION_VERBS: &[&str] = &["build", "create", "make", "write", "develop"];
|
||||
const DEMONSTRATIVES: &[&str] = &["that", "this", "it", "those", "these"];
|
||||
const ARTICLES: &[&str] = &["the", "a", "an", "this", "these", "those", "one"];
|
||||
|
||||
// ── decision lattices (enums, not string soup) ──────────────────────────
|
||||
|
||||
/// Action-vs-discussion (§17N).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ActionIntent {
|
||||
Action,
|
||||
Discussion,
|
||||
}
|
||||
|
||||
impl ActionIntent {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ActionIntent::Action => "ACTION",
|
||||
ActionIntent::Discussion => "DISCUSSION",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// intent_tokens — set of lowercase alphanumeric tokens.
|
||||
/// DIVERGENCE (documented improvement): the oracle's re.split yields {""}
|
||||
/// for empty/all-separator input; the port yields {} (an empty token is
|
||||
/// never a meaningful signal — every consumer tests membership).
|
||||
pub fn intent_tokens(text: &str) -> std::collections::BTreeSet<String> {
|
||||
text.to_lowercase()
|
||||
.split(|c: char| !c.is_ascii_alphanumeric() || c == '_')
|
||||
.filter(|w| !w.is_empty())
|
||||
.map(|w| w.to_string())
|
||||
.collect()
|
||||
/// Approval-state lattice (§17L). The kernel only ever EMITS Discussion
|
||||
/// and UserApproved (authorization is granted, never held as Proposal —
|
||||
/// proposal recording is Scaffolding's side of the boundary); the full
|
||||
/// lattice is modeled for exhaustiveness at the boundary.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ApprovalState {
|
||||
Discussion,
|
||||
#[allow(dead_code)] // §17L lattice completeness; never emitted
|
||||
Proposal,
|
||||
UserApproved,
|
||||
}
|
||||
|
||||
const CONSTRUCTION_VERBS: &[&str] = &[
|
||||
"build",
|
||||
"implement",
|
||||
"add",
|
||||
"replace",
|
||||
"fix",
|
||||
"create",
|
||||
"make",
|
||||
"write",
|
||||
"port",
|
||||
"migrate",
|
||||
"refactor",
|
||||
"integrate",
|
||||
"remove",
|
||||
"extend",
|
||||
"wire",
|
||||
"ship",
|
||||
];
|
||||
|
||||
const QUESTION_OPENERS: &[&str] = &[
|
||||
"how", "what", "why", "when", "where", "who", "which", "should", "could", "would", "can", "is",
|
||||
"are", "did",
|
||||
];
|
||||
|
||||
const LEAD_VERBS: &[&str] = &[
|
||||
"build",
|
||||
"implement",
|
||||
"add",
|
||||
"fix",
|
||||
"make",
|
||||
"create",
|
||||
"write",
|
||||
"replace",
|
||||
"remove",
|
||||
];
|
||||
|
||||
const IDIOM_PREFIXES: &[&str] = &["go ahead and", "go and", "please", "kindly", "then", "now"];
|
||||
|
||||
fn strip_idiom_prefixes(t: &str) -> String {
|
||||
let mut out = t.to_string();
|
||||
for _ in 0..3 {
|
||||
let mut changed = false;
|
||||
let low = out.to_lowercase();
|
||||
for p in IDIOM_PREFIXES {
|
||||
if low.starts_with(&format!("{} ", p)) || low == *p {
|
||||
out = out[p.len()..].trim_start().to_string();
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
impl ApprovalState {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ApprovalState::Discussion => "DISCUSSION",
|
||||
ApprovalState::Proposal => "PROPOSAL",
|
||||
ApprovalState::UserApproved => "USER_APPROVED",
|
||||
}
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ambiguity lattice (§17M). Scope exists in the spec but is never
|
||||
/// produced by the oracle — modeled for lattice completeness.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Ambiguity {
|
||||
None,
|
||||
Referent,
|
||||
Target,
|
||||
#[allow(dead_code)] // spec lattice completeness; oracle never emits it
|
||||
Scope,
|
||||
}
|
||||
|
||||
impl Ambiguity {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Ambiguity::None => "NONE",
|
||||
Ambiguity::Referent => "REFERENT",
|
||||
Ambiguity::Target => "TARGET",
|
||||
Ambiguity::Scope => "SCOPE",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consequence lattice — destructive > mutating > light vocabulary.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Consequence {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
|
||||
impl Consequence {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Consequence::High => "HIGH",
|
||||
Consequence::Medium => "MEDIUM",
|
||||
Consequence::Low => "LOW",
|
||||
}
|
||||
}
|
||||
|
||||
/// `_consequence` — first matching tier wins (HIGH checked first).
|
||||
fn classify(text: &str) -> Consequence {
|
||||
let low = text.to_lowercase();
|
||||
if ["replace", "migrate", "rewrite", "delete", "remove all", "subsystem"]
|
||||
.iter()
|
||||
.any(|w| low.contains(w))
|
||||
{
|
||||
return Consequence::High;
|
||||
}
|
||||
if ["add", "extend", "port", "integrate"].iter().any(|w| low.contains(w)) {
|
||||
return Consequence::Medium;
|
||||
}
|
||||
Consequence::Low
|
||||
}
|
||||
}
|
||||
|
||||
fn re(pat: &'static str) -> &'static Regex {
|
||||
static CACHE: OnceLock<std::collections::HashMap<&'static str, Regex>> = OnceLock::new();
|
||||
CACHE
|
||||
.get_or_init(|| {
|
||||
let mut m = std::collections::HashMap::new();
|
||||
m.insert("new_article", Regex::new(r"\b(me a|me an|a|an|another)\b").unwrap());
|
||||
m.insert(
|
||||
"question_markers",
|
||||
Regex::new(r"(?i)^(?:\s*(?:how|what|why|when|where|who|which|should|could|would|can|is|are|did|do you|do we|do they|does it|does this|does that)\b|\?\s*$)")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"analysis",
|
||||
Regex::new(r"(?i)\b(?:think|opinion|view|recommend|suggest|assess|evaluate|review|compare|consider|worth it|worth it\?|needs? improvement|any ideas?)\b")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"go_ahead",
|
||||
Regex::new(r"(?i)^\s*(?:yes|yeah|yep|ok|okay|sure|go ahead|please do|do it|do that|carry on|proceed|approved|lgtm|ship it)\b[\s.,!]*$")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"affirmation",
|
||||
Regex::new(r"(?i)^\s*(?:yes|yeah|yep|yup|ok|okay|sure|agreed|absolutely|indeed|alright|right)\b")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"approval_idiom",
|
||||
Regex::new(r"(?i)\b(?:go ahead|do it|do that|carry on|build that|implement that|make it so|make it happen|make that happen|that's the one|go with|let's go|let's do|let's have it|proceed|good with that|fine with that|happy with that|on board|approved|ship it)\b")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"negation",
|
||||
Regex::new(r"(?i)^\s*(?:don't|do not|dont|never|not yet|stop|hold off|no need|skip it|forget it)\b")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"idiom_prefix",
|
||||
Regex::new(r"(?i)^\s*(?:go ahead and|go and|please|kindly|then|now)\s+").unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"question_guard",
|
||||
Regex::new(r"(?i)^\s*(?:build|implement|add|fix|make|create|write|replace|remove)\b")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"info_opener",
|
||||
Regex::new(r"(?i)^\s*(?:how|what|why|when|where|which)\b").unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"request_modal",
|
||||
Regex::new(r"(?i)\b(?:can you|could you|would you|will you|please)\b").unwrap(),
|
||||
);
|
||||
m.insert(
|
||||
"option_referents",
|
||||
Regex::new(r"\b(first|second|third|other|latter|former)\s+(option|approach|one)\b")
|
||||
.unwrap(),
|
||||
);
|
||||
m.insert("word_runs", Regex::new(r"[a-z']+").unwrap());
|
||||
m.insert("first_word", Regex::new(r"(?i)^\s*(\w+)").unwrap());
|
||||
m.insert("token_split", Regex::new(r"[^a-z0-9]+").unwrap());
|
||||
m
|
||||
})
|
||||
.get(pat)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// `_tokens` — set of lowercase alphanumeric runs (owned; lowercases the
|
||||
/// input like the oracle). Mirrors Python re.split semantics (leading/
|
||||
/// trailing separators yield "" pieces).
|
||||
pub fn tokens(text: &str) -> HashSet<String> {
|
||||
re("token_split").split(&text.to_lowercase()).map(String::from).collect()
|
||||
}
|
||||
|
||||
/// Zero-copy twin of [`tokens`] for input that is ALREADY lowercase —
|
||||
/// the hot decision path lowers once and borrows thereafter.
|
||||
fn tokens_lower(text: &str) -> HashSet<&str> {
|
||||
re("token_split").split(text).collect()
|
||||
}
|
||||
|
||||
/// `_is_question` — interrogative STRUCTURE, not vocabulary: terminal '?'
|
||||
/// (unless a construction verb opens the turn) or a leading wh/auxiliary.
|
||||
pub fn is_question(text: &str) -> bool {
|
||||
if text.contains('?') && !re("question_guard").is_match(text) {
|
||||
return true;
|
||||
}
|
||||
re("question_markers").is_match(text)
|
||||
}
|
||||
|
||||
/// `_imperative_verb` — leading imperative construction verb after idiom
|
||||
/// prefixes are stripped (up to 3 passes, mirroring the oracle). Returns
|
||||
/// the canonical verb — zero-copy, no allocation, no borrowed-temp hazard.
|
||||
pub fn imperative_verb(text: &str) -> Option<&'static str> {
|
||||
let mut t = text;
|
||||
for _ in 0..3 {
|
||||
match re("idiom_prefix").find(t) {
|
||||
Some(m) if m.start() == 0 => t = &t[m.end()..],
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
let word = re("first_word").captures(t)?.get(1)?.as_str();
|
||||
// the oracle returns the lowercased leading word; for a construction
|
||||
// verb that is exactly the canonical set entry
|
||||
CONSTRUCTION_VERBS.iter().find(|v| word.eq_ignore_ascii_case(v)).copied()
|
||||
}
|
||||
|
||||
/// `_is_relative_that` — 'that' reads as a relative pronoun when its
|
||||
/// PRECEDING word is an article; demonstrative when sentence-initial,
|
||||
/// post-verb, or after any other word (T18 18N fix).
|
||||
fn is_relative_that(low: &str, dem_tokens: &HashSet<&str>) -> bool {
|
||||
let words: Vec<&str> = re("word_runs").find_iter(low).map(|m| m.as_str()).collect();
|
||||
let mut seen_that = false;
|
||||
for (i, w) in words.iter().enumerate() {
|
||||
if *w != "that" {
|
||||
continue;
|
||||
}
|
||||
seen_that = true;
|
||||
let prev = if i > 0 { Some(words[i - 1]) } else { None };
|
||||
match prev {
|
||||
None => return false,
|
||||
Some(p) if CONSTRUCTION_VERBS.contains(&p) => return false,
|
||||
Some(p) if !ARTICLES.contains(&p) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
seen_that && dem_tokens.contains("that")
|
||||
}
|
||||
|
||||
/// `_references` — distinctive content words (len>4) of the proposal
|
||||
/// summary appearing in the user's text revive a stale proposal.
|
||||
fn references(summary: &str, low: &str) -> bool {
|
||||
let words: HashSet<String> =
|
||||
tokens(summary).into_iter().filter(|w| w.chars().count() > 4).collect();
|
||||
if words.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let low_tokens = tokens_lower(low);
|
||||
words.iter().any(|w| low_tokens.contains(w.as_str()))
|
||||
}
|
||||
|
||||
/// `_constraints` — constraint language captured from the last 6 turns.
|
||||
fn constraints(ctx: &Value) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
let turns = ctx.get("recent_turns").and_then(|v| v.as_array());
|
||||
if let Some(turns) = turns {
|
||||
for turn in turns.iter().rev().take(6).rev() {
|
||||
let txt = turn.get("text").and_then(|v| v.as_str()).unwrap_or("").to_lowercase();
|
||||
if txt.contains("must not")
|
||||
|| txt.contains("don't")
|
||||
|| txt.contains("never")
|
||||
|| txt.contains("no hardcoded")
|
||||
{
|
||||
let text = turn.get("text").and_then(|v| v.as_str()).unwrap_or("");
|
||||
out.push(text.chars().take(120).collect::<String>());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// is_question — interrogative STRUCTURE: terminal '?' (unless a lead
|
||||
/// construction verb opens it) or a leading wh/auxiliary marker.
|
||||
pub fn is_question(text: &str) -> bool {
|
||||
// lead-verb guard: /^(build|implement|add|fix|make|create|write|replace|remove)\b/i
|
||||
let t = strip_idiom_prefixes(text);
|
||||
let first = t.split_whitespace().next().unwrap_or("").to_lowercase();
|
||||
let lead_verb = LEAD_VERBS.contains(&first.as_str());
|
||||
if text.contains('?') && !lead_verb {
|
||||
return true;
|
||||
/// A pending proposal borrowed from the context Value — zero-copy through
|
||||
/// the referent lattice (language advantage: lifetimes over clone-churn).
|
||||
struct Prop<'a> {
|
||||
proposal_id: &'a str,
|
||||
summary: &'a str,
|
||||
created_turn: i64,
|
||||
}
|
||||
|
||||
fn live_proposals(ctx: &Value) -> Vec<Prop<'_>> {
|
||||
ctx.get("pending_proposals")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter(|p| !p.get("superseded").and_then(|v| v.as_bool()).unwrap_or(false))
|
||||
.map(|p| Prop {
|
||||
proposal_id: p.get("proposal_id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
summary: p.get("summary").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
created_turn: p.get("created_turn").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn active_project(ctx: &Value) -> Option<&str> {
|
||||
ctx.get("active_project").and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Python truthiness of context.active_project ("" is falsy too).
|
||||
fn active_target(ctx: &Value) -> Option<&str> {
|
||||
active_project(ctx).filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn turn_of(ctx: &Value) -> i64 {
|
||||
ctx.get("turn").and_then(|v| v.as_i64()).unwrap_or(0)
|
||||
}
|
||||
|
||||
enum Referent<'a> {
|
||||
None,
|
||||
Proposal(Prop<'a>),
|
||||
Ambiguous(Vec<Prop<'a>>),
|
||||
}
|
||||
|
||||
/// `_resolve_referent` — recency-first resolution over LIVE proposals,
|
||||
/// stale-context-resistant (older than STALE_GAP_TURNS unless the text
|
||||
/// explicitly references the proposal's content).
|
||||
fn resolve_referent<'a>(low: &str, ctx: &'a Value) -> Referent<'a> {
|
||||
let live = live_proposals(ctx);
|
||||
if live.is_empty() {
|
||||
return Referent::None;
|
||||
}
|
||||
// _QUESTION_MARKERS: ^\s*(openers)\b or \?\s*$
|
||||
let low = text.to_lowercase();
|
||||
let trimmed = low.trim_start();
|
||||
for q in QUESTION_OPENERS {
|
||||
if trimmed == *q || trimmed.starts_with(&format!("{} ", q)) {
|
||||
return true;
|
||||
let turn = turn_of(ctx);
|
||||
let fresh: Vec<Prop<'a>> = live
|
||||
.into_iter()
|
||||
.filter(|p| turn - p.created_turn <= STALE_GAP_TURNS || references(p.summary, low))
|
||||
.collect();
|
||||
if fresh.is_empty() {
|
||||
return Referent::None; // all live proposals are stale for this turn
|
||||
}
|
||||
let live = fresh;
|
||||
if let Some(m) = re("option_referents").find(low) {
|
||||
let word = m.as_str().split_whitespace().next().unwrap_or("");
|
||||
let idx = match word {
|
||||
"first" => Some(0usize),
|
||||
"second" => Some(1),
|
||||
"third" => Some(2),
|
||||
"other" => Some(1),
|
||||
"latter" => Some(live.len() - 1),
|
||||
"former" => Some(0),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
if idx < live.len() {
|
||||
return Referent::Proposal(into_prop(live, idx));
|
||||
}
|
||||
}
|
||||
if live.len() > 1 {
|
||||
return Referent::Ambiguous(live);
|
||||
}
|
||||
}
|
||||
// do you / do we / do they / does it / does this / does that
|
||||
for phrase in [
|
||||
"do you",
|
||||
"do we",
|
||||
"do they",
|
||||
"does it",
|
||||
"does this",
|
||||
"does that",
|
||||
] {
|
||||
if let Some(rest) = trimmed.strip_prefix(phrase) {
|
||||
if rest.is_empty() || rest.starts_with(' ') || rest.starts_with('?') {
|
||||
return true;
|
||||
if live.len() > 1 {
|
||||
let newest = &live[live.len() - 1];
|
||||
let runner = &live[live.len() - 2];
|
||||
if newest.created_turn - runner.created_turn >= 2 {
|
||||
let n = live.len() - 1;
|
||||
return Referent::Proposal(into_prop(live, n));
|
||||
}
|
||||
return Referent::Ambiguous(live);
|
||||
}
|
||||
Referent::Proposal(into_prop(live, 0))
|
||||
}
|
||||
|
||||
fn into_prop(mut live: Vec<Prop<'_>>, idx: usize) -> Prop<'_> {
|
||||
live.swap_remove(idx)
|
||||
}
|
||||
|
||||
/// `_intent` — the ConstructionIntent builder (resolved_referent=None;
|
||||
/// consequence defaults by action_intent; enums serialize at the boundary).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn intent(
|
||||
objective: &str,
|
||||
action_intent: ActionIntent,
|
||||
approval_state: ApprovalState,
|
||||
confidence: f64,
|
||||
ambiguity: Ambiguity,
|
||||
ev: Vec<String>,
|
||||
probes: Vec<String>,
|
||||
ctx: &Value,
|
||||
origin_context_id: Option<&str>,
|
||||
requested_effect: Option<String>,
|
||||
consequence_level: Option<Consequence>,
|
||||
) -> Value {
|
||||
json!({
|
||||
"intent_id": "",
|
||||
"objective": objective,
|
||||
"requested_effect": requested_effect.filter(|s| !s.is_empty()).unwrap_or_else(|| objective.to_string()),
|
||||
"resolved_target": active_project(ctx),
|
||||
"resolved_referent": Value::Null,
|
||||
"origin_context_id": origin_context_id,
|
||||
"action_intent": action_intent.as_str(),
|
||||
"approval_state": approval_state.as_str(),
|
||||
"confidence": confidence,
|
||||
"ambiguity": ambiguity.as_str(),
|
||||
"consequence_level": consequence_level.map(Consequence::as_str).unwrap_or_else(|| match action_intent {
|
||||
ActionIntent::Discussion => "LOW",
|
||||
ActionIntent::Action => "MEDIUM",
|
||||
}),
|
||||
"relevant_constraints": constraints(ctx),
|
||||
"classification_evidence": ev,
|
||||
"probe_questions": probes,
|
||||
})
|
||||
}
|
||||
|
||||
/// `resolve_construction_intent` — resolve one user turn into a structured
|
||||
/// ConstructionIntent (criteria §17J in precedence order: negation frame,
|
||||
/// request modals, interrogative structure, authorization of a prior
|
||||
/// proposal, explicit imperative demand, default discussion).
|
||||
pub fn resolve_construction_intent(text: &str, ctx: &Value, origin_context_id: Option<&str>) -> Value {
|
||||
let t = text.trim();
|
||||
let low = t.to_lowercase();
|
||||
let mut ev: Vec<String> = vec![];
|
||||
let mut probes: Vec<String> = vec![];
|
||||
|
||||
// 0. NEGATION (§18N): construction language inside a negation frame
|
||||
// is NOT a demand.
|
||||
if re("negation").is_match(t) {
|
||||
ev.push("negation frame — explicitly blocks construction".into());
|
||||
return intent(t, ActionIntent::Discussion, ApprovalState::Discussion, 0.85,
|
||||
Ambiguity::None, ev, probes, ctx, origin_context_id, None, None);
|
||||
}
|
||||
|
||||
// 1a. REQUEST MODALS: information openers outrank modals — "how would
|
||||
// you build X?" asks about METHOD; "can you fix that?" requests ACTION.
|
||||
let information_opener = re("info_opener").is_match(&low);
|
||||
let mut request_modal = !information_opener && re("request_modal").is_match(&low);
|
||||
if request_modal {
|
||||
request_modal = tokens_lower(&low).iter().any(|w| CONSTRUCTION_VERBS.contains(w));
|
||||
}
|
||||
if request_modal {
|
||||
ev.push("request modal (can/could/would you, please) + construction verb → action request, not discussion".into());
|
||||
let dem_tokens_m: HashSet<&str> =
|
||||
tokens_lower(&low).into_iter().filter(|w| DEMONSTRATIVES.contains(w)).collect();
|
||||
let has_live = !live_proposals(ctx).is_empty();
|
||||
if !dem_tokens_m.is_empty() && has_live {
|
||||
if let Referent::Proposal(tp) = resolve_referent(&low, ctx) {
|
||||
ev.push(format!("modal authorization resolves proposal {}", tp.proposal_id));
|
||||
return json!({
|
||||
"intent_id": "",
|
||||
"objective": tp.summary,
|
||||
"requested_effect": format!("construct: {}", tp.summary),
|
||||
"resolved_target": active_project(ctx),
|
||||
"resolved_referent": tp.proposal_id,
|
||||
"origin_context_id": origin_context_id,
|
||||
"action_intent": ActionIntent::Action.as_str(),
|
||||
"approval_state": ApprovalState::UserApproved.as_str(),
|
||||
"confidence": 0.85,
|
||||
"ambiguity": Ambiguity::None.as_str(),
|
||||
"consequence_level": Consequence::classify(t).as_str(),
|
||||
"relevant_constraints": constraints(ctx),
|
||||
"classification_evidence": ev,
|
||||
"probe_questions": probes,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(target) = active_target(ctx) {
|
||||
ev.push(format!("target resolved from active context: {target}"));
|
||||
return json!({
|
||||
"intent_id": "",
|
||||
"objective": t,
|
||||
"requested_effect": format!("fix/act: {t}"),
|
||||
"resolved_target": target,
|
||||
"resolved_referent": "this-turn-topic",
|
||||
"origin_context_id": origin_context_id,
|
||||
"action_intent": ActionIntent::Action.as_str(),
|
||||
"approval_state": ApprovalState::UserApproved.as_str(),
|
||||
"confidence": 0.85,
|
||||
"ambiguity": Ambiguity::None.as_str(),
|
||||
"consequence_level": Consequence::classify(t).as_str(),
|
||||
"relevant_constraints": constraints(ctx),
|
||||
"classification_evidence": ev,
|
||||
"probe_questions": probes,
|
||||
});
|
||||
}
|
||||
ev.push("request modal but no resolvable target".into());
|
||||
probes.push("where should that change land — which project?".into());
|
||||
return intent(t, ActionIntent::Action, ApprovalState::Discussion, 0.5,
|
||||
Ambiguity::Target, ev, probes, ctx, origin_context_id, None, None);
|
||||
}
|
||||
|
||||
// 1. questions are discussion — even with construction verbs inside
|
||||
if is_question(t) {
|
||||
ev.push("interrogative structure → discussion, not authorization".into());
|
||||
if re("analysis").is_match(t) {
|
||||
ev.push("analysis/advice request — a proposal may be recorded for later authorization".into());
|
||||
}
|
||||
return json!({
|
||||
"intent_id": "",
|
||||
"objective": t,
|
||||
"requested_effect": "analysis/discussion",
|
||||
"resolved_target": active_project(ctx),
|
||||
"resolved_referent": Value::Null,
|
||||
"origin_context_id": origin_context_id,
|
||||
"action_intent": ActionIntent::Discussion.as_str(),
|
||||
"approval_state": ApprovalState::Discussion.as_str(),
|
||||
"confidence": 0.85,
|
||||
"ambiguity": Ambiguity::None.as_str(),
|
||||
"consequence_level": Consequence::Low.as_str(),
|
||||
// the oracle's question-path return omits relevant_constraints
|
||||
// (dataclass default [] — fuzz-caught divergence)
|
||||
"relevant_constraints": [],
|
||||
"classification_evidence": ev,
|
||||
"probe_questions": probes,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. authorization of a prior proposal (bare approval or demonstrative)
|
||||
let bare_approval = re("go_ahead").is_match(t);
|
||||
let imperative = imperative_verb(t);
|
||||
let mut dem_tokens: HashSet<&str> =
|
||||
tokens_lower(&low).into_iter().filter(|w| DEMONSTRATIVES.contains(w)).collect();
|
||||
if dem_tokens.contains("that") && is_relative_that(&low, &dem_tokens) {
|
||||
dem_tokens.remove("that");
|
||||
ev.push("'that' is a relative pronoun here, not a referent".into());
|
||||
}
|
||||
let option_ref = re("option_referents").is_match(&low);
|
||||
let demonstrative = !dem_tokens.is_empty() || option_ref;
|
||||
let approval_idiom = re("approval_idiom").is_match(&low);
|
||||
let affirmation = re("affirmation").is_match(t);
|
||||
let authorized = bare_approval
|
||||
|| approval_idiom
|
||||
|| (affirmation && (demonstrative || option_ref))
|
||||
|| (imperative.is_some() && (demonstrative || option_ref))
|
||||
|| (option_ref && imperative.is_some());
|
||||
|
||||
if authorized {
|
||||
match resolve_referent(&low, ctx) {
|
||||
Referent::None => {
|
||||
if imperative.is_none() {
|
||||
ev.push("authorization language but NO live resolvable referent — never guessed".into());
|
||||
probes.push("what should that refer to? I don't have an active proposal matching it.".into());
|
||||
return intent(t, ActionIntent::Action, ApprovalState::Discussion, 0.4,
|
||||
Ambiguity::Referent, ev, probes, ctx, origin_context_id, None, None);
|
||||
}
|
||||
ev.push("no live proposal matches; treating as direct demand (explicit imperative object present)".into());
|
||||
ev.push("proposals stale for this turn — not selected".into());
|
||||
ev.push("stale proposals ignored; explicit imperative → demand path".into());
|
||||
return demand_path(t, &low, imperative, ctx, origin_context_id, ev, probes);
|
||||
}
|
||||
Referent::Ambiguous(cands) => {
|
||||
ev.push("multiple equally plausible referents — asking, not guessing (§17M)".into());
|
||||
let joined = cands
|
||||
.iter()
|
||||
.map(|p| p.summary.chars().take(40).collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or ");
|
||||
probes.push(format!("which one — {joined}?"));
|
||||
return intent(t, ActionIntent::Action, ApprovalState::Discussion, 0.4,
|
||||
Ambiguity::Referent, ev, probes, ctx, origin_context_id, None, None);
|
||||
}
|
||||
Referent::Proposal(tp) => {
|
||||
ev.push(format!("referent resolved: {} (recency-first among live proposals)", tp.proposal_id));
|
||||
ev.push("prior proposal + explicit authorization → USER_APPROVED".into());
|
||||
return json!({
|
||||
"intent_id": "",
|
||||
"objective": tp.summary,
|
||||
"requested_effect": format!("construct: {}", tp.summary),
|
||||
"resolved_target": active_project(ctx),
|
||||
"resolved_referent": tp.proposal_id,
|
||||
"origin_context_id": origin_context_id,
|
||||
"action_intent": ActionIntent::Action.as_str(),
|
||||
"approval_state": ApprovalState::UserApproved.as_str(),
|
||||
"confidence": 0.9,
|
||||
"ambiguity": Ambiguity::None.as_str(),
|
||||
"consequence_level": Consequence::classify(t).as_str(),
|
||||
"relevant_constraints": constraints(ctx),
|
||||
"classification_evidence": ev,
|
||||
"probe_questions": probes,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if low.trim_end().ends_with('?') {
|
||||
return true;
|
||||
|
||||
// 3. explicit imperative construction demand with resolved target
|
||||
if let Some(imp) = imperative {
|
||||
ev.push(format!(
|
||||
"imperative construction verb: {imp} — strong/direct demand signal (§17K)"
|
||||
));
|
||||
if let Some(target) = active_target(ctx) {
|
||||
ev.push(format!("target resolved from active context: {target}"));
|
||||
return intent(t, ActionIntent::Action, ApprovalState::UserApproved, 0.9,
|
||||
Ambiguity::None, ev, probes, ctx, origin_context_id,
|
||||
Some(format!("construct: {t}")), Some(Consequence::classify(t)));
|
||||
}
|
||||
if CREATION_VERBS.contains(&imp) && re("new_article").is_match(&low) {
|
||||
ev.push("creation verb + indefinite article → new standalone artifact demand; no existing target required".into());
|
||||
return intent(t, ActionIntent::Action, ApprovalState::UserApproved, 0.85,
|
||||
Ambiguity::None, ev, probes, ctx, origin_context_id,
|
||||
Some(format!("construct new: {t}")), None);
|
||||
}
|
||||
ev.push("imperative demand but no resolvable target — clarification, never guessing".into());
|
||||
probes.push("build where — which project/system?".into());
|
||||
return intent(t, ActionIntent::Action, ApprovalState::Discussion, 0.5,
|
||||
Ambiguity::Target, ev, probes, ctx, origin_context_id, None, None);
|
||||
}
|
||||
false
|
||||
|
||||
// 4. default: discussion / context statement
|
||||
ev.push("no authorization structure, no imperative demand → discussion".into());
|
||||
intent(t, ActionIntent::Discussion, ApprovalState::Discussion, 0.7, Ambiguity::None,
|
||||
ev, probes, ctx, origin_context_id, None, None)
|
||||
}
|
||||
|
||||
/// imperative_verb — leading imperative construction verb after idiom
|
||||
/// prefixes are stripped (up to 3 passes, mirroring the oracle).
|
||||
pub fn imperative_verb(text: &str) -> Option<String> {
|
||||
let t = strip_idiom_prefixes(text);
|
||||
let first = t.split_whitespace().next().unwrap_or("").to_lowercase();
|
||||
// oracle: re.match(r"^\s*(\w+)", t) then membership in _CONSTRUCTION_VERBS
|
||||
// (\w includes underscore/digits — split_whitespace().next() matches on
|
||||
// the token level; punctuation-attached verbs like "build," differ — see test)
|
||||
let word: String = first
|
||||
.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect();
|
||||
if CONSTRUCTION_VERBS.contains(&word.as_str()) {
|
||||
return Some(word);
|
||||
/// The demand path shared by stale/fallthrough authorization (target from
|
||||
/// context or clarify — never stale). Returns the intent Value.
|
||||
fn demand_path(
|
||||
t: &str,
|
||||
low: &str,
|
||||
imperative: Option<&'static str>,
|
||||
ctx: &Value,
|
||||
origin_context_id: Option<&str>,
|
||||
mut ev: Vec<String>,
|
||||
mut probes: Vec<String>,
|
||||
) -> Value {
|
||||
let target = active_target(ctx);
|
||||
let creation_standalone = match imperative {
|
||||
Some(imp) if CREATION_VERBS.contains(&imp) => re("new_article").is_match(low),
|
||||
_ => false,
|
||||
};
|
||||
if target.is_none() && creation_standalone {
|
||||
ev.push("creation verb + indefinite article → new standalone artifact; no existing target required".into());
|
||||
return intent(t, ActionIntent::Action, ApprovalState::UserApproved, 0.85,
|
||||
Ambiguity::None, ev, probes, ctx, origin_context_id,
|
||||
Some(format!("construct new: {t}")), Some(Consequence::classify(t)));
|
||||
}
|
||||
None
|
||||
if let Some(target) = target {
|
||||
ev.push(format!("target resolved from active context: {target}"));
|
||||
return intent(t, ActionIntent::Action, ApprovalState::UserApproved, 0.8,
|
||||
Ambiguity::None, ev, probes, ctx, origin_context_id,
|
||||
Some(format!("construct: {t}")), Some(Consequence::classify(t)));
|
||||
}
|
||||
probes.push("build where — which project/system?".into());
|
||||
intent(t, ActionIntent::Action, ApprovalState::Discussion, 0.5, Ambiguity::Target,
|
||||
ev, probes, ctx, origin_context_id, None, None)
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// ci_resolve({"text", "origin_context_id"?, "context": {
|
||||
/// "turn", "recent_turns", "pending_proposals",
|
||||
/// "active_project", "active_task_id"}})
|
||||
/// → {"intent": {...ConstructionIntent dict, intent_id ""}}. The context
|
||||
/// arrives POST-mutation (the Python facade advanced it); kernel is pure.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ci_resolve(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let text = a.get("text").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let origin = a.get("origin_context_id").and_then(|v| v.as_str());
|
||||
let ctx = a.get("context").cloned().unwrap_or_else(|| Value::Object(Map::new()));
|
||||
json!({"intent": resolve_construction_intent(text, &ctx, origin)})
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no ci_free_string export — sibling crates sharing the Python
|
||||
// process already export identical free symbols; a duplicate breaks the
|
||||
// cdylib link. Callers free through capability_scoring's cs_free_string.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn signal_helpers_parity() {
|
||||
// oracle-probed (batch 123): question STRUCTURE, not vocabulary
|
||||
assert!(is_question("How would you build this?"));
|
||||
assert!(is_question("what time is it?"));
|
||||
assert!(!is_question("Build me a program"));
|
||||
assert!(!is_question("The weather is nice"));
|
||||
assert!(!is_question("replace the old widget"));
|
||||
assert_eq!(
|
||||
imperative_verb("Build me a program"),
|
||||
Some("build".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
imperative_verb("please implement X now"),
|
||||
Some("implement".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
imperative_verb("go ahead and fix the bug"),
|
||||
Some("fix".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
imperative_verb(" Build something"),
|
||||
Some("build".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
imperative_verb("replace the old widget"),
|
||||
Some("replace".to_string())
|
||||
);
|
||||
assert_eq!(imperative_verb("How would you build this?"), None);
|
||||
assert_eq!(imperative_verb("The weather is nice"), None);
|
||||
// tokens: lowercase alphanumeric split (empty -> {""} like re.split)
|
||||
let t = intent_tokens("Hello, World! 123");
|
||||
assert!(t.contains("hello") && t.contains("world") && t.contains("123"));
|
||||
assert_eq!(t.len(), 3);
|
||||
let t2 = intent_tokens("a-b_c.d");
|
||||
assert!(t2.contains("a") && t2.contains("b") && t2.contains("c") && t2.contains("d"));
|
||||
assert_eq!(t2.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intent_classification() {
|
||||
// pin the Rust kernel's actual behavior (the oracle's _is_question
|
||||
// is a separate function in the Python module — not part of
|
||||
// classify_intent's contract)
|
||||
assert_eq!(classify_intent("fix the parser bug")["intent"], "fix");
|
||||
assert_eq!(classify_intent("add a new feature")["intent"], "feature");
|
||||
assert_eq!(classify_intent("test the changes")["intent"], "verify");
|
||||
assert_eq!(
|
||||
classify_intent("design the architecture")["intent"],
|
||||
"design"
|
||||
);
|
||||
assert_eq!(classify_intent("hello world")["intent"], "unknown");
|
||||
}
|
||||
}
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1,512 @@
|
||||
//! construction_mode branch battery — parity vs the frozen Python-oracle
|
||||
//! truth (/tmp/cm_truth.json, 20 classify cases across every branch,
|
||||
//! frozen 2026-10-11).
|
||||
|
||||
use crate::classify_mode;
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"classify": [
|
||||
{
|
||||
"input": {
|
||||
"objective": "build a standalone scheduler service from scratch",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "PROJECT_BUILD",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: ['standalone', 'from scratch']"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.9,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": true,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "fix the typo in the README",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"evidence": [
|
||||
"smallness markers: ['typo']",
|
||||
"file mention binds scoped target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "small patch to the module",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"evidence": [
|
||||
"smallness markers: ['patch']",
|
||||
"scoping words bind to known target: ['the module']"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "add a flag to this project's deploy script",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "implement the new feature in the codebase",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "what is the confidence score",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"evidence": [
|
||||
"non-construction intent markers: ['confidence', 'what is']",
|
||||
"no construction verb present"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": null,
|
||||
"confidence": 0.85,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "make a list of items",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"evidence": [
|
||||
"non-construction intent markers: ['list']",
|
||||
"construction-verb object is not a buildable artifact"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": null,
|
||||
"confidence": 0.85,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"empty objective"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": null,
|
||||
"confidence": 0.0,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"what should be built?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build the thing",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"construction verb present but no target-scope evidence and no standalone/new-lifecycle language"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.35,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"is this a change to an existing project or a new standalone one?",
|
||||
"which project/module is the target?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build a standalone scheduler",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"standalone/new-lifecycle language: ['standalone']",
|
||||
"BUT target project already exists (structural evidence contradicts standalone language)"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.3,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"should this extend the existing project or start a genuinely separate one?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build a standalone scheduler",
|
||||
"kwargs": {
|
||||
"project_exists": null
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: ['standalone']",
|
||||
"no project-existence evidence for the target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.4,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"does a project for this already exist?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "refactor the module",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target",
|
||||
"BUT structural evidence proves the target project does not exist \u2014 refusing to manufacture existing-project semantics"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.3,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"project 'the target' has no structural record \u2014 is it new (project build) or does it live somewhere not bound?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "build me a program that renames photos",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "PROJECT_BUILD",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: []"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.9,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": true,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "fix a bug",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"construction verb present but no target-scope evidence and no standalone/new-lifecycle language"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.35,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"is this a change to an existing project or a new standalone one?",
|
||||
"which project/module is the target?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "add a flag to the deploy script",
|
||||
"kwargs": {
|
||||
"project_exists": false
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "PROJECT_BUILD",
|
||||
"evidence": [
|
||||
"indefinite-article new-artifact language",
|
||||
"standalone/new-lifecycle language: []"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.9,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": true,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "explain how the module works",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "create a CLI that sorts files",
|
||||
"kwargs": {
|
||||
"target_project": "rune",
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target",
|
||||
"target project: rune"
|
||||
],
|
||||
"target_project": "rune",
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "write tests for the service",
|
||||
"kwargs": {
|
||||
"project_exists": true
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"evidence": [
|
||||
"change scoped to an existing project/target"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "implement export feature",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "UNKNOWN",
|
||||
"evidence": [
|
||||
"construction verb present but no target-scope evidence and no standalone/new-lifecycle language"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.35,
|
||||
"clarification_required": true,
|
||||
"probe_questions": [
|
||||
"is this a change to an existing project or a new standalone one?",
|
||||
"which project/module is the target?"
|
||||
],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"objective": "quick fix for that module",
|
||||
"kwargs": {}
|
||||
},
|
||||
"out": {
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"evidence": [
|
||||
"smallness markers: ['quick fix']",
|
||||
"scoping words bind to known target: ['that module']"
|
||||
],
|
||||
"target_project": null,
|
||||
"project_authority": "rune_internal",
|
||||
"confidence": 0.8,
|
||||
"clarification_required": false,
|
||||
"probe_questions": [],
|
||||
"bootstrap_required": false,
|
||||
"override": null,
|
||||
"proposed_by_model": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
fn run_case(case: &serde_json::Value) -> serde_json::Value {
|
||||
let input = &case["input"];
|
||||
let tp = input["kwargs"].get("target_project").and_then(|v| v.as_str());
|
||||
let pe = match input["kwargs"].get("project_exists") {
|
||||
Some(serde_json::Value::Bool(b)) => Some(*b),
|
||||
_ => None,
|
||||
};
|
||||
classify_mode(
|
||||
input["objective"].as_str().unwrap_or(""),
|
||||
tp,
|
||||
pe,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_matches_frozen_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["classify"].as_array().expect("classify array").iter().enumerate() {
|
||||
let expected = &case["out"];
|
||||
let got = run_case(case);
|
||||
assert_eq!(
|
||||
got["mode"], expected["mode"],
|
||||
"case {} ({:?}): mode mismatch — got {}, want {}",
|
||||
idx,
|
||||
case["input"]["objective"],
|
||||
got["mode"], expected["mode"]
|
||||
);
|
||||
assert_eq!(
|
||||
got["confidence"], expected["confidence"],
|
||||
"case {} confidence mismatch",
|
||||
idx
|
||||
);
|
||||
assert_eq!(
|
||||
got["clarification_required"], expected["clarification_required"],
|
||||
"case {} clarification mismatch",
|
||||
idx
|
||||
);
|
||||
assert_eq!(
|
||||
got["probe_questions"], expected["probe_questions"],
|
||||
"case {} probes mismatch",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
//! construction_mode — contextual construction classification (W6).
|
||||
//! PARITY port of src/rune/construction_mode.py classify_mode.
|
||||
//! construction_mode — contextual construction classification.
|
||||
//! PARITY port of src/rune/construction_mode.py classify_mode (full decision
|
||||
//! tree: override, NOT_CONSTRUCTION verb-object check, empty objective,
|
||||
//! standalone/article-standalone with project-existence branching, local+
|
||||
//! scope corroboration, file-scope via known files, scoping, surface
|
||||
//! escalation, model-proposal pass-through).
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const MODES: &[&str] = &[
|
||||
"NOT_CONSTRUCTION",
|
||||
@@ -9,142 +15,349 @@ pub const MODES: &[&str] = &[
|
||||
"UNKNOWN",
|
||||
];
|
||||
|
||||
pub fn classify_mode(
|
||||
text: &str,
|
||||
project_exists: Option<bool>,
|
||||
is_standalone: bool,
|
||||
has_explicit_target: bool,
|
||||
) -> serde_json::Value {
|
||||
const STANDALONE_WORDS: &[&str] = &[
|
||||
"standalone", "new app", "new application", "new service", "new tool",
|
||||
"new project", "new repository", "own repo", "from scratch",
|
||||
"separate app", "separate service", "greenfield", "scaffold a new",
|
||||
"new web",
|
||||
];
|
||||
const KNOWN_FILES: &[&str] = &[
|
||||
"readme", "makefile", "dockerfile", "changelog", "license", "gemfile",
|
||||
"procfile", "jenkinsfile", "justfile",
|
||||
];
|
||||
const LOCAL_WORDS: &[&str] = &[
|
||||
"typo", "small fix", "tiny fix", "quick fix", "patch", "minor",
|
||||
"one-liner", "cosmetic", "comment fix", "small correction",
|
||||
];
|
||||
const SCOPING_WORDS: &[&str] = &[
|
||||
"existing", "the module", "the project", "the app", "the service",
|
||||
"current", "that module", "the codebase", "this module", "this project",
|
||||
"this app",
|
||||
];
|
||||
const CONSTRUCTION_WORDS: &[&str] = &[
|
||||
"build", "create", "make", "implement", "add", "write", "develop",
|
||||
"construct", "fix", "repair", "refactor", "migrate", "change", "extend",
|
||||
"replace", "feature", "export", "module", "service", "app", "tool",
|
||||
"component", "schema", "api",
|
||||
];
|
||||
const NOT_CONSTRUCTION_HINTS: &[&str] = &[
|
||||
"explain", "what is", "how does", "why does", "list", "confidence",
|
||||
"improvisation", "summarize", "describe", "teach me",
|
||||
];
|
||||
const CREATION_VERBS: &[&str] = &["build", "create", "make", "write", "develop"];
|
||||
const ARTIFACT_NOUNS: &[&str] = &[
|
||||
"program", "cli", "tool", "app", "script", "service", "library",
|
||||
"package", "module", "dashboard", "website", "bot", "generator",
|
||||
"tracker", "game", "application", "command",
|
||||
];
|
||||
const NON_CONSTRUCTIBLE: &[&str] = &["list", "confidence", "explanation", "summary", "chord", "note"];
|
||||
|
||||
fn tokens(text: &str) -> Vec<String> {
|
||||
let lower = text.to_lowercase();
|
||||
lower
|
||||
.split(|c: char| !c.is_ascii_alphanumeric())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Not a build request
|
||||
let not_construction_words = [
|
||||
"what", "how", "why", "tell me", "explain", "status", "check",
|
||||
];
|
||||
if not_construction_words.iter().any(|w| lower.starts_with(w))
|
||||
&& !lower.contains("build")
|
||||
&& !lower.contains("fix")
|
||||
&& !lower.contains("create")
|
||||
fn contains_any(text_lower: &str, phrases: &[&str]) -> Vec<String> {
|
||||
phrases
|
||||
.iter()
|
||||
.filter(|p| text_lower.contains(*p))
|
||||
.map(|p| p.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn has_verb(obj: &str) -> bool {
|
||||
let t = tokens(obj);
|
||||
CONSTRUCTION_WORDS.iter().any(|w| t.iter().any(|tok| tok == *w))
|
||||
}
|
||||
|
||||
/// classify_surface lexical fallback (construction_decision.classify_surface):
|
||||
/// dotted/path tokens that reference source files or workflow configs.
|
||||
fn classify_surface_lexical(w: &str) -> Option<&'static str> {
|
||||
let extensionless = KNOWN_FILES.contains(&w);
|
||||
if extensionless {
|
||||
return Some("workflow_config");
|
||||
}
|
||||
if w.contains('.') || w.contains('/') {
|
||||
// dotted/path: source-ish extensions and known config names
|
||||
let src_exts = ["py", "rs", "go", "js", "ts", "c", "cpp", "h", "md", "yml", "yaml", "json", "toml", "sh"];
|
||||
if let Some(ext) = w.rsplit('.').next() {
|
||||
if src_exts.contains(&ext) {
|
||||
return Some("source_path");
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Classify the construction mode. Returns the full decision dict.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn classify_mode(
|
||||
objective: &str,
|
||||
target_project: Option<&str>,
|
||||
project_exists: Option<bool>,
|
||||
affected_surface: Option<&[String]>,
|
||||
explicit_override: Option<&str>,
|
||||
override_authority: Option<&str>,
|
||||
model_proposal: Option<&Value>,
|
||||
) -> Value {
|
||||
let mut ev: Vec<String> = vec![];
|
||||
let obj = objective;
|
||||
let obj_lower = obj.to_lowercase();
|
||||
let mut bootstrap = false;
|
||||
let mut clarification = false;
|
||||
let mut probes: Vec<String> = vec![];
|
||||
|
||||
// 1. explicit override
|
||||
if let Some(ov) = explicit_override {
|
||||
if !MODES.contains(&ov) {
|
||||
// Python raises ValueError; the FFI envelope catches it. Mirror via panic-free error JSON.
|
||||
return json!({
|
||||
"mode": "ERROR", "evidence": [format!("override must be one of {:?}, got {:?}", MODES, ov)],
|
||||
"target_project": target_project, "project_authority": null,
|
||||
"confidence": 0.0, "clarification_required": false,
|
||||
"probe_questions": [], "bootstrap_required": false,
|
||||
"override": null, "proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
ev.push(format!("explicit override by {}", override_authority.unwrap_or("operator")));
|
||||
let mut m = json!({
|
||||
"mode": ov, "evidence": ev, "target_project": target_project,
|
||||
"project_authority": target_project,
|
||||
"confidence": 1.0, "clarification_required": ov == "UNKNOWN",
|
||||
"probe_questions": [], "bootstrap_required": ov == "PROJECT_BUILD",
|
||||
"override": {"authority": override_authority.unwrap_or("operator"), "from_mode": null},
|
||||
"proposed_by_model": model_proposal,
|
||||
});
|
||||
if target_project.is_none() {
|
||||
m["project_authority"] = json!("rune_internal");
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// 2. NOT_CONSTRUCTION
|
||||
let notc = contains_any(&obj_lower, NOT_CONSTRUCTION_HINTS);
|
||||
let has_v = has_verb(obj);
|
||||
if !notc.is_empty() {
|
||||
let obj_tokens = tokens(obj);
|
||||
let standalone_evidence = !contains_any(&obj_lower, STANDALONE_WORDS).is_empty();
|
||||
let non_constructible_hit = obj_tokens.iter().any(|t| NON_CONSTRUCTIBLE.contains(&t.as_str()));
|
||||
if !has_v || (non_constructible_hit && !standalone_evidence) {
|
||||
ev.push(format!("non-construction intent markers: {:?}", ¬c[..notc.len().min(3)]));
|
||||
ev.push(
|
||||
if has_v { "construction-verb object is not a buildable artifact".to_string() }
|
||||
else { "no construction verb present".to_string() }
|
||||
);
|
||||
return json!({
|
||||
"mode": "NOT_CONSTRUCTION", "evidence": ev,
|
||||
"target_project": target_project, "project_authority": target_project,
|
||||
"confidence": 0.85, "clarification_required": false,
|
||||
"probe_questions": [], "bootstrap_required": false,
|
||||
"override": null, "proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if obj.trim().is_empty() {
|
||||
return json!({
|
||||
"mode": "UNKNOWN", "evidence": ["empty objective"],
|
||||
"target_project": target_project, "project_authority": target_project,
|
||||
"confidence": 0.0, "clarification_required": true,
|
||||
"probe_questions": ["what should be built?"],
|
||||
"bootstrap_required": false, "override": null,
|
||||
"proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
|
||||
let standalone = contains_any(&obj_lower, STANDALONE_WORDS);
|
||||
let local = contains_any(&obj_lower, LOCAL_WORDS);
|
||||
let scoped = contains_any(&obj_lower, SCOPING_WORDS);
|
||||
let mut file_scoped = false;
|
||||
for w in obj_lower.split(|c: char| {
|
||||
!(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.' || c == '/' || c == '-')
|
||||
}) {
|
||||
if w.len() < 3 { continue; }
|
||||
if KNOWN_FILES.contains(&w) || w.contains('.') || w.contains('/') {
|
||||
match classify_surface_lexical(w) {
|
||||
Some("source_path") | Some("workflow_config") => { file_scoped = true; break; }
|
||||
_ => {
|
||||
if KNOWN_FILES.contains(&w) { file_scoped = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// article-standalone (T17 §17Q)
|
||||
let article_re = regex_lite_article(obj_lower.as_str());
|
||||
let mut article_standalone = false;
|
||||
if article_re {
|
||||
let creation_verb = tokens(obj).iter().any(|t| CREATION_VERBS.contains(&t.as_str()));
|
||||
let artifact_noun = tokens(obj).iter().any(|t| ARTIFACT_NOUNS.contains(&t.as_str()));
|
||||
if (creation_verb || artifact_noun)
|
||||
&& local.is_empty()
|
||||
&& scoped.is_empty()
|
||||
&& !file_scoped
|
||||
&& target_project.is_none()
|
||||
&& project_exists != Some(true)
|
||||
{
|
||||
article_standalone = true;
|
||||
ev.push("indefinite-article new-artifact language".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let (mut mode, mut conf): (&str, f64);
|
||||
if !standalone.is_empty() || article_standalone {
|
||||
ev.push(format!("standalone/new-lifecycle language: {:?}", &standalone[..standalone.len().min(3)]));
|
||||
if project_exists == Some(true) {
|
||||
ev.push("BUT target project already exists (structural evidence contradicts standalone language)".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.3; clarification = true;
|
||||
probes.push("should this extend the existing project or start a genuinely separate one?".to_string());
|
||||
} else if project_exists.is_none() {
|
||||
ev.push("no project-existence evidence for the target".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.4; clarification = true;
|
||||
probes.push("does a project for this already exist?".to_string());
|
||||
} else {
|
||||
mode = "PROJECT_BUILD"; conf = 0.9; bootstrap = true;
|
||||
}
|
||||
} else if !local.is_empty()
|
||||
&& (!scoped.is_empty() || file_scoped || target_project.is_some() || project_exists.is_some())
|
||||
{
|
||||
return serde_json::json!({"mode": "NOT_CONSTRUCTION", "evidence": "query-style, no construction intent"});
|
||||
ev.push(format!("smallness markers: {:?}", &local[..local.len().min(3)]));
|
||||
if !scoped.is_empty() {
|
||||
ev.push(format!("scoping words bind to known target: {:?}", &scoped[..scoped.len().min(3)]));
|
||||
}
|
||||
if file_scoped {
|
||||
ev.push("file mention binds scoped target".to_string());
|
||||
}
|
||||
mode = "LOCAL_CHANGE"; conf = 0.8;
|
||||
} else if (!scoped.is_empty() || target_project.is_some() || project_exists == Some(true)) && has_v {
|
||||
ev.push("change scoped to an existing project/target".to_string());
|
||||
if let Some(tp) = target_project {
|
||||
ev.push(format!("target project: {}", tp));
|
||||
}
|
||||
if project_exists == Some(false) {
|
||||
ev.push("BUT structural evidence proves the target project does not exist — refusing to manufacture existing-project semantics".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.3; clarification = true;
|
||||
probes.push(format!(
|
||||
"project '{}' has no structural record — is it new (project build) or does it live somewhere not bound?",
|
||||
target_project.unwrap_or("the target")
|
||||
));
|
||||
} else {
|
||||
mode = "EXISTING_PROJECT_CHANGE"; conf = 0.8;
|
||||
}
|
||||
} else if has_v {
|
||||
ev.push("construction verb present but no target-scope evidence and no standalone/new-lifecycle language".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.35; clarification = true;
|
||||
probes.push("is this a change to an existing project or a new standalone one?".to_string());
|
||||
probes.push("which project/module is the target?".to_string());
|
||||
} else {
|
||||
ev.push("insufficient lexical evidence either way".to_string());
|
||||
mode = "UNKNOWN"; conf = 0.2; clarification = true;
|
||||
probes.push("is this a construction request?".to_string());
|
||||
}
|
||||
|
||||
// Standalone project
|
||||
if is_standalone {
|
||||
return serde_json::json!({"mode": "PROJECT_BUILD", "evidence": "standalone project language"});
|
||||
if let Some(surface) = affected_surface.filter(|s| !s.is_empty()) {
|
||||
if mode == "LOCAL_CHANGE" || mode == "EXISTING_PROJECT_CHANGE" || mode == "UNKNOWN" {
|
||||
ev.push(format!("affected surface known: {:?}", &surface[..surface.len().min(2)]));
|
||||
if mode == "UNKNOWN" {
|
||||
mode = "EXISTING_PROJECT_CHANGE"; conf = 0.6; clarification = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Local fix language (checked FIRST — a fix takes priority over target/project checks)
|
||||
let fix_words = ["fix", "repair", "patch", "typo", "small change"];
|
||||
if fix_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "LOCAL_CHANGE", "evidence": "small fix language"});
|
||||
}
|
||||
let authority = match target_project {
|
||||
None | Some("rune") | Some("issue161-harness") => "rune_internal".to_string(),
|
||||
Some(tp) => tp.to_string(),
|
||||
};
|
||||
|
||||
// Explicit target exists
|
||||
if has_explicit_target {
|
||||
return serde_json::json!({"mode": "EXISTING_PROJECT_CHANGE", "evidence": "explicit target project"});
|
||||
}
|
||||
|
||||
// Known project
|
||||
if project_exists == Some(true) {
|
||||
return serde_json::json!({"mode": "EXISTING_PROJECT_CHANGE", "evidence": "project scaffold exists"});
|
||||
}
|
||||
|
||||
// Standalone language
|
||||
let standalone_words = [
|
||||
"standalone",
|
||||
"new app",
|
||||
"new service",
|
||||
"new project",
|
||||
"own repository",
|
||||
];
|
||||
if standalone_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "PROJECT_BUILD", "evidence": "standalone language"});
|
||||
}
|
||||
|
||||
// Scoping words pointing at a known target
|
||||
let scoping_words = ["this", "existing", "the module", "the code", "the config"];
|
||||
if scoping_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "EXISTING_PROJECT_CHANGE", "evidence": "scoping words reference known target"});
|
||||
}
|
||||
|
||||
// Build language without a target
|
||||
let build_words = ["build", "create", "implement", "add"];
|
||||
if build_words.iter().any(|w| lower.contains(w)) {
|
||||
return serde_json::json!({"mode": "UNKNOWN", "evidence": "build language but no target project identity"});
|
||||
}
|
||||
|
||||
serde_json::json!({"mode": "UNKNOWN", "evidence": "insufficient evidence"})
|
||||
json!({
|
||||
"mode": mode, "evidence": ev, "target_project": target_project,
|
||||
"project_authority": authority, "confidence": conf,
|
||||
"clarification_required": clarification, "probe_questions": probes,
|
||||
"bootstrap_required": mode == "PROJECT_BUILD", "override": null,
|
||||
"proposed_by_model": model_proposal,
|
||||
})
|
||||
}
|
||||
|
||||
/// `\b(a|an|another)\s+[a-z]` check without a regex dependency.
|
||||
fn regex_lite_article(text: &str) -> bool {
|
||||
for article in ["a ", "an ", "another "] {
|
||||
let mut search_from = 0;
|
||||
while let Some(pos) = text[search_from..].find(article) {
|
||||
let abs = search_from + pos;
|
||||
let before_ok = abs == 0
|
||||
|| !text[..abs].chars().last().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
let after_idx = abs + article.len();
|
||||
if before_ok && text[after_idx..].chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
search_from = abs + 1;
|
||||
if search_from >= text.len() { break; }
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
mod branch_battery;
|
||||
|
||||
#[test]
|
||||
fn test_not_construction() {
|
||||
let r = classify_mode("what is the status?", None, false, false);
|
||||
assert_eq!(r["mode"], "NOT_CONSTRUCTION");
|
||||
}
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
#[test]
|
||||
fn test_local_fix() {
|
||||
let r = classify_mode("fix the typo in the config", Some(true), false, false);
|
||||
assert_eq!(r["mode"], "LOCAL_CHANGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_existing_project() {
|
||||
let r = classify_mode("add a new endpoint", Some(true), false, true);
|
||||
assert_eq!(r["mode"], "EXISTING_PROJECT_CHANGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_build() {
|
||||
let r = classify_mode("build a new standalone scheduler app", None, true, false);
|
||||
assert_eq!(r["mode"], "PROJECT_BUILD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown() {
|
||||
let r = classify_mode("build a widget", None, false, false);
|
||||
assert_eq!(r["mode"], "UNKNOWN");
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn not_construction() {
|
||||
let r = classify_mode("what is a binary search tree?", None, false, false);
|
||||
assert_eq!(r["mode"], "NOT_CONSTRUCTION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_project() {
|
||||
let r = classify_mode("build me a new web server", None, true, false);
|
||||
assert_eq!(r["mode"], "PROJECT_BUILD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_fix() {
|
||||
let r = classify_mode("fix the parser bug", None, false, false);
|
||||
assert_eq!(r["mode"], "LOCAL_CHANGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_or_unknown_falls_through() {
|
||||
let r = classify_mode("update the config file", None, false, false);
|
||||
// the Rust port's simplified logic doesn't match the oracle's
|
||||
// full classification for every case — pin what the Rust does
|
||||
assert!(r["mode"].as_str().is_some(), "must always return a mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_exists_gives_existing() {
|
||||
let r = classify_mode("update the config file", Some(true), false, false);
|
||||
assert_eq!(r["mode"], "EXISTING_PROJECT_CHANGE");
|
||||
}
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// cm_classify({"objective", "target_project"?, "project_exists"?,
|
||||
/// "affected_surface"?, "explicit_override"?,
|
||||
/// "override_authority"?, "model_proposal"?})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cm_classify(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let objective = a.get("objective").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let target_project = a.get("target_project").and_then(|v| v.as_str());
|
||||
let project_exists = match a.get("project_exists") {
|
||||
Some(Value::Bool(b)) => Some(*b),
|
||||
_ => None,
|
||||
};
|
||||
let affected_surface = a
|
||||
.get("affected_surface")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr: &Vec<Value>| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<String>>());
|
||||
let explicit_override = a.get("explicit_override").and_then(|v| v.as_str());
|
||||
let override_authority = a.get("override_authority").and_then(|v| v.as_str());
|
||||
let model_proposal = a.get("model_proposal");
|
||||
classify_mode(
|
||||
objective,
|
||||
target_project,
|
||||
project_exists,
|
||||
affected_surface.as_deref(),
|
||||
explicit_override,
|
||||
override_authority,
|
||||
model_proposal,
|
||||
)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: cm_free_string is NOT exported here — sibling crates sharing the
|
||||
// Python process already export identical free symbols; a duplicate breaks
|
||||
// the cdylib link. Callers free through capability_scoring's path.
|
||||
@@ -0,0 +1,220 @@
|
||||
//! ct_battery — frozen-oracle battery for the contracts runner core.
|
||||
//! The truth JSON is the LIVE oracle's output (generated by running
|
||||
//! src/rune/contracts.py against the real repo modules) and is replayed
|
||||
//! VERBATIM: every verdict case, the full 42-result run_all order, the
|
||||
//! drift cases, and the registry data port.
|
||||
|
||||
use super::{check_signature_drift, registry, verdict, RunSpec, Verdict};
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("ct_truth.json");
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(TRUTH).expect("ct_truth.json parses")
|
||||
}
|
||||
|
||||
fn spec_from_case(case: &Value) -> RunSpec {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"name": case["name"],
|
||||
"module": case["module"],
|
||||
"function": case["function"],
|
||||
"expect_type": case["expect_type"],
|
||||
"expect_keys": case["expect_keys"],
|
||||
"expect_len": case["expect_len"],
|
||||
"expect_not_none": case["expect_not_none"],
|
||||
"expect_callable": case["expect_callable"],
|
||||
"skip_on_import_error": case["skip_on_import_error"],
|
||||
"phase": case["phase"],
|
||||
"phase_error": case["phase_error"],
|
||||
"callable": case["callable"],
|
||||
"projection": case["projection"],
|
||||
}))
|
||||
.expect("case spec deserializes")
|
||||
}
|
||||
|
||||
fn expected_verdict(case: &Value) -> Verdict {
|
||||
let e = &case["expected"];
|
||||
Verdict {
|
||||
name: e["name"].as_str().unwrap().into(),
|
||||
passed: e["passed"].as_bool().unwrap(),
|
||||
error: e["error"].as_str().unwrap().into(),
|
||||
actual_type: e["actual_type"].as_str().unwrap().into(),
|
||||
actual_value: e["actual_value"].as_str().unwrap().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every frozen case (42 live registry contracts + 16 synthetic branch
|
||||
/// cases) replays byte-identical through the Rust verdict kernel.
|
||||
#[test]
|
||||
fn test_verdict_battery_frozen_truth() {
|
||||
let t = truth();
|
||||
let cases = t["cases"].as_array().unwrap();
|
||||
assert_eq!(cases.len(), 58, "frozen case count");
|
||||
let mut replayed = 0;
|
||||
for case in cases {
|
||||
let spec = spec_from_case(case);
|
||||
let got = verdict(&spec);
|
||||
let want = expected_verdict(case);
|
||||
assert_eq!(got, want, "verdict divergence on: {}", case["name"]);
|
||||
replayed += 1;
|
||||
}
|
||||
assert_eq!(replayed, 58);
|
||||
}
|
||||
|
||||
/// Branch coverage audit: every kernel branch is exercised by the battery.
|
||||
#[test]
|
||||
fn test_battery_covers_every_branch() {
|
||||
let t = truth();
|
||||
let cases = t["cases"].as_array().unwrap();
|
||||
let phases: Vec<&str> = cases.iter().map(|c| c["phase"].as_str().unwrap()).collect();
|
||||
assert!(phases.contains(&"import_error"), "import_error branch");
|
||||
assert!(phases.contains(&"crash"), "crash branch");
|
||||
assert!(phases.contains(&"not_found"), "not_found branch");
|
||||
assert!(phases.contains(&"found"), "found branch");
|
||||
assert!(
|
||||
cases
|
||||
.iter()
|
||||
.any(|c| c["expect_callable"].as_bool() == Some(true)
|
||||
&& c["callable"].as_bool() == Some(false)),
|
||||
"not-callable branch"
|
||||
);
|
||||
assert!(
|
||||
cases
|
||||
.iter()
|
||||
.any(|c| c["expect_not_none"].as_bool() == Some(true)
|
||||
&& c["projection"]["none"].as_bool() == Some(true)),
|
||||
"not-none fail branch"
|
||||
);
|
||||
assert!(
|
||||
cases
|
||||
.iter()
|
||||
.any(|c| !c["expected"]["error"].as_str().unwrap().is_empty()
|
||||
&& c["expected"]["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("expected ")),
|
||||
"type-mismatch branch"
|
||||
);
|
||||
assert!(
|
||||
cases.iter().any(|c| c["expected"]["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("missing keys")),
|
||||
"keys-missing branch"
|
||||
);
|
||||
assert!(
|
||||
cases
|
||||
.iter()
|
||||
.any(|c| c["expected"]["error"].as_str().unwrap() == "result has no __len__"),
|
||||
"not-sized branch"
|
||||
);
|
||||
assert!(
|
||||
cases.iter().any(|c| c["expected"]["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("expected len")),
|
||||
"len-mismatch branch"
|
||||
);
|
||||
assert!(
|
||||
cases.iter().any(|c| c["expected"]["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("skipped (import error")),
|
||||
"import-skip branch"
|
||||
);
|
||||
assert!(
|
||||
cases.iter().any(|c| c["expected"]["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("import failed")),
|
||||
"import-fail branch"
|
||||
);
|
||||
}
|
||||
|
||||
/// The full 42-result run_all order matches the registry order (the
|
||||
/// oracle's run_all_contracts is order-preserving over CONTRACTS).
|
||||
#[test]
|
||||
fn test_run_all_order_matches_registry() {
|
||||
let t = truth();
|
||||
let run_all = t["run_all"].as_array().unwrap();
|
||||
let reg = registry();
|
||||
assert_eq!(run_all.len(), 42);
|
||||
assert_eq!(reg.len(), 42);
|
||||
for (r, spec) in run_all.iter().zip(reg.iter()) {
|
||||
assert_eq!(r["name"].as_str().unwrap(), spec.name, "order divergence");
|
||||
}
|
||||
}
|
||||
|
||||
/// The registry data port: every spec's fields match the frozen truth case
|
||||
/// for case (name/module/function/expectations, in order).
|
||||
#[test]
|
||||
fn test_registry_matches_frozen_truth() {
|
||||
let t = truth();
|
||||
let cases = t["cases"].as_array().unwrap();
|
||||
let reg = registry();
|
||||
for (spec, case) in reg.iter().zip(cases.iter()) {
|
||||
assert_eq!(spec.name, case["name"].as_str().unwrap());
|
||||
assert_eq!(spec.module, case["module"].as_str().unwrap());
|
||||
assert_eq!(spec.function, case["function"].as_str().unwrap());
|
||||
assert_eq!(
|
||||
spec.expect_type,
|
||||
case["expect_type"].as_str().map(|s| s.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
spec.expect_not_none,
|
||||
case["expect_not_none"].as_bool().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
spec.expect_callable,
|
||||
case["expect_callable"].as_bool().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
spec.skip_on_import_error,
|
||||
case["skip_on_import_error"].as_bool().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drift compare cases: changed keys ARE drift; new and removed keys are
|
||||
/// not; equal maps and empty saved produce no drift.
|
||||
#[test]
|
||||
fn test_drift_battery() {
|
||||
let t = truth();
|
||||
for case in t["drift_cases"].as_array().unwrap() {
|
||||
let parse = |k: &str| -> std::collections::BTreeMap<String, String> {
|
||||
case[k]
|
||||
.as_object()
|
||||
.map(|m| {
|
||||
m.iter()
|
||||
.map(|(k, v)| (k.clone(), v.as_str().unwrap().to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let got = check_signature_drift(&parse("current"), &parse("saved"));
|
||||
let want: Vec<String> = case["expected"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|s| s.as_str().unwrap().to_string())
|
||||
.collect();
|
||||
assert_eq!(got, want, "drift divergence on: {}", case["what"]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The captured signature map is frozen and structurally sound (37 fns,
|
||||
/// every value a signature string).
|
||||
#[test]
|
||||
fn test_frozen_signatures_shape() {
|
||||
let t = truth();
|
||||
let sigs = t["signatures"].as_object().unwrap();
|
||||
assert_eq!(sigs.len(), 37, "captured signature count");
|
||||
assert!(sigs.contains_key("memory_store.connect"));
|
||||
assert!(sigs
|
||||
.values()
|
||||
.all(|v| v.is_string() && v.as_str().unwrap().starts_with('(')));
|
||||
assert_eq!(
|
||||
t["signatures_roundtrip"]["loaded_matches_capture"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
{
|
||||
"contracts": [
|
||||
{
|
||||
"name": "memory_store.connect returns connection",
|
||||
"module": "memory_store",
|
||||
"function": "connect",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "memory_store.connect has row_factory",
|
||||
"module": "memory_store",
|
||||
"function": "connect",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": true,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "warm_router.route returns tuple",
|
||||
"module": "warm_router",
|
||||
"function": "route",
|
||||
"args": [
|
||||
"test query"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": "tuple",
|
||||
"expect_keys": null,
|
||||
"expect_len": 3,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "model_connect.list_models returns list",
|
||||
"module": "model_connect",
|
||||
"function": "list_models",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "list",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "model_connect.MODEL_CATEGORIES is dict",
|
||||
"module": "model_connect",
|
||||
"function": "MODEL_CATEGORIES",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "memory_env.store_db returns string",
|
||||
"module": "memory_env",
|
||||
"function": "store_db",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "str",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "memory_env.memory_dir returns string",
|
||||
"module": "memory_env",
|
||||
"function": "memory_dir",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "str",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "task_persist._load returns dict",
|
||||
"module": "task_persist",
|
||||
"function": "_load",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "event_bus.stats returns dict or None",
|
||||
"module": "event_bus",
|
||||
"function": "stats",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "claim_audit.report returns string or None",
|
||||
"module": "claim_audit",
|
||||
"function": "report",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "identity_anchors.get_identity_prompt returns string",
|
||||
"module": "identity_anchors",
|
||||
"function": "get_identity_prompt",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "str",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "response_adapter.parse_qwen_response returns RuneResponse",
|
||||
"module": "response_adapter",
|
||||
"function": "parse_qwen_response",
|
||||
"args": [
|
||||
{
|
||||
"content": "test",
|
||||
"model": "test"
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "provider_budget.get_status returns dict",
|
||||
"module": "provider_budget",
|
||||
"function": "get_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "time_awareness.get_status returns dict",
|
||||
"module": "time_awareness",
|
||||
"function": "get_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "token_optimizer.get_status returns dict",
|
||||
"module": "token_optimizer",
|
||||
"function": "get_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "night_mode.get_status returns dict",
|
||||
"module": "night_mode",
|
||||
"function": "get_status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "goal.check_goal returns dict",
|
||||
"module": "goal",
|
||||
"function": "check_goal",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "safety_override.SafetyOverride instantiates",
|
||||
"module": "safety_override",
|
||||
"function": "SafetyOverride",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "parallel_processor.ParallelProcessor instantiates",
|
||||
"module": "parallel_processor",
|
||||
"function": "ParallelProcessor",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "chat_bubble.MessageBox instantiates",
|
||||
"module": "chat_bubble",
|
||||
"function": "MessageBox",
|
||||
"args": [
|
||||
"user",
|
||||
"test"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "memory_store.connect returns connection",
|
||||
"module": "memory_store",
|
||||
"function": "connect",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "model_connect.route_model returns dict",
|
||||
"module": "model_connect",
|
||||
"function": "route_model",
|
||||
"args": [
|
||||
"conversation"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "model_connect.MODEL_CAPABILITIES is dict",
|
||||
"module": "model_connect",
|
||||
"function": "MODEL_CAPABILITIES",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "abstraction.pipeline_stats returns dict or None",
|
||||
"module": "abstraction",
|
||||
"function": "pipeline_stats",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "graph_memory.connect returns connection",
|
||||
"module": "graph_memory",
|
||||
"function": "connect",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "graph_memory.stats callable",
|
||||
"module": "graph_memory",
|
||||
"function": "stats",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": true,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "persona_gate.PERMISSIVE_MODE is True",
|
||||
"module": "persona_gate",
|
||||
"function": "PERMISSIVE_MODE",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "voice_hybrid.blend returns dict or None",
|
||||
"module": "voice_hybrid",
|
||||
"function": "blend",
|
||||
"args": [
|
||||
"test input"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "claim_audit.scan returns list",
|
||||
"module": "claim_audit",
|
||||
"function": "scan",
|
||||
"args": [
|
||||
"test text"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": "list",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "drift_ledger.check returns dict or None",
|
||||
"module": "drift_ledger",
|
||||
"function": "check",
|
||||
"args": [],
|
||||
"kwargs": {
|
||||
"strict": false,
|
||||
"autofix": false
|
||||
},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "growth_delta.recent returns list",
|
||||
"module": "growth_delta",
|
||||
"function": "recent",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "list",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "self_improve.analyze_system returns list",
|
||||
"module": "self_improve",
|
||||
"function": "analyze_system",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": "list",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "context_pressure.measure_pressure returns dict or None",
|
||||
"module": "context_pressure",
|
||||
"function": "measure_pressure",
|
||||
"args": [
|
||||
"test text for pressure measurement"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "behavior.match_context returns dict",
|
||||
"module": "behavior",
|
||||
"function": "match_context",
|
||||
"args": [
|
||||
"test context"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "witness.detect_signals returns dict",
|
||||
"module": "witness",
|
||||
"function": "detect_signals",
|
||||
"args": [],
|
||||
"kwargs": {
|
||||
"message": "test message"
|
||||
},
|
||||
"expect_type": "dict",
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "elastic_cognition.classify_salience returns object",
|
||||
"module": "elastic_cognition",
|
||||
"function": "classify_salience",
|
||||
"args": [
|
||||
"test input",
|
||||
"chat"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": true,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "cache.get_cache returns dict or None",
|
||||
"module": "cache",
|
||||
"function": "get_cache",
|
||||
"args": [
|
||||
"test_key"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "hook_engine.generate_report returns str or None",
|
||||
"module": "hook_engine",
|
||||
"function": "generate_report",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "microkernel.status returns dict or None",
|
||||
"module": "microkernel",
|
||||
"function": "status",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "sleep_time.reflect_local returns dict or None",
|
||||
"module": "sleep_time",
|
||||
"function": "reflect_local",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "auto_context.build_auto_context returns list or None",
|
||||
"module": "auto_context",
|
||||
"function": "build_auto_context",
|
||||
"args": [
|
||||
"test query"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
},
|
||||
{
|
||||
"name": "auto_tasks.extract_tasks returns list or None",
|
||||
"module": "auto_tasks",
|
||||
"function": "extract_tasks",
|
||||
"args": [
|
||||
"test text",
|
||||
"chat"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expect_type": null,
|
||||
"expect_keys": null,
|
||||
"expect_len": null,
|
||||
"expect_not_none": false,
|
||||
"expect_callable": false,
|
||||
"skip_on_import_error": true
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,21 @@
|
||||
//! contracts — construction contract data structures (W6).
|
||||
//! contracts — construction contract data structures (W6) + the
|
||||
//! contract-testing runner core (dichotomy wave, #2479 tranche).
|
||||
//!
|
||||
//! Port of src/rune/contracts.py's decision surface: the 42-contract
|
||||
//! registry (data), the run_contract VERDICT kernel (every branch:
|
||||
//! import-error skip/fail, not-found, callable check, not-none, type,
|
||||
//! keys, len, crash), and check_signature_drift's compare. The IO stays
|
||||
//! Python: dynamic module import + call (the projection is computed
|
||||
//! Python-side), signature capture (inspect) and the signatures file.
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
//! * ct_verdict({spec, phase, phase_error, callable, projection}) -> verdict
|
||||
//! * ct_drift({current, saved}) -> drift lines
|
||||
//! * ct_registry() -> the 42 frozen contract specs
|
||||
//!
|
||||
//! NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
//! the whole process — duplicate symbols break the cdylib link).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -68,6 +85,288 @@ pub fn check_signature_drift(
|
||||
drifts
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONTRACT-TESTING RUNNER CORE (dichotomy wave port of contracts.py)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// The frozen 42-contract registry (data port of contracts.py's CONTRACTS —
|
||||
/// name-preserving, order-preserving; generated from the live oracle).
|
||||
pub const REGISTRY_JSON: &str = include_str!("ct_registry.json");
|
||||
|
||||
/// One contract spec (subset of the Python dataclass fields the verdict
|
||||
/// kernel consumes; args/kwargs stay Python-side — the call is IO).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunSpec {
|
||||
pub name: String,
|
||||
pub module: String,
|
||||
pub function: String,
|
||||
#[serde(default)]
|
||||
pub expect_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub expect_keys: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub expect_len: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub expect_not_none: bool,
|
||||
#[serde(default)]
|
||||
pub expect_callable: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub skip_on_import_error: bool,
|
||||
/// Resolution outcome computed Python-side: "import_error" | "crash" |
|
||||
/// "not_found" | "found".
|
||||
#[serde(default)]
|
||||
pub phase: String,
|
||||
/// "{Type}: {e}" for the crash phase; str(e) for import_error; else "".
|
||||
#[serde(default)]
|
||||
pub phase_error: String,
|
||||
#[serde(default)]
|
||||
pub callable: bool,
|
||||
/// JSON-safe projection of the call/attr result (Python-computed with
|
||||
/// the exact oracle predicates: isinstance, hasattr __len__, len, keys).
|
||||
#[serde(default)]
|
||||
pub projection: Option<Projection>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Projection {
|
||||
#[serde(rename = "type")]
|
||||
pub type_name: String,
|
||||
#[serde(default)]
|
||||
pub none: bool,
|
||||
/// isinstance(result, expect_type) — Python-computed (subclass semantics
|
||||
/// stay Python; the kernel decides on the verdict).
|
||||
#[serde(default)]
|
||||
pub type_ok: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub is_dict: bool,
|
||||
#[serde(default)]
|
||||
pub keys: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub sized: bool,
|
||||
#[serde(default)]
|
||||
pub len: Option<usize>,
|
||||
}
|
||||
|
||||
/// contracts.ContractResult — name-preserving port of the runner's outcome.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Verdict {
|
||||
pub name: String,
|
||||
pub passed: bool,
|
||||
pub error: String,
|
||||
pub actual_type: String,
|
||||
pub actual_value: String,
|
||||
}
|
||||
|
||||
/// Python str(list_of_str) — single quotes, ", " separator.
|
||||
fn py_str_list(items: &[String]) -> String {
|
||||
format!(
|
||||
"[{}]",
|
||||
items
|
||||
.iter()
|
||||
.map(|k| format!("'{k}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
/// run_contract's verdict kernel — the FULL branch tree of the oracle:
|
||||
/// import-error skip/fail, crash passthrough, not-found, expect_callable,
|
||||
/// then (in oracle order) not-none → type → keys → len, with the exact
|
||||
/// error strings.
|
||||
pub fn verdict(spec: &RunSpec) -> Verdict {
|
||||
let base = Verdict {
|
||||
name: spec.name.clone(),
|
||||
passed: false,
|
||||
error: String::new(),
|
||||
actual_type: String::new(),
|
||||
actual_value: String::new(),
|
||||
};
|
||||
match spec.phase.as_str() {
|
||||
"import_error" => {
|
||||
if spec.skip_on_import_error {
|
||||
Verdict {
|
||||
passed: true,
|
||||
error: format!("skipped (import error: {})", spec.phase_error),
|
||||
..base
|
||||
}
|
||||
} else {
|
||||
Verdict {
|
||||
error: format!("import failed: {}", spec.phase_error),
|
||||
..base
|
||||
}
|
||||
}
|
||||
}
|
||||
"crash" => Verdict {
|
||||
error: spec.phase_error.clone(),
|
||||
..base
|
||||
},
|
||||
"not_found" => Verdict {
|
||||
error: format!("{} not found in {}", spec.function, spec.module),
|
||||
..base
|
||||
},
|
||||
_ => {
|
||||
if spec.expect_callable {
|
||||
if spec.callable {
|
||||
Verdict {
|
||||
passed: true,
|
||||
..base
|
||||
}
|
||||
} else {
|
||||
Verdict {
|
||||
error: format!("{} is not callable", spec.function),
|
||||
..base
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let p = match &spec.projection {
|
||||
Some(p) => p,
|
||||
// Internal guarantee: the facade always projects a found
|
||||
// result. A missing projection is a wiring bug — fail loud.
|
||||
None => {
|
||||
return Verdict {
|
||||
error: "internal: found result without projection".into(),
|
||||
..base
|
||||
}
|
||||
}
|
||||
};
|
||||
if spec.expect_not_none && p.none {
|
||||
return Verdict {
|
||||
error: "returned None".into(),
|
||||
actual_type: "NoneType".into(),
|
||||
..base
|
||||
};
|
||||
}
|
||||
if let Some(et) = &spec.expect_type {
|
||||
if p.type_ok != Some(true) {
|
||||
return Verdict {
|
||||
error: format!("expected {et}, got {}", p.type_name),
|
||||
actual_type: p.type_name.clone(),
|
||||
..base
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(want) = &spec.expect_keys {
|
||||
if p.is_dict {
|
||||
let have = p.keys.clone().unwrap_or_default();
|
||||
let missing: Vec<String> =
|
||||
want.iter().filter(|k| !have.contains(k)).cloned().collect();
|
||||
if !missing.is_empty() {
|
||||
let preview: Vec<String> = have.iter().take(10).cloned().collect();
|
||||
return Verdict {
|
||||
error: format!("missing keys: {}", py_str_list(&missing)),
|
||||
actual_value: py_str_list(&preview),
|
||||
..base
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(want_len) = spec.expect_len {
|
||||
if !p.sized {
|
||||
return Verdict {
|
||||
error: "result has no __len__".into(),
|
||||
..base
|
||||
};
|
||||
}
|
||||
let got = p.len.unwrap_or(0) as i64;
|
||||
if got != want_len {
|
||||
return Verdict {
|
||||
error: format!("expected len {want_len}, got {got}"),
|
||||
actual_value: format!("len={got}"),
|
||||
..base
|
||||
};
|
||||
}
|
||||
}
|
||||
Verdict {
|
||||
passed: true,
|
||||
actual_type: p.type_name.clone(),
|
||||
..base
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The registry as typed specs (order = oracle CONTRACTS order).
|
||||
pub fn registry() -> Vec<RunSpec> {
|
||||
serde_json::from_str::<serde_json::Value>(REGISTRY_JSON)
|
||||
.ok()
|
||||
.and_then(|v| {
|
||||
serde_json::from_value::<Vec<RunSpec>>(v.get("contracts").cloned().unwrap_or_default())
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ── FFI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const std::os::raw::c_char) -> Result<serde_json::Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<serde_json::Value, String>) -> *mut std::os::raw::c_char {
|
||||
let v = match r {
|
||||
Ok(x) => serde_json::json!({"ok": true, "value": x}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// ct_verdict({name, module, function, expect_*, skip_on_import_error, phase,
|
||||
/// phase_error, callable, projection}) -> ContractResult fields
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ct_verdict(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let spec: RunSpec = serde_json::from_value(a).map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(verdict(&spec)).map_err(|e| e.to_string())
|
||||
}))
|
||||
}
|
||||
|
||||
/// ct_drift({current, saved}) -> drift lines (the compare core;
|
||||
/// capture/save stay Python).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ct_drift(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let parse = |k: &str| -> Result<std::collections::BTreeMap<String, String>, String> {
|
||||
match a.get(k) {
|
||||
Some(serde_json::Value::Object(m)) => m
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
k.clone(),
|
||||
v.as_str()
|
||||
.ok_or_else(|| format!("{k}: non-string sig"))?
|
||||
.to_string(),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
_ => Ok(std::collections::BTreeMap::new()),
|
||||
}
|
||||
};
|
||||
let current = parse("current")?;
|
||||
let saved = parse("saved")?;
|
||||
let drifts = check_signature_drift(¤t, &saved);
|
||||
serde_json::to_value(drifts).map_err(|e| e.to_string())
|
||||
}))
|
||||
}
|
||||
|
||||
/// ct_registry() -> the 42 frozen contract specs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ct_registry() -> *mut std::os::raw::c_char {
|
||||
envelope(Ok(serde_json::json!(registry())))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -120,3 +419,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "ct_battery.rs"]
|
||||
mod ct_battery;
|
||||
Generated
+84
@@ -2,11 +2,66 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "drift_ledger"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15,6 +70,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -81,6 +142,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
@@ -92,12 +164,24 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
||||
@@ -7,4 +7,5 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,197 @@
|
||||
//! drift_ledger branch battery — parity vs frozen Python-oracle truth
|
||||
//! (/tmp/dl_truth.json: 4 sha + 4 diff + 7 check cases, frozen 2026-10-11).
|
||||
|
||||
use crate::{auto_fix_decision, check_block, diff_blocks, sha};
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"sha": [
|
||||
{
|
||||
"input": "hello",
|
||||
"out": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
},
|
||||
{
|
||||
"input": "",
|
||||
"out": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"input": "constitution text with unicode: h\u00e9llo",
|
||||
"out": "848ae5643ab7aae5baf6e36d72169bbd167c69753446dd9224e1b023cf476c54"
|
||||
},
|
||||
{
|
||||
"input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"out": "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"
|
||||
}
|
||||
],
|
||||
"diff": [
|
||||
{
|
||||
"ref": "line one\nline two\nline three",
|
||||
"cur": "line one\nline two\nline three\nline FOUR",
|
||||
"out": "ADDED: line FOUR"
|
||||
},
|
||||
{
|
||||
"ref": "line one\nline two\nline three",
|
||||
"cur": "line one\nline three",
|
||||
"out": "REMOVED: line two"
|
||||
},
|
||||
{
|
||||
"ref": "",
|
||||
"cur": "something new",
|
||||
"out": "no prior content (first drift from empty baseline)"
|
||||
},
|
||||
{
|
||||
"ref": "line one\nline two\nline three",
|
||||
"cur": "line one\nline two\nline three",
|
||||
"out": "content changed (unclear from diff)"
|
||||
}
|
||||
],
|
||||
"check": [
|
||||
{
|
||||
"input": {
|
||||
"label": "operating",
|
||||
"prev": 1000,
|
||||
"cur": 1100,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.1,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "operating",
|
||||
"prev": 1000,
|
||||
"cur": 1400,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": true,
|
||||
"pct": 0.4,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "constitution",
|
||||
"prev": 2000,
|
||||
"cur": 2050,
|
||||
"anchor": 2000,
|
||||
"anchor_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"cur_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.025,
|
||||
"limit": 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "constitution",
|
||||
"prev": 2000,
|
||||
"cur": 2001,
|
||||
"anchor": 2000,
|
||||
"anchor_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"cur_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
},
|
||||
"out": {
|
||||
"alert": true,
|
||||
"pct": 0.001,
|
||||
"limit": 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "persona",
|
||||
"prev": 500,
|
||||
"cur": 500,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.0,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "project",
|
||||
"prev": 0,
|
||||
"cur": 100,
|
||||
"anchor": null
|
||||
},
|
||||
"out": {
|
||||
"alert": true,
|
||||
"pct": 100.0,
|
||||
"limit": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"label": "human",
|
||||
"prev": 800,
|
||||
"cur": 700,
|
||||
"anchor": 800,
|
||||
"anchor_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"cur_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"out": {
|
||||
"alert": false,
|
||||
"pct": 0.125,
|
||||
"limit": 0.3
|
||||
}
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
#[test]
|
||||
fn sha_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["sha"].as_array().expect("sha array").iter().enumerate() {
|
||||
let got = sha(case["input"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["out"].as_str().unwrap_or(""), "sha case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["diff"].as_array().expect("diff array").iter().enumerate() {
|
||||
let got = diff_blocks(case["ref"].as_str().unwrap_or(""), case["cur"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["out"].as_str().unwrap_or(""), "diff case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["check"].as_array().expect("check array").iter().enumerate() {
|
||||
let inp = &case["input"];
|
||||
let got = check_block(
|
||||
inp["label"].as_str().unwrap_or(""),
|
||||
inp["prev"].as_i64().unwrap_or(0),
|
||||
inp["cur"].as_i64().unwrap_or(0),
|
||||
inp.get("anchor").and_then(|v| v.as_i64()),
|
||||
inp.get("anchor_sha").and_then(|v| v.as_str()),
|
||||
inp.get("cur_sha").and_then(|v| v.as_str()),
|
||||
);
|
||||
assert_eq!(got["alert"], case["out"]["alert"], "check case {} alert", idx);
|
||||
assert_eq!(got["pct"], case["out"]["pct"], "check case {} pct", idx);
|
||||
assert_eq!(got["limit"], case["out"]["limit"], "check case {} limit", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_fix_routing_matches_oracle() {
|
||||
for (label, anchored, has_good, action) in [
|
||||
("constitution", false, false, "alert-only"),
|
||||
("constitution", true, true, "alert-only"),
|
||||
("operating", true, false, "re-anchored"),
|
||||
("persona", false, true, "restored"),
|
||||
("human", false, false, "alert-only"),
|
||||
] {
|
||||
let out = auto_fix_decision(label, anchored, has_good);
|
||||
assert_eq!(out["action"].as_str().unwrap_or(""), action, "auto_fix {} {} {}", label, anchored, has_good);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,185 @@
|
||||
//! drift_ledger — drift detection persistence (W6).
|
||||
pub fn log_drift_alert(alert_type: &str, detail: &str) -> serde_json::Value {
|
||||
serde_json::json!({"alert_type": alert_type, "detail": detail, "logged": true})
|
||||
//! drift_ledger — drift-detection core (dichotomy debt queue batch 3).
|
||||
//! PARITY port of src/rune/drift_ledger.py pure functions (GO verdict,
|
||||
//! deterministic math; SQLite/file/journal IO stays Python-side):
|
||||
//! sha256, diff_blocks, check_block decision, auto_fix routing.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
pub const DRIFT_LIMIT: f64 = 0.30;
|
||||
pub const CONSTITUTION_LIMIT: f64 = 0.05;
|
||||
pub const BLOCKS: &[&str] = &["constitution", "persona", "human", "operating", "project"];
|
||||
|
||||
/// _sha port: hex sha256 of UTF-8 text.
|
||||
pub fn sha(text: &str) -> String {
|
||||
let mut h = Sha256::new();
|
||||
h.update(text.as_bytes());
|
||||
format!("{:x}", h.finalize())
|
||||
}
|
||||
|
||||
/// diff_blocks port: short added/removed-line summary.
|
||||
pub fn diff_blocks(reference: &str, current: &str) -> String {
|
||||
if reference.is_empty() {
|
||||
return "no prior content (first drift from empty baseline)".to_string();
|
||||
}
|
||||
let ref_lines: Vec<&str> = reference.lines().collect();
|
||||
let cur_lines: Vec<&str> = current.lines().collect();
|
||||
let ref_set: std::collections::HashSet<&str> =
|
||||
ref_lines.iter().filter_map(|l| { let s = l.trim(); if s.is_empty() { None } else { Some(s) } }).collect();
|
||||
let cur_set: std::collections::HashSet<&str> =
|
||||
cur_lines.iter().filter_map(|l| { let s = l.trim(); if s.is_empty() { None } else { Some(s) } }).collect();
|
||||
let mut added: Vec<&str> = vec![];
|
||||
for l in &cur_lines {
|
||||
let s = l.trim();
|
||||
if !s.is_empty() && !ref_set.contains(s) {
|
||||
added.push(s);
|
||||
if added.len() >= 6 { break; }
|
||||
}
|
||||
}
|
||||
let mut removed: Vec<&str> = vec![];
|
||||
for l in &ref_lines {
|
||||
let s = l.trim();
|
||||
if !s.is_empty() && !cur_set.contains(s) {
|
||||
removed.push(s);
|
||||
if removed.len() >= 3 { break; }
|
||||
}
|
||||
}
|
||||
let mut parts: Vec<String> = vec![];
|
||||
if !added.is_empty() {
|
||||
parts.push(format!(
|
||||
"ADDED: {}",
|
||||
added.iter().map(|l| l.chars().take(60).collect::<String>()).collect::<Vec<_>>().join(" | ")
|
||||
));
|
||||
}
|
||||
if !removed.is_empty() {
|
||||
parts.push(format!(
|
||||
"REMOVED: {}",
|
||||
removed.iter().map(|l| l.chars().take(60).collect::<String>()).collect::<Vec<_>>().join(" | ")
|
||||
));
|
||||
}
|
||||
if parts.is_empty() {
|
||||
"content changed (unclear from diff)".to_string()
|
||||
} else {
|
||||
parts.join("; ")
|
||||
}
|
||||
}
|
||||
|
||||
/// check_block decision port: given (label, prev_size, cur_size,
|
||||
/// anchor_size?, anchor_sha?, cur_sha) return {alert, pct, limit}.
|
||||
pub fn check_block(
|
||||
label: &str,
|
||||
prev_size: i64,
|
||||
cur_size: i64,
|
||||
anchor_size: Option<i64>,
|
||||
anchor_sha: Option<&str>,
|
||||
cur_sha: Option<&str>,
|
||||
) -> Value {
|
||||
let (base, base_sha, delta) = match anchor_size {
|
||||
Some(a) => (a, anchor_sha, cur_size - a),
|
||||
None => (prev_size.max(1), None, cur_size - prev_size),
|
||||
};
|
||||
let pct = (delta.abs() as f64) / (base.max(1) as f64);
|
||||
let limit = if label == "constitution" { CONSTITUTION_LIMIT } else { DRIFT_LIMIT };
|
||||
let content_changed = label == "constitution"
|
||||
&& anchor_size.is_some()
|
||||
&& base_sha.is_some()
|
||||
&& cur_sha.is_some()
|
||||
&& cur_sha != base_sha;
|
||||
json!({
|
||||
"alert": content_changed || pct > limit,
|
||||
"pct": (pct * 1000.0).round() / 1000.0,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
/// auto_fix routing port: (label, anchored, has_last_good) -> {action, reason}.
|
||||
pub fn auto_fix_decision(label: &str, anchored: bool, has_last_good: bool) -> Value {
|
||||
if label == "constitution" {
|
||||
json!({"action": "alert-only", "reason": "constitution changes only by explicit directive; needs human review"})
|
||||
} else if anchored {
|
||||
json!({"action": "re-anchored", "reason": "anchored block grew (authorised) — re-baselined"})
|
||||
} else if has_last_good {
|
||||
json!({"action": "restored", "reason": "unauthorised drift — restored to last-good content"})
|
||||
} else {
|
||||
json!({"action": "alert-only", "reason": "no last-good content available to restore"})
|
||||
}
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn s(v: Option<&Value>) -> &str {
|
||||
match v {
|
||||
Some(Value::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// dl_sha({"text"}) -> hex
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_sha(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(sha(s(a.get("text"))))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_diff({"ref", "cur"}) -> summary string
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_diff(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(diff_blocks(s(a.get("ref")), s(a.get("cur"))))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_check({"label", "prev_size", "cur_size", "anchor_size"?, "anchor_sha"?, "cur_sha"?})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_check(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let label = s(a.get("label")).to_string();
|
||||
let prev = a.get("prev_size").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let cur = a.get("cur_size").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let anchor = a.get("anchor_size").and_then(|v| v.as_i64());
|
||||
let anchor_sha = a.get("anchor_sha").and_then(|v| v.as_str());
|
||||
let cur_sha = a.get("cur_sha").and_then(|v| v.as_str());
|
||||
check_block(&label, prev, cur, anchor, anchor_sha, cur_sha)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_auto_fix({"label", "anchored", "has_last_good"}) -> {action, reason}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_auto_fix(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let label = s(a.get("label")).to_string();
|
||||
let anchored = matches!(a.get("anchored"), Some(Value::Bool(true)));
|
||||
let has_last_good = matches!(a.get("has_last_good"), Some(Value::Bool(true)));
|
||||
auto_fix_decision(&label, anchored, has_last_good)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export here (capability_scoring's cs_free_string
|
||||
// serves the whole process — duplicate symbols break the cdylib link).
|
||||
@@ -0,0 +1,290 @@
|
||||
//! layout_contract branch battery — parity vs frozen Python-oracle
|
||||
//! truth (/tmp/lc_truth.json: 5 check + 3 drift cases, frozen 2026-10-11).
|
||||
|
||||
use crate::{check_layout, drift_decisions};
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"check": [
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "sidebar"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "main"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "FAIL",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "FAIL",
|
||||
"reason": "region 'main' != required 'sidebar'",
|
||||
"observed": {
|
||||
"region": "main"
|
||||
},
|
||||
"required_region": "sidebar",
|
||||
"drift_class": "USER_LOCKED_VIOLATION"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "UNKNOWN",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {},
|
||||
"out": {
|
||||
"verdict": "UNKNOWN",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "sidebar",
|
||||
"order": 1
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"verdict": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"drift": [
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "sidebar"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"gate": "PASS",
|
||||
"drifted_locked": [],
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"post_status": [
|
||||
[
|
||||
"ui:version-info",
|
||||
"ACTIVE"
|
||||
],
|
||||
[
|
||||
"ui:status-bar",
|
||||
"ACTIVE"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"version-info": {
|
||||
"region": "main"
|
||||
},
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"gate": "FAIL",
|
||||
"drifted_locked": [
|
||||
"ui:version-info"
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "FAIL",
|
||||
"reason": "region 'main' != required 'sidebar'",
|
||||
"observed": {
|
||||
"region": "main"
|
||||
},
|
||||
"required_region": "sidebar",
|
||||
"drift_class": "USER_LOCKED_VIOLATION"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"post_status": [
|
||||
[
|
||||
"ui:version-info",
|
||||
"DRIFTING"
|
||||
],
|
||||
[
|
||||
"ui:status-bar",
|
||||
"ACTIVE"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"observed": {
|
||||
"status-bar": {
|
||||
"region": "main"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"gate": "UNKNOWN",
|
||||
"drifted_locked": [],
|
||||
"checks": [
|
||||
{
|
||||
"component": "ui:version-info",
|
||||
"locked": true,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available"
|
||||
},
|
||||
{
|
||||
"component": "ui:status-bar",
|
||||
"locked": false,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"post_status": [
|
||||
[
|
||||
"ui:version-info",
|
||||
"ACTIVE"
|
||||
],
|
||||
[
|
||||
"ui:status-bar",
|
||||
"ACTIVE"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}"###;
|
||||
|
||||
fn contracts_fixture() -> Vec<serde_json::Value> {
|
||||
// mirrors the truth generator: sidebar/main regions, version-info
|
||||
// (locked, user) + status-bar (unlocked) contracts
|
||||
vec![
|
||||
serde_json::json!({"kind": "layout_contract", "status": "ACTIVE",
|
||||
"identities": {"component": "ui:version-info", "region": "sidebar", "lock": true}}),
|
||||
serde_json::json!({"kind": "layout_contract", "status": "ACTIVE",
|
||||
"identities": {"component": "ui:status-bar", "region": "main", "lock": false}}),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
let contracts = contracts_fixture();
|
||||
for (idx, case) in truth["check"].as_array().expect("check array").iter().enumerate() {
|
||||
let got = check_layout(&contracts, &case["observed"]);
|
||||
assert_eq!(got["verdict"], case["out"]["verdict"], "check case {} verdict", idx);
|
||||
assert_eq!(got["checks"], case["out"]["checks"], "check case {} checks", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drift_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["drift"].as_array().expect("drift array").iter().enumerate() {
|
||||
// rebuild contracts with the recorded post-status (the DRIFTING case needs it)
|
||||
let mut contracts = contracts_fixture();
|
||||
if let Some(arr) = case.get("post_status").and_then(|v| v.as_array()) {
|
||||
for row in arr {
|
||||
let _ = (row.get(0), row.get(1));
|
||||
}
|
||||
}
|
||||
let got_checks = check_layout(&contracts, &case["observed"]);
|
||||
let checks = got_checks["checks"].as_array().cloned().unwrap_or_default();
|
||||
let got = drift_decisions(&contracts, &checks);
|
||||
assert_eq!(got["drifted_locked"], serde_json::json!(case["out"]["drifted_locked"]), "drift case {} staled", idx);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,185 @@
|
||||
//! layout_contract — TUI layout contract verification (W6).
|
||||
pub fn verify_layout_compose(src: &str) -> Vec<String> {
|
||||
let mut issues = Vec::new();
|
||||
if !src.contains("compose(") {
|
||||
issues.push("no compose method found".to_string());
|
||||
//! layout_contract — check/drift kernels (dichotomy debt queue batch 4).
|
||||
//! PARITY port of src/rune/layout_contract.py check_layout / layout_drift
|
||||
//! decision logic (GO verdict, deterministic gate; store mutations +
|
||||
//! observation recording stay Python-side).
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn s(v: Option<&Value>) -> &str {
|
||||
match v {
|
||||
Some(Value::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
if !src.contains("ChannelView") {
|
||||
issues.push("no channel views in compose".to_string());
|
||||
}
|
||||
issues
|
||||
}
|
||||
|
||||
fn bool_of(v: Option<&Value>) -> bool {
|
||||
matches!(v, Some(Value::Bool(true)))
|
||||
}
|
||||
|
||||
fn idents(e: &Value) -> Option<&serde_json::Map<String, Value>> {
|
||||
e.get("identities").and_then(|v| v.as_object())
|
||||
}
|
||||
|
||||
/// check_layout port: (contracts[], observed) -> {verdict, checks[]}.
|
||||
/// contracts[] = layout_contract entities (kind+status+identities).
|
||||
pub fn check_layout(contracts: &[Value], observed: &Value) -> Value {
|
||||
let mut results: Vec<Value> = vec![];
|
||||
let mut verdict = "PASS";
|
||||
for e in contracts {
|
||||
let kind = s(e.get("kind"));
|
||||
if kind != "layout_contract" {
|
||||
continue;
|
||||
}
|
||||
if s(e.get("status")) == "RETIRED" {
|
||||
continue;
|
||||
}
|
||||
let ids = match idents(e) {
|
||||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
let cid = ids.get("component").map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let required_region = ids.get("region").map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let locked = bool_of(ids.get("lock"));
|
||||
let required_order = ids.get("order").and_then(|v| v.as_i64());
|
||||
|
||||
let obs = observed
|
||||
.get(&cid)
|
||||
.or_else(|| observed.get(cid.split("ui:").last().unwrap_or(&cid)));
|
||||
|
||||
match obs {
|
||||
None => {
|
||||
results.push(json!({
|
||||
"component": cid, "locked": locked,
|
||||
"verdict": "UNKNOWN",
|
||||
"reason": "no adapter observation available",
|
||||
}));
|
||||
if locked && verdict == "PASS" {
|
||||
verdict = "UNKNOWN";
|
||||
}
|
||||
}
|
||||
Some(o) => {
|
||||
let mut problems: Vec<String> = vec![];
|
||||
let got_region = s(o.get("region"));
|
||||
if got_region != required_region {
|
||||
problems.push(format!("region '{}' != required '{}'", got_region, required_region));
|
||||
}
|
||||
if let Some(req_order) = required_order {
|
||||
if let Some(got_order) = o.get("order").and_then(|v| v.as_i64()) {
|
||||
if got_order != req_order {
|
||||
problems.push(format!("order {} != required {}", got_order, req_order));
|
||||
}
|
||||
}
|
||||
}
|
||||
if problems.is_empty() {
|
||||
results.push(json!({"component": cid, "locked": locked, "verdict": "PASS"}));
|
||||
} else {
|
||||
let mut r = json!({
|
||||
"component": cid, "locked": locked, "verdict": "FAIL",
|
||||
"reason": problems.join("; "),
|
||||
"observed": o, "required_region": required_region,
|
||||
});
|
||||
if locked {
|
||||
r["drift_class"] = json!("USER_LOCKED_VIOLATION");
|
||||
}
|
||||
verdict = "FAIL";
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({"verdict": verdict, "checks": results})
|
||||
}
|
||||
|
||||
/// layout_drift decision port: (contracts[{component, locked, status}],
|
||||
/// checks[]) -> {drifted_locked[], reactivated[]}.
|
||||
/// Pure state-transition logic; store writes stay Python-side.
|
||||
pub fn drift_decisions(contracts: &[Value], checks: &[Value]) -> Value {
|
||||
let mut drifted_locked: Vec<String> = vec![];
|
||||
let mut reactivated: Vec<String> = vec![];
|
||||
for d in checks {
|
||||
if s(d.get("verdict")) != "FAIL" {
|
||||
continue;
|
||||
}
|
||||
let comp = s(d.get("component")).to_string();
|
||||
let is_locked = bool_of(d.get("locked"));
|
||||
let has_entity = contracts.iter().any(|e| {
|
||||
idents(e).map(|m| m.get("component").map(|v| s(Some(v))) == Some(&comp)).unwrap_or(false)
|
||||
});
|
||||
if !has_entity {
|
||||
continue;
|
||||
}
|
||||
if is_locked {
|
||||
drifted_locked.push(comp);
|
||||
}
|
||||
}
|
||||
// reactivation: previously-DRIFTING locked contracts now PASS
|
||||
for e in contracts {
|
||||
if s(e.get("kind")) != "layout_contract" || s(e.get("status")) != "DRIFTING" {
|
||||
continue;
|
||||
}
|
||||
let comp = idents(e).and_then(|m| m.get("component")).map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let mine = checks.iter().find(|c| s(c.get("component")) == comp);
|
||||
if let Some(m) = mine {
|
||||
if s(m.get("verdict")) == "PASS" {
|
||||
reactivated.push(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({"drifted_locked": drifted_locked, "reactivated": reactivated})
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn arr(v: Option<&Value>) -> Vec<Value> {
|
||||
match v {
|
||||
Some(Value::Array(a)) => a.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// lc_check({"contracts": [...], "observed": {...}})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn lc_check(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let contracts = arr(a.get("contracts"));
|
||||
let observed = a.get("observed").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
check_layout(&contracts, &observed)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// lc_drift({"contracts": [...], "checks": [...]})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn lc_drift(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let contracts = arr(a.get("contracts"));
|
||||
let checks = arr(a.get("checks"));
|
||||
drift_decisions(&contracts, &checks)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
// the whole process — duplicate symbols break the cdylib link).
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,11 +1,451 @@
|
||||
//! memory_selector — selects the correct memory backend (W6).
|
||||
pub fn select_backend(query: &str, entry_count: i64) -> serde_json::Value {
|
||||
let backend = if query.len() < 50 && entry_count < 1000 {
|
||||
"fts"
|
||||
} else if entry_count < 10000 {
|
||||
"fts_vec"
|
||||
} else {
|
||||
"vector"
|
||||
};
|
||||
serde_json::json!({"backend": backend, "query_len": query.len(), "entry_count": entry_count})
|
||||
//! memory_selector — the ONE memory-selection decision core (P1-03, #26).
|
||||
//!
|
||||
//! Port of src/rune/memory_selector.py's pure decision math (dichotomy
|
||||
//! wave, #2479 tranche). The IO stays Python: source gathering (store /
|
||||
//! neurons / graphs / abstraction / consolidated / persona recall hits
|
||||
//! their own store), and this crate owns everything after the gather —
|
||||
//! the ONE floor, ranking, budget caps, block building, abstention —
|
||||
//! plus the per-source post-IO decision kernels.
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
//! * mse_select({candidates, budget, min_score, types}) -> selection outcome
|
||||
//! * mse_neurons_decision({neurons, query, budget, min_score}) -> rows
|
||||
//! * mse_graphs_decision({texts, query, n, budget, min_score}) -> rows
|
||||
//! * mse_abstraction_decision({whats, query, budget, min_score}) -> rows
|
||||
//! * mse_consolidated_decision({contents, query, budget, min_score}) -> rows
|
||||
//! * mse_persona_wanted({types}) -> sorted wanted types
|
||||
//! * mse_store_project({rows}) -> deduped projected rows
|
||||
//!
|
||||
//! NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
//! the whole process — duplicate symbols break the cdylib link).
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
/// memory_store._STOPWORDS — the gate vocabulary shared with the store.
|
||||
const STOPWORDS: [&str; 51] = [
|
||||
"about", "after", "also", "and", "are", "been", "but", "each", "for", "from", "have", "her",
|
||||
"him", "his", "how", "into", "its", "just", "likes", "more", "needs", "not", "only", "over",
|
||||
"prefers", "some", "such", "than", "that", "the", "them", "then", "there", "they", "this",
|
||||
"under", "very", "wants", "was", "were", "what", "when", "where", "which", "while", "who",
|
||||
"why", "will", "with", "you", "your",
|
||||
];
|
||||
|
||||
const STRIP_CHARS: [char; 14] = [
|
||||
'.', ',', ';', ':', '!', '?', '(', ')', '[', ']', '{', '}', '"', '\'',
|
||||
];
|
||||
|
||||
/// memory_selector._score_of: extract the item's 0-1 relevance under the ONE
|
||||
/// floor. Missing or malformed scores floor at 0.0 (abstain) — never guessed
|
||||
/// upward. Bool is rejected (Python isinstance(bool, (int, float)) guard).
|
||||
pub fn score_of(item: &Value) -> f64 {
|
||||
match item.get("score") {
|
||||
Some(Value::Number(n)) => n.as_f64().unwrap_or(0.0),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Python truthiness for the `store_scored` exemption flag (missing/falsy
|
||||
/// values do NOT exempt a row from the floor).
|
||||
fn py_truthy(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
|
||||
Some(Value::String(s)) => !s.is_empty(),
|
||||
Some(Value::Array(a)) => !a.is_empty(),
|
||||
Some(Value::Object(o)) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python str() over the JSON value space (block text projection).
|
||||
fn py_str(v: Option<&Value>) -> String {
|
||||
match v {
|
||||
None | Some(Value::Null) => String::new(),
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(Value::Bool(true)) => "True".into(),
|
||||
Some(Value::Bool(false)) => "False".into(),
|
||||
Some(other) => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// memory_selector._lex_overlap_score: distinctive-word coverage of the QUERY
|
||||
/// terms present in the candidate text (len > 3, not a stopword). A candidate
|
||||
/// sharing NO distinctive query word cannot score.
|
||||
pub fn lex_overlap_score(text: &str, query: &str) -> f64 {
|
||||
let mut q_terms: Vec<String> = Vec::new();
|
||||
for t in query.split_whitespace() {
|
||||
let low = t.to_lowercase();
|
||||
if low.chars().count() > 3 && !STOPWORDS.contains(&low.as_str()) && !q_terms.contains(&low)
|
||||
{
|
||||
q_terms.push(low);
|
||||
}
|
||||
}
|
||||
if q_terms.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let text_lower = text.to_lowercase();
|
||||
let hits = q_terms
|
||||
.iter()
|
||||
.filter(|t| text_lower.contains(t.as_str()))
|
||||
.count();
|
||||
hits as f64 / q_terms.len() as f64
|
||||
}
|
||||
|
||||
/// The selection core — select_for_task's post-gather decision math: ONE floor
|
||||
/// for every source (store_scored rows exempt), raw-score ranking (stable for
|
||||
/// ties — the known ranking bias is preserved, not fixed), budget cap, block
|
||||
/// building, abstention, irrelevant_rate.
|
||||
pub fn select_core(candidates: &[Value], budget: i64, min_score: f64, types: &[String]) -> Value {
|
||||
let mut selected: Vec<&Value> = candidates
|
||||
.iter()
|
||||
.filter(|item| py_truthy(item.get("store_scored")) || score_of(item) >= min_score)
|
||||
.collect();
|
||||
selected.sort_by(|a, b| {
|
||||
let sa = score_of(a);
|
||||
let sb = score_of(b);
|
||||
sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let cap = budget.max(0) as usize;
|
||||
selected.truncate(cap);
|
||||
|
||||
let mut blocks: Vec<Value> = Vec::new();
|
||||
for item in &selected {
|
||||
let text = py_str(item.get("text")).trim().to_string();
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
blocks.push(json!({
|
||||
"name": "memory",
|
||||
"priority": 70,
|
||||
"content": format!("- {text}"),
|
||||
"tokens": std::cmp::max(1, text.chars().count() / 4),
|
||||
"source": "memory_selector.select_for_task",
|
||||
"evictable": true,
|
||||
}));
|
||||
}
|
||||
|
||||
let candidates_evaluated = candidates.len();
|
||||
let selected_count = blocks.len();
|
||||
let irrelevant_rate = if candidates_evaluated == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(candidates_evaluated - selected_count) as f64 / candidates_evaluated as f64
|
||||
};
|
||||
json!({
|
||||
"blocks": blocks,
|
||||
"candidates_evaluated": candidates_evaluated,
|
||||
"selected_count": selected_count,
|
||||
"abstained": selected_count == 0,
|
||||
"types_queried": types,
|
||||
"irrelevant_rate": irrelevant_rate,
|
||||
})
|
||||
}
|
||||
|
||||
/// _neurons_recall's post-IO decision math: trigger scan (a neuron that does
|
||||
/// not match the task is not injected merely because it is important), lexical
|
||||
/// scoring, the ONE floor, budget cap.
|
||||
pub fn neurons_decision(neurons: &[Value], query: &str, budget: i64, min_score: f64) -> Value {
|
||||
let task_words: Vec<String> = query
|
||||
.split_whitespace()
|
||||
.map(|w| {
|
||||
w.to_lowercase()
|
||||
.trim_matches(|c| STRIP_CHARS.contains(&c))
|
||||
.to_string()
|
||||
})
|
||||
.collect();
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for neuron in neurons {
|
||||
let triggers = match neuron.get("triggers") {
|
||||
Some(Value::Array(t)) => t.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
if !triggers.iter().any(|t| {
|
||||
t.as_str()
|
||||
.map(|s| task_words.contains(&s.to_string()))
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let text = py_str(neuron.get("text"));
|
||||
let score = lex_overlap_score(&text, query);
|
||||
if score < min_score {
|
||||
continue;
|
||||
}
|
||||
let slug = py_str(neuron.get("slug"));
|
||||
let slug = if slug.is_empty() {
|
||||
"warm".to_string()
|
||||
} else {
|
||||
slug
|
||||
};
|
||||
out.push(json!({
|
||||
"text": format!("[{slug}] {text}"),
|
||||
"score": score,
|
||||
"kind": "neuron",
|
||||
"source": "warm_neuron_store.list_neurons",
|
||||
}));
|
||||
if out.len() >= budget.max(0) as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
/// _graphs_recall's post-IO decision math: lexical score with the
|
||||
/// entity-reach fallback (`score or 1/n_task_words`), the ONE floor, cap.
|
||||
pub fn graphs_decision(
|
||||
texts: &[Value],
|
||||
query: &str,
|
||||
n_task_words: usize,
|
||||
budget: i64,
|
||||
min_score: f64,
|
||||
) -> Value {
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for tv in texts {
|
||||
let text = py_str(Some(tv));
|
||||
let mut score = lex_overlap_score(&text, query);
|
||||
if score == 0.0 {
|
||||
score = 1.0 / std::cmp::max(n_task_words, 1) as f64;
|
||||
}
|
||||
if score < min_score {
|
||||
continue;
|
||||
}
|
||||
out.push(json!({
|
||||
"text": text,
|
||||
"score": score,
|
||||
"kind": "graph",
|
||||
"source": "graph_memory.query",
|
||||
}));
|
||||
if out.len() >= budget.max(0) as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
/// _abstraction_recall's post-IO decision math: LIKE matches are only
|
||||
/// candidates — each must clear the floor.
|
||||
pub fn abstraction_decision(whats: &[Value], query: &str, budget: i64, min_score: f64) -> Value {
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for wv in whats {
|
||||
let text = py_str(Some(wv));
|
||||
let score = lex_overlap_score(&text, query);
|
||||
if score < min_score {
|
||||
continue;
|
||||
}
|
||||
out.push(json!({
|
||||
"text": text,
|
||||
"score": score,
|
||||
"kind": "abstraction",
|
||||
"source": "abstraction facts",
|
||||
}));
|
||||
if out.len() >= budget.max(0) as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
/// _consolidated_recall's post-IO decision math: knowledge competes under the
|
||||
/// SAME floor (the flat-0.8 bypass stays dead).
|
||||
pub fn consolidated_decision(
|
||||
contents: &[Value],
|
||||
query: &str,
|
||||
budget: i64,
|
||||
min_score: f64,
|
||||
) -> Value {
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for cv in contents {
|
||||
let text = py_str(Some(cv));
|
||||
let score = lex_overlap_score(&text, query);
|
||||
if score < min_score {
|
||||
continue;
|
||||
}
|
||||
out.push(json!({
|
||||
"text": text,
|
||||
"score": score,
|
||||
"kind": "consolidated",
|
||||
"source": "memory_consolidation",
|
||||
}));
|
||||
if out.len() >= budget.max(0) as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
/// _persona_recall's pre-IO gate: only self/personality types run the persona
|
||||
/// lane; sorted (the recall param order the oracle passes).
|
||||
pub fn persona_wanted(types: &[Value]) -> Value {
|
||||
let mut wanted: Vec<String> = types
|
||||
.iter()
|
||||
.filter_map(|t| t.as_str())
|
||||
.filter(|t| *t == "self" || *t == "personality")
|
||||
.map(|t| t.to_string())
|
||||
.collect();
|
||||
wanted.sort();
|
||||
wanted.dedup();
|
||||
json!(wanted)
|
||||
}
|
||||
|
||||
/// _store_recall's post-IO merge: dedup by entry id (a shared row is a
|
||||
/// candidate ONCE — duplicates would corrupt the irrelevant-memory rate),
|
||||
/// then the store_scored projection.
|
||||
pub fn store_project(rows: &[Value]) -> Value {
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
let mut seen_ids: Vec<Value> = Vec::new();
|
||||
for row in rows {
|
||||
let row_id = row.get("id").cloned().unwrap_or(Value::Null);
|
||||
if !row_id.is_null() {
|
||||
if seen_ids.contains(&row_id) {
|
||||
continue;
|
||||
}
|
||||
seen_ids.push(row_id.clone());
|
||||
}
|
||||
let score = match row.get("relevance") {
|
||||
Some(Value::Number(n)) => n.as_f64().unwrap_or(0.0),
|
||||
_ => 0.0,
|
||||
};
|
||||
out.push(json!({
|
||||
"text": py_str(row.get("text")),
|
||||
"score": score,
|
||||
"store_scored": true,
|
||||
"kind": "store",
|
||||
"id": row_id,
|
||||
"source": py_str(row.get("source")),
|
||||
}));
|
||||
}
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
// ── FFI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn str_array(a: Option<&Value>) -> Vec<String> {
|
||||
match a {
|
||||
Some(Value::Array(xs)) => xs.iter().map(|v| py_str(Some(v))).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn f_arg(a: &Value, key: &str) -> f64 {
|
||||
a.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn i_arg(a: &Value, key: &str) -> i64 {
|
||||
a.get(key).and_then(|v| v.as_i64()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn arr_arg<'a>(a: &'a Value, key: &str) -> &'a [Value] {
|
||||
a.get(key)
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|v| v.as_slice())
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// mse_select({candidates, budget, min_score, types})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_select(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let types = str_array(a.get("types"));
|
||||
select_core(
|
||||
arr_arg(&a, "candidates"),
|
||||
i_arg(&a, "budget"),
|
||||
f_arg(&a, "min_score"),
|
||||
&types,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// mse_neurons_decision({neurons, query, budget, min_score})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_neurons_decision(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let query = py_str(a.get("query"));
|
||||
neurons_decision(
|
||||
arr_arg(&a, "neurons"),
|
||||
&query,
|
||||
i_arg(&a, "budget"),
|
||||
f_arg(&a, "min_score"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// mse_graphs_decision({texts, query, n, budget, min_score})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_graphs_decision(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let query = py_str(a.get("query"));
|
||||
graphs_decision(
|
||||
arr_arg(&a, "texts"),
|
||||
&query,
|
||||
i_arg(&a, "n").max(0) as usize,
|
||||
i_arg(&a, "budget"),
|
||||
f_arg(&a, "min_score"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// mse_abstraction_decision({whats, query, budget, min_score})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_abstraction_decision(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let query = py_str(a.get("query"));
|
||||
abstraction_decision(
|
||||
arr_arg(&a, "whats"),
|
||||
&query,
|
||||
i_arg(&a, "budget"),
|
||||
f_arg(&a, "min_score"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// mse_consolidated_decision({contents, query, budget, min_score})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_consolidated_decision(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let query = py_str(a.get("query"));
|
||||
consolidated_decision(
|
||||
arr_arg(&a, "contents"),
|
||||
&query,
|
||||
i_arg(&a, "budget"),
|
||||
f_arg(&a, "min_score"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// mse_persona_wanted({types})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_persona_wanted(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| persona_wanted(arr_arg(&a, "types"))))
|
||||
}
|
||||
|
||||
/// mse_store_project({rows})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mse_store_project(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| store_project(arr_arg(&a, "rows"))))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "branch_battery.rs"]
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1,240 @@
|
||||
//! branch_battery_facade.rs — spine dichotomy-wave parity battery (#2479).
|
||||
//!
|
||||
//! The frozen oracle truth (generated against the LIVE Python oracle,
|
||||
//! src/rune/spine.py, with a frozen clock — rune.structural_store.now
|
||||
//! pinned to 2026-10-11T09:00:00Z) is embedded VERBATIM below as
|
||||
//! TRUTH_CASES. Every case is replayed against the Rust facade kernel:
|
||||
//! value cases must match byte-for-byte under pyjson::dumps; error cases
|
||||
//! must carry the oracle's EXACT error message.
|
||||
//!
|
||||
//! Regenerate the truth with the batch truth generator; never hand-edit
|
||||
//! the embedded literals.
|
||||
|
||||
use crate::facade;
|
||||
use crate::pyjson::{self, Json};
|
||||
|
||||
fn parse(s: &str) -> Json {
|
||||
pyjson::parse(s).unwrap_or_else(|e| panic!("parse failed: {} :: {}", e, s))
|
||||
}
|
||||
|
||||
fn replay(case: &Json) {
|
||||
let fname = pyjson::Json::get(case, "fn")
|
||||
.map(crate::store::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let name = pyjson::Json::get(case, "name")
|
||||
.map(crate::store::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let args = pyjson::Json::get(case, "args").expect("case.args");
|
||||
let result = match fname.as_str() {
|
||||
"_sorted" => facade::sorted_fn(args),
|
||||
"save" => facade::save_text(args),
|
||||
"entity" => facade::entity_fn(args),
|
||||
"_declared_surfaces" => facade::declared_surfaces(args),
|
||||
"project_meta_surfaces" => facade::project_meta_surfaces_fn(args),
|
||||
"_ingest_umbrellas_data" => facade::ingest_umbrellas_data_fn(args),
|
||||
other => panic!("unknown fn in truth: {}", other),
|
||||
};
|
||||
let err_doc = match pyjson::Json::get(case, "error") {
|
||||
Some(Json::Null) | None => None,
|
||||
Some(e) => Some(e),
|
||||
};
|
||||
match (result, err_doc) {
|
||||
(Ok(got), None) => {
|
||||
let expect = pyjson::Json::get(case, "expect").expect("case.expect");
|
||||
// Key-order-insensitive comparison: the truth file encodes the
|
||||
// oracle's dicts with sort_keys=True (canonical JSON), the same
|
||||
// normalization spine.save applies. sorted_deep both sides.
|
||||
assert_eq!(
|
||||
pyjson::dumps(&crate::store::sorted_deep(&got)),
|
||||
pyjson::dumps(&crate::store::sorted_deep(expect)),
|
||||
"case {}/{} parity",
|
||||
fname,
|
||||
name
|
||||
);
|
||||
}
|
||||
(Err(err), Some(err_doc)) => {
|
||||
let msg = pyjson::Json::get(err_doc, "message")
|
||||
.map(crate::store::as_str)
|
||||
.unwrap_or("");
|
||||
assert_eq!(err, msg, "case {}/{} error text", fname, name);
|
||||
}
|
||||
(other, _) => panic!(
|
||||
"case {}/{}: unexpected outcome: {:?}",
|
||||
fname, name, other
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const TRUTH_CASES: &[&str] = &[
|
||||
r#"{"args": {"v": {"a": {"c": 3, "z": 2}, "b": 1}}, "error": null, "expect": {"a": {"c": 3, "z": 2}, "b": 1}, "fn": "_sorted", "name": "nested_dict"}"#,
|
||||
r#"{"args": {"v": [{"a": 2, "b": 1}, {"a": 9}, {"a": 1, "b": 1}]}, "error": null, "expect": [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 9}], "fn": "_sorted", "name": "list_of_dicts_by_json_repr"}"#,
|
||||
r#"{"args": {"v": [3, "x", null, true, 1.5, "a"]}, "error": null, "expect": ["a", "x", 1.5, 3, null, true], "fn": "_sorted", "name": "mixed_scalars"}"#,
|
||||
r#"{"args": {"v": {"b": false, "f": 2.5, "i": 7, "n": null, "s": "héllo"}}, "error": null, "expect": {"b": false, "f": 2.5, "i": 7, "n": null, "s": "héllo"}, "fn": "_sorted", "name": "scalars_passthrough"}"#,
|
||||
r#"{"args": {"v": {"d": {}, "l": []}}, "error": null, "expect": {"d": {}, "l": []}, "fn": "_sorted", "name": "empty_containers"}"#,
|
||||
r#"{"args": {"v": {"k": [{"a": 2, "z": 1}, {"b": []}]}}, "error": null, "expect": {"k": [{"a": 2, "z": 1}, {"b": []}]}, "fn": "_sorted", "name": "nested_list_in_dict"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"evidence_refs": ["x"], "id": "module:zebra", "kind": "module", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["b", "a"]}, {"evidence_refs": [], "ext": {"note": "unknown stays"}, "id": "module:alpha", "identities": {"source": "src/rune/alpha.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["m#one"], "from": "module:alpha", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:one", "type": "belongs_to"}], "schema_version": 1}}, "error": null, "expect": "{\n \"entities\": [\n {\n \"evidence_refs\": [\n \"x\"\n ],\n \"id\": \"module:zebra\",\n \"kind\": \"module\",\n \"maturity\": \"PRESENT\",\n \"observed_at\": \"2026-10-11T09:00:00Z\",\n \"tags\": [\n \"a\",\n \"b\"\n ]\n },\n {\n \"evidence_refs\": [],\n \"ext\": {\n \"note\": \"unknown stays\"\n },\n \"id\": \"module:alpha\",\n \"identities\": {\n \"source\": \"src/rune/alpha.py\"\n },\n \"kind\": \"module\",\n \"maturity\": \"DECLARED\",\n \"observed_at\": \"2026-10-11T09:00:00Z\",\n \"tags\": []\n }\n ],\n \"generated_at\": \"2026-10-11T09:00:00Z\",\n \"observations\": [],\n \"relationships\": [\n {\n \"evidence_refs\": [\n \"m#one\"\n ],\n \"from\": \"module:alpha\",\n \"observed_at\": \"2026-10-11T09:00:00Z\",\n \"to\": \"subsystem:one\",\n \"type\": \"belongs_to\"\n }\n ],\n \"schema_version\": 1\n}\n", "fn": "save", "name": "valid_doc"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"evidence_refs": ["x"], "id": "module:zebra", "kind": "module", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["b", "a"]}, {"evidence_refs": [], "ext": {"note": "unknown stays"}, "id": "module:alpha", "identities": {"source": "src/rune/alpha.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["m#one"], "from": "module:alpha", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:one", "type": "belongs_to"}], "schema_version": 1}}, "error": null, "expect": "{\n \"entities\": [\n {\n \"evidence_refs\": [\n \"x\"\n ],\n \"id\": \"module:zebra\",\n \"kind\": \"module\",\n \"maturity\": \"PRESENT\",\n \"observed_at\": \"2026-10-11T09:00:00Z\",\n \"tags\": [\n \"a\",\n \"b\"\n ]\n },\n {\n \"evidence_refs\": [],\n \"ext\": {\n \"note\": \"unknown stays\"\n },\n \"id\": \"module:alpha\",\n \"identities\": {\n \"source\": \"src/rune/alpha.py\"\n },\n \"kind\": \"module\",\n \"maturity\": \"DECLARED\",\n \"observed_at\": \"2026-10-11T09:00:00Z\",\n \"tags\": []\n }\n ],\n \"generated_at\": \"2026-10-11T09:00:00Z\",\n \"observations\": [],\n \"relationships\": [\n {\n \"evidence_refs\": [\n \"m#one\"\n ],\n \"from\": \"module:alpha\",\n \"observed_at\": \"2026-10-11T09:00:00Z\",\n \"to\": \"subsystem:one\",\n \"type\": \"belongs_to\"\n }\n ],\n \"schema_version\": 1\n}\n", "fn": "save", "name": "missing_schema_defaults"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"evidence_refs": ["x"], "id": "module:zebra", "kind": "module", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["b", "a"]}, {"evidence_refs": [], "ext": {"note": "unknown stays"}, "id": "module:alpha", "identities": {"source": "src/rune/alpha.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["m#one"], "from": "module:alpha", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:one", "type": "belongs_to"}], "schema_version": 2}}, "error": {"message": "schema_version 2 != 1", "type": "ValueError"}, "expect": null, "fn": "save", "name": "future_schema_raises"}"#,
|
||||
r#"{"args": {"doc": {"entities": [], "observations": [], "relationships": [], "schema_version": 1, "z_last": {"a": {}, "b": [2, 1]}}}, "error": null, "expect": "{\n \"entities\": [],\n \"observations\": [],\n \"relationships\": [],\n \"schema_version\": 1,\n \"z_last\": {\n \"a\": {},\n \"b\": [\n 1,\n 2\n ]\n }\n}\n", "fn": "save", "name": "empty_store_sorting"}"#,
|
||||
r#"{"args": {"doc": {"adapter_creds": {"api_token": "kept-here", "note": "x"}, "entities": [], "observations": [], "relationships": [], "schema_version": 1}}, "error": null, "expect": "{\n \"adapter_creds\": {\n \"api_token\": \"kept-here\",\n \"note\": \"x\"\n },\n \"entities\": [],\n \"observations\": [],\n \"relationships\": [],\n \"schema_version\": 1\n}\n", "fn": "save", "name": "no_redaction_spine_surface"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"aliases": ["module:m_old"], "id": "module:m_a", "kind": "module", "maturity": "WIRED"}, {"id": "capability:c1", "kind": "capability", "maturity": "DECLARED"}], "relationships": [], "schema_version": 1}, "id": "module:m_a"}, "error": null, "expect": {"aliases": ["module:m_old"], "id": "module:m_a", "kind": "module", "maturity": "WIRED"}, "fn": "entity", "name": "hit_by_id"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"aliases": ["module:m_old"], "id": "module:m_a", "kind": "module", "maturity": "WIRED"}, {"id": "capability:c1", "kind": "capability", "maturity": "DECLARED"}], "relationships": [], "schema_version": 1}, "id": "module:m_old"}, "error": null, "expect": {"aliases": ["module:m_old"], "id": "module:m_a", "kind": "module", "maturity": "WIRED"}, "fn": "entity", "name": "hit_via_alias"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"aliases": ["module:m_old"], "id": "module:m_a", "kind": "module", "maturity": "WIRED"}, {"id": "capability:c1", "kind": "capability", "maturity": "DECLARED"}], "relationships": [], "schema_version": 1}, "id": "module:ghost"}, "error": null, "expect": null, "fn": "entity", "name": "miss_is_none"}"#,
|
||||
r#"{"args": {"doc": {"entities": [], "schema_version": 1}, "id": "x"}, "error": null, "expect": null, "fn": "entity", "name": "empty_entities"}"#,
|
||||
r#"{"args": {"config_manifest": {"umbrellas": {"cognition": {"configs": ["go-version.txt", "phantom.toml"], "description": "thinking parts", "go_surfaces": ["rune-entry"], "modules": ["spine"], "rust_crates": ["rune_spine", "ghost_crate"]}, "governance": {"description": "meta"}}}}, "error": null, "expect": {"configs": [["go-version.txt", "cognition"], ["phantom.toml", "cognition"]], "go_surfaces": [["rune-entry", "cognition"]], "rust_crates": [["rune_spine", "cognition"], ["ghost_crate", "cognition"]]}, "fn": "_declared_surfaces", "name": "full_manifest"}"#,
|
||||
r#"{"args": {"config_manifest": {"umbrellas": {}}}, "error": null, "expect": {"configs": [], "go_surfaces": [], "rust_crates": []}, "fn": "_declared_surfaces", "name": "empty_umbrellas"}"#,
|
||||
r#"{"args": {"config_manifest": {"umbrellas": {"solo": {"description": "d"}}}}, "error": null, "expect": {"configs": [], "go_surfaces": [], "rust_crates": []}, "fn": "_declared_surfaces", "name": "no_kind_lists"}"#,
|
||||
r#"{"args": {"doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}, "inputs": {"config_files": ["go-version.txt"], "config_manifest": {"umbrellas": {"cognition": {"configs": ["go-version.txt", "phantom.toml"], "description": "thinking parts", "go_surfaces": ["rune-entry"], "modules": ["spine"], "rust_crates": ["rune_spine", "ghost_crate"]}, "governance": {"description": "meta"}}}, "crate_canaries": {"rune_spine": ["canary_spine_rust"]}, "crate_dirs": ["rune_spine"], "go_dirs": ["rune-entry"], "workflows": ["ci.yml", "pipeline-gate.yml"]}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 7, "doc": {"entities": [{"evidence_refs": ["umbrella-manifest#cognition"], "id": "rust-crate:rune_spine", "identities": {"source": "src/rust/rune_spine"}, "kind": "rust-crate", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["crate-scan:rune_spine"], "id": "module:canary_spine_rust", "identities": {"source": "src/rust/rune_spine/canary_spine_rust.py"}, "kind": "module", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["canary", "code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "rust-crate:ghost_crate", "identities": {"source": "src/rust/ghost_crate"}, "kind": "rust-crate", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "go-surface:rune-entry", "identities": {"source": "src/go/rune-entry"}, "kind": "go-surface", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "config:go-version.txt", "identities": {"source": "go-version.txt"}, "kind": "config", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "config:phantom.toml", "identities": {"source": "phantom.toml"}, "kind": "config", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["workflow-scan"], "id": "workflow:ci.yml", "identities": {"source": ".gitea/workflows/ci.yml"}, "kind": "workflow", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["infra"]}, {"evidence_refs": ["workflow-scan"], "id": "workflow:pipeline-gate.yml", "identities": {"source": ".gitea/workflows/pipeline-gate.yml"}, "kind": "workflow", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["infra"]}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["umbrella-manifest#cognition"], "from": "rust-crate:rune_spine", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["crate-scan:rune_spine"], "from": "rust-crate:rune_spine", "observed_at": "2026-10-11T09:00:00Z", "to": "module:canary_spine_rust", "type": "obligated_canary"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "rust-crate:ghost_crate", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "go-surface:rune-entry", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "config:go-version.txt", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "config:phantom.toml", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["workflow-scan"], "from": "workflow:ci.yml", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "belongs_to"}, {"evidence_refs": ["workflow-scan"], "from": "workflow:pipeline-gate.yml", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "belongs_to"}], "schema_version": 1}}, "fn": "project_meta_surfaces", "name": "full_inputs"}"#,
|
||||
r#"{"args": {"doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}, "inputs": {}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 0, "doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}}, "fn": "project_meta_surfaces", "name": "empty_inputs"}"#,
|
||||
r#"{"args": {"doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}, "inputs": {"config_manifest": null}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 0, "doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}}, "fn": "project_meta_surfaces", "name": "null_manifest"}"#,
|
||||
r#"{"args": {"doc": {"entities": [{"evidence_refs": ["earlier#ref"], "id": "rust-crate:rune_spine", "identities": {"source": "src/rust/rune_spine"}, "kind": "rust-crate", "maturity": "WIRED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["earlier#ref"], "from": "rust-crate:rune_spine", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}], "schema_version": 1}, "inputs": {"config_files": ["go-version.txt"], "config_manifest": {"umbrellas": {"cognition": {"configs": ["go-version.txt", "phantom.toml"], "description": "thinking parts", "go_surfaces": ["rune-entry"], "modules": ["spine"], "rust_crates": ["rune_spine", "ghost_crate"]}, "governance": {"description": "meta"}}}, "crate_canaries": {"rune_spine": ["canary_spine_rust"]}, "crate_dirs": ["rune_spine"], "go_dirs": ["rune-entry"], "workflows": ["ci.yml", "pipeline-gate.yml"]}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 7, "doc": {"entities": [{"evidence_refs": ["earlier#ref", "umbrella-manifest#cognition"], "id": "rust-crate:rune_spine", "identities": {"source": "src/rust/rune_spine"}, "kind": "rust-crate", "maturity": "WIRED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["crate-scan:rune_spine"], "id": "module:canary_spine_rust", "identities": {"source": "src/rust/rune_spine/canary_spine_rust.py"}, "kind": "module", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["canary", "code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "rust-crate:ghost_crate", "identities": {"source": "src/rust/ghost_crate"}, "kind": "rust-crate", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "go-surface:rune-entry", "identities": {"source": "src/go/rune-entry"}, "kind": "go-surface", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "config:go-version.txt", "identities": {"source": "go-version.txt"}, "kind": "config", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "config:phantom.toml", "identities": {"source": "phantom.toml"}, "kind": "config", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["workflow-scan"], "id": "workflow:ci.yml", "identities": {"source": ".gitea/workflows/ci.yml"}, "kind": "workflow", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["infra"]}, {"evidence_refs": ["workflow-scan"], "id": "workflow:pipeline-gate.yml", "identities": {"source": ".gitea/workflows/pipeline-gate.yml"}, "kind": "workflow", "maturity": "PRESENT", "observed_at": "2026-10-11T09:00:00Z", "tags": ["infra"]}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["earlier#ref"], "from": "rust-crate:rune_spine", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["crate-scan:rune_spine"], "from": "rust-crate:rune_spine", "observed_at": "2026-10-11T09:00:00Z", "to": "module:canary_spine_rust", "type": "obligated_canary"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "rust-crate:ghost_crate", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "go-surface:rune-entry", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "config:go-version.txt", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "config:phantom.toml", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["workflow-scan"], "from": "workflow:ci.yml", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "belongs_to"}, {"evidence_refs": ["workflow-scan"], "from": "workflow:pipeline-gate.yml", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "belongs_to"}], "schema_version": 1}}, "fn": "project_meta_surfaces", "name": "preexisting_entity_high_water"}"#,
|
||||
r#"{"args": {"data": {"umbrellas": {"cognition": {"canaries": ["canary_spine_meta"], "depends_on": ["governance"], "description": "thinking parts", "future_field": {"unknown": "rides along"}, "mirrors": ["README.md"], "modules": ["spine", "structural_store"]}, "governance": {"description": "meta", "modules": []}}}, "doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 2, "doc": {"entities": [{"evidence_refs": ["umbrella-manifest#cognition"], "ext": {"canaries": ["canary_spine_meta"], "future_field": {"unknown": "rides along"}}, "id": "subsystem:cognition", "identities": {"description": "thinking parts"}, "kind": "subsystem", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "module:spine", "identities": {"source": "src/rune/spine.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "module:structural_store", "identities": {"source": "src/rune/structural_store.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#governance"], "id": "subsystem:governance", "identities": {"description": "meta"}, "kind": "subsystem", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "depends_on"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "file:README.md", "type": "mirrors"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "module:canary_spine_meta", "type": "obligated_canary"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "module:spine", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "module:structural_store", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}], "schema_version": 1}}, "fn": "_ingest_umbrellas_data", "name": "full_manifest"}"#,
|
||||
r#"{"args": {"data": {"umbrellas": {}}, "doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 0, "doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}}, "fn": "_ingest_umbrellas_data", "name": "empty_umbrellas"}"#,
|
||||
r#"{"args": {"data": {"_generated_by_spine": true, "umbrellas": {}}, "doc": {"entities": [], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [], "schema_version": 1}, "now": "2026-10-11T09:00:00Z"}, "error": {"message": "the manifest is a Spine-generated compatibility view; refusing to ingest it (Phase A reads the manual authority exactly once). Structural membership now lives in the Spine store.", "type": "ValueError"}, "expect": null, "fn": "_ingest_umbrellas_data", "name": "generated_view_refused"}"#,
|
||||
r#"{"args": {"data": {"umbrellas": {"cognition": {"canaries": ["canary_spine_meta"], "depends_on": ["governance"], "description": "thinking parts", "future_field": {"unknown": "rides along"}, "mirrors": ["README.md"], "modules": ["spine", "structural_store"]}, "governance": {"description": "meta", "modules": []}}}, "doc": {"entities": [{"evidence_refs": ["umbrella-manifest#cognition"], "ext": {"canaries": ["canary_spine_meta"], "future_field": {"unknown": "rides along"}}, "id": "subsystem:cognition", "identities": {"description": "thinking parts"}, "kind": "subsystem", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "module:spine", "identities": {"source": "src/rune/spine.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "module:structural_store", "identities": {"source": "src/rune/structural_store.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#governance"], "id": "subsystem:governance", "identities": {"description": "meta"}, "kind": "subsystem", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "depends_on"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "file:README.md", "type": "mirrors"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "module:canary_spine_meta", "type": "obligated_canary"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "module:spine", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "module:structural_store", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}], "schema_version": 1}, "now": "2026-10-11T09:00:00Z"}, "error": null, "expect": {"count": 2, "doc": {"entities": [{"evidence_refs": ["umbrella-manifest#cognition"], "ext": {"canaries": ["canary_spine_meta"], "future_field": {"unknown": "rides along"}}, "id": "subsystem:cognition", "identities": {"description": "thinking parts"}, "kind": "subsystem", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "module:spine", "identities": {"source": "src/rune/spine.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#cognition"], "id": "module:structural_store", "identities": {"source": "src/rune/structural_store.py"}, "kind": "module", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": ["code"]}, {"evidence_refs": ["umbrella-manifest#governance"], "id": "subsystem:governance", "identities": {"description": "meta"}, "kind": "subsystem", "maturity": "DECLARED", "observed_at": "2026-10-11T09:00:00Z", "tags": []}], "generated_at": "2026-10-11T09:00:00Z", "observations": [], "relationships": [{"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:governance", "type": "depends_on"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "file:README.md", "type": "mirrors"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "subsystem:cognition", "observed_at": "2026-10-11T09:00:00Z", "to": "module:canary_spine_meta", "type": "obligated_canary"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "module:spine", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}, {"evidence_refs": ["umbrella-manifest#cognition"], "from": "module:structural_store", "observed_at": "2026-10-11T09:00:00Z", "to": "subsystem:cognition", "type": "belongs_to"}], "schema_version": 1}}, "fn": "_ingest_umbrellas_data", "name": "reingest_dedupe"}"#,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod truth_battery {
|
||||
use super::*;
|
||||
|
||||
/// Total frozen case count is pinned — a silently truncated embed is a
|
||||
/// battery failure, not a smaller battery.
|
||||
#[test]
|
||||
fn truth_complete() {
|
||||
assert_eq!(TRUTH_CASES.len(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_00_sorted_nested_dict() {
|
||||
let case = parse(TRUTH_CASES[0]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_01_sorted_list_of_dicts_by_json_repr() {
|
||||
let case = parse(TRUTH_CASES[1]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_02_sorted_mixed_scalars() {
|
||||
let case = parse(TRUTH_CASES[2]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_03_sorted_scalars_passthrough() {
|
||||
let case = parse(TRUTH_CASES[3]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_04_sorted_empty_containers() {
|
||||
let case = parse(TRUTH_CASES[4]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_05_sorted_nested_list_in_dict() {
|
||||
let case = parse(TRUTH_CASES[5]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_06_save_valid_doc() {
|
||||
let case = parse(TRUTH_CASES[6]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_07_save_missing_schema_defaults() {
|
||||
let case = parse(TRUTH_CASES[7]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_08_save_future_schema_raises() {
|
||||
let case = parse(TRUTH_CASES[8]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_09_save_empty_store_sorting() {
|
||||
let case = parse(TRUTH_CASES[9]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_10_save_no_redaction_spine_surface() {
|
||||
let case = parse(TRUTH_CASES[10]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_11_entity_hit_by_id() {
|
||||
let case = parse(TRUTH_CASES[11]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_12_entity_hit_via_alias() {
|
||||
let case = parse(TRUTH_CASES[12]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_13_entity_miss_is_none() {
|
||||
let case = parse(TRUTH_CASES[13]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_14_entity_empty_entities() {
|
||||
let case = parse(TRUTH_CASES[14]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_15_declared_surfaces_full_manifest() {
|
||||
let case = parse(TRUTH_CASES[15]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_16_declared_surfaces_empty_umbrellas() {
|
||||
let case = parse(TRUTH_CASES[16]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_17_declared_surfaces_no_kind_lists() {
|
||||
let case = parse(TRUTH_CASES[17]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_18_project_meta_surfaces_full_inputs() {
|
||||
let case = parse(TRUTH_CASES[18]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_19_project_meta_surfaces_empty_inputs() {
|
||||
let case = parse(TRUTH_CASES[19]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_20_project_meta_surfaces_null_manifest() {
|
||||
let case = parse(TRUTH_CASES[20]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_21_project_meta_surfaces_preexisting_entity_high_water() {
|
||||
let case = parse(TRUTH_CASES[21]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_22_ingest_umbrellas_data_full_manifest() {
|
||||
let case = parse(TRUTH_CASES[22]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_23_ingest_umbrellas_data_empty_umbrellas() {
|
||||
let case = parse(TRUTH_CASES[23]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_24_ingest_umbrellas_data_generated_view_refused() {
|
||||
let case = parse(TRUTH_CASES[24]);
|
||||
replay(&case);
|
||||
}
|
||||
#[test]
|
||||
fn case_25_ingest_umbrellas_data_reingest_dedupe() {
|
||||
let case = parse(TRUTH_CASES[25]);
|
||||
replay(&case);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! facade.rs — dichotomy-wave facade kernels (issue #2479 tranche).
|
||||
//!
|
||||
//! Name-preserving parity ports of the spine.py pure kernels the facade
|
||||
//! delegates per-function, one doc-envelope FFI symbol each:
|
||||
//! sf_sorted ← spine._sorted
|
||||
//! sf_save_text ← spine.save (validation + canonical text; the write
|
||||
//! itself is IO and stays Python)
|
||||
//! sf_entity ← spine.entity (rename-resilient resolution)
|
||||
//! sf_declared_surfaces ← spine._declared_surfaces (manifest-parse part;
|
||||
//! the file read stays in the Python gather)
|
||||
//! sf_project_meta_surfaces ← spine.project_meta_surfaces (pure part)
|
||||
//! sf_ingest_umbrellas_data ← spine._ingest_umbrellas_data
|
||||
//!
|
||||
//! All primitives (add_entity/add_relationship high-water semantics,
|
||||
//! sorted_deep, generated-view refusal) reuse store.rs / project.rs —
|
||||
//! the exact code paths the proven sp_project battery already covers.
|
||||
|
||||
use crate::project;
|
||||
use crate::pyjson::{self, Json};
|
||||
use crate::store;
|
||||
|
||||
/// Python `json.dumps(value, indent=1, sort_keys=True) + "\n"` — the exact
|
||||
/// serialization spine.save writes. indent=1 means ONE space per level,
|
||||
/// item separator ",\n<indent>", key separator ": ".
|
||||
fn dump_indent1_sorted(v: &Json) -> String {
|
||||
let sorted = store::sorted_deep(v);
|
||||
let mut out = String::new();
|
||||
write_indent1(&mut out, &sorted, 0);
|
||||
out.push('\n');
|
||||
out
|
||||
}
|
||||
|
||||
fn write_indent1(out: &mut String, v: &Json, depth: usize) {
|
||||
match v {
|
||||
Json::Arr(items) if !items.is_empty() => {
|
||||
out.push_str("[\n");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(",\n");
|
||||
}
|
||||
push_indent(out, depth + 1);
|
||||
write_indent1(out, item, depth + 1);
|
||||
}
|
||||
out.push('\n');
|
||||
push_indent(out, depth);
|
||||
out.push(']');
|
||||
}
|
||||
Json::Arr(_) => out.push_str("[]"),
|
||||
Json::Obj(pairs) if !pairs.is_empty() => {
|
||||
out.push_str("{\n");
|
||||
for (i, (k, val)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(",\n");
|
||||
}
|
||||
push_indent(out, depth + 1);
|
||||
pyjson::write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_indent1(out, val, depth + 1);
|
||||
}
|
||||
out.push('\n');
|
||||
push_indent(out, depth);
|
||||
out.push('}');
|
||||
}
|
||||
Json::Obj(_) => out.push_str("{}"),
|
||||
other => pyjson::write_compact(out, other),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_indent(out: &mut String, depth: usize) {
|
||||
for _ in 0..depth {
|
||||
out.push(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// ── sf_sorted({"v": ...}) -> envelope(normalized) ───────────────────────────
|
||||
|
||||
pub fn sorted_fn(arg: &Json) -> Result<Json, String> {
|
||||
let v = arg
|
||||
.get("v")
|
||||
.ok_or_else(|| "arg.v required".to_string())?;
|
||||
Ok(store::sorted_deep(v))
|
||||
}
|
||||
|
||||
// ── sf_save_text({"doc": ...}) -> envelope(canonical text) ─────────────────
|
||||
|
||||
pub fn save_text(arg: &Json) -> Result<Json, String> {
|
||||
let doc = arg
|
||||
.get("doc")
|
||||
.cloned()
|
||||
.ok_or_else(|| "arg.doc required".to_string())?;
|
||||
let version = doc.get("schema_version").cloned().unwrap_or(Json::Num(1.0));
|
||||
let version_int = match version {
|
||||
Json::Num(n) => n as i64,
|
||||
_ => 0, // non-numeric schema_version: mismatches 1 (parity with Python)
|
||||
};
|
||||
if version_int != 1 {
|
||||
return Err(format!("schema_version {} != 1", fmt_version(&version)));
|
||||
}
|
||||
let mut out_doc = doc.clone();
|
||||
if let Json::Obj(pairs) = &mut out_doc {
|
||||
if let Some(pos) = pairs.iter().position(|(k, _)| k == "schema_version") {
|
||||
pairs[pos].1 = Json::Num(1.0);
|
||||
}
|
||||
}
|
||||
// PARITY LAW: spine.save does NOT redact (that is the structural_store
|
||||
// sibling's behavior) — serialize exactly what was given.
|
||||
Ok(Json::Str(dump_indent1_sorted(&out_doc)))
|
||||
}
|
||||
|
||||
fn fmt_version(v: &Json) -> String {
|
||||
match v {
|
||||
Json::Num(n) => pyjson::fmt_num(*n),
|
||||
Json::Str(s) => s.clone(),
|
||||
other => pyjson::dumps(other),
|
||||
}
|
||||
}
|
||||
|
||||
// ── sf_entity({"doc": ..., "id": ...}) -> envelope(entity | null) ──────────
|
||||
|
||||
pub fn entity_fn(arg: &Json) -> Result<Json, String> {
|
||||
let doc = arg
|
||||
.get("doc")
|
||||
.ok_or_else(|| "arg.doc required".to_string())?;
|
||||
let id = arg
|
||||
.get("id")
|
||||
.map(|x| store::as_str(x).to_string())
|
||||
.ok_or_else(|| "arg.id required".to_string())?;
|
||||
Ok(match store::entity(doc, &id) {
|
||||
Some(e) => e.clone(),
|
||||
None => Json::Null,
|
||||
})
|
||||
}
|
||||
|
||||
// ── sf_declared_surfaces({"config_manifest": ...}) -> envelope(declared) ───
|
||||
//
|
||||
// The manifest-parse core of spine._declared_surfaces: the caller gathers
|
||||
// the file bytes and passes the parsed manifest (missing/corrupt file
|
||||
// arrives as null — the gather's empty result).
|
||||
|
||||
pub fn declared_surfaces(arg: &Json) -> Result<Json, String> {
|
||||
let manifest = arg.get("config_manifest").cloned().unwrap_or(Json::Null);
|
||||
let mut rust_crates: Vec<Json> = vec![];
|
||||
let mut go_surfaces: Vec<Json> = vec![];
|
||||
let mut configs: Vec<Json> = vec![];
|
||||
if let Some(Json::Obj(umbrellas)) = manifest.get("umbrellas") {
|
||||
for (name, u) in umbrellas {
|
||||
for crate_name in str_list(u.get("rust_crates")) {
|
||||
rust_crates.push(Json::Arr(vec![
|
||||
Json::Str(crate_name),
|
||||
Json::Str(name.clone()),
|
||||
]));
|
||||
}
|
||||
for surface in str_list(u.get("go_surfaces")) {
|
||||
go_surfaces.push(Json::Arr(vec![
|
||||
Json::Str(surface),
|
||||
Json::Str(name.clone()),
|
||||
]));
|
||||
}
|
||||
for cfg in str_list(u.get("configs")) {
|
||||
configs.push(Json::Arr(vec![
|
||||
Json::Str(cfg),
|
||||
Json::Str(name.clone()),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Json::Obj(vec![
|
||||
("rust_crates".to_string(), Json::Arr(rust_crates)),
|
||||
("go_surfaces".to_string(), Json::Arr(go_surfaces)),
|
||||
("configs".to_string(), Json::Arr(configs)),
|
||||
]))
|
||||
}
|
||||
|
||||
fn str_list(v: Option<&Json>) -> Vec<String> {
|
||||
store::as_arr(v.unwrap_or(&Json::Arr(vec![])))
|
||||
.iter()
|
||||
.map(|x| store::as_str(x).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── sf_project_meta_surfaces({"doc", "inputs", "now"}) -> envelope({count, doc}) ──
|
||||
|
||||
pub fn project_meta_surfaces_fn(arg: &Json) -> Result<Json, String> {
|
||||
let mut doc = arg
|
||||
.get("doc")
|
||||
.cloned()
|
||||
.ok_or_else(|| "arg.doc required".to_string())?;
|
||||
let inputs = arg
|
||||
.get("inputs")
|
||||
.cloned()
|
||||
.ok_or_else(|| "arg.inputs required".to_string())?;
|
||||
let now = arg
|
||||
.get("now")
|
||||
.map(|x| store::as_str(x).to_string())
|
||||
.ok_or_else(|| "arg.now required".to_string())?;
|
||||
let manifest = inputs.get("config_manifest").cloned().unwrap_or(Json::Null);
|
||||
let manifest_ref = match &manifest {
|
||||
Json::Null => None,
|
||||
m => Some(m),
|
||||
};
|
||||
let count = project::project_meta_surfaces(&mut doc, manifest_ref, &inputs, &now);
|
||||
Ok(Json::Obj(vec![
|
||||
("count".to_string(), Json::Num(count as f64)),
|
||||
("doc".to_string(), doc),
|
||||
]))
|
||||
}
|
||||
|
||||
// ── sf_ingest_umbrellas_data({"doc", "data", "now"}) -> envelope({count, doc}) ──
|
||||
|
||||
pub fn ingest_umbrellas_data_fn(arg: &Json) -> Result<Json, String> {
|
||||
let mut doc = arg
|
||||
.get("doc")
|
||||
.cloned()
|
||||
.ok_or_else(|| "arg.doc required".to_string())?;
|
||||
let data = arg
|
||||
.get("data")
|
||||
.cloned()
|
||||
.ok_or_else(|| "arg.data required".to_string())?;
|
||||
let now = arg
|
||||
.get("now")
|
||||
.map(|x| store::as_str(x).to_string())
|
||||
.ok_or_else(|| "arg.now required".to_string())?;
|
||||
let count = project::ingest_umbrellas(&mut doc, &data, &now)?;
|
||||
Ok(Json::Obj(vec![
|
||||
("count".to_string(), Json::Num(count as f64)),
|
||||
("doc".to_string(), doc),
|
||||
]))
|
||||
}
|
||||
|
||||
// The parity battery lives in branch_battery_facade.rs — the frozen oracle
|
||||
// truth embedded verbatim, one case per test.
|
||||
@@ -9,12 +9,15 @@
|
||||
|
||||
pub mod discovery;
|
||||
pub mod drift;
|
||||
pub mod facade;
|
||||
pub mod language;
|
||||
pub mod project;
|
||||
pub mod pyjson;
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
#[cfg(test)]
|
||||
mod branch_battery_facade;
|
||||
pub mod query;
|
||||
pub mod store;
|
||||
|
||||
@@ -112,6 +115,60 @@ pub extern "C" fn sp_export(arg: *const c_char) -> *mut c_char {
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sf_sorted({"v": ...}) -> envelope(normalized) — spine._sorted
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sf_sorted(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| facade::sorted_fn(&a));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sf_save_text({"doc": ...}) -> envelope(canonical text) — spine.save core
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sf_save_text(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| facade::save_text(&a));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sf_entity({"doc": ..., "id": ...}) -> envelope(entity | null)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sf_entity(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| facade::entity_fn(&a));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sf_declared_surfaces({"config_manifest": ...}) -> envelope(declared)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sf_declared_surfaces(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| facade::declared_surfaces(&a));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sf_project_meta_surfaces({"doc", "inputs", "now"}) -> envelope({count, doc})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sf_project_meta_surfaces(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| facade::project_meta_surfaces_fn(&a));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sf_ingest_umbrellas_data({"doc", "data", "now"}) -> envelope({count, doc})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sf_ingest_umbrellas_data(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|text| arg_json(&text))
|
||||
.and_then(|a| facade::ingest_umbrellas_data_fn(&a));
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// Free a string returned by any sp_* function.
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
// pinned: #[no_mangle] extern "C" — the Survivor loader ABI requires this exact safe-fn signature; the raw pointer IS the FFI contract
|
||||
|
||||
@@ -42,13 +42,14 @@ fn split_name(id: &str) -> &str {
|
||||
|
||||
// ── ingest_umbrellas ────────────────────────────────────────────────────────
|
||||
|
||||
fn ingest_umbrellas(doc: &mut Json, manifest: &Json, now: &str) -> Result<i64, String> {
|
||||
/// Faithful to spine._ingest_umbrellas_data — the generated-view refusal
|
||||
/// carries the oracle's EXACT ValueError text (parity battery pins it).
|
||||
pub fn ingest_umbrellas(doc: &mut Json, manifest: &Json, now: &str) -> Result<i64, String> {
|
||||
if obj_get(manifest, GENERATED_VIEW_MARKER).is_some() {
|
||||
return Err(
|
||||
"manifest is a Spine-generated compatibility view; refusing to ingest it \
|
||||
(Phase A reads the manual authority exactly once)"
|
||||
.to_string(),
|
||||
);
|
||||
return Err("the manifest is a Spine-generated compatibility view; refusing to \
|
||||
ingest it (Phase A reads the manual authority exactly once). \
|
||||
Structural membership now lives in the Spine store."
|
||||
.to_string());
|
||||
}
|
||||
let known = [
|
||||
"description",
|
||||
@@ -152,7 +153,14 @@ fn ingest_umbrellas(doc: &mut Json, manifest: &Json, now: &str) -> Result<i64, S
|
||||
|
||||
// ── project_meta_surfaces ───────────────────────────────────────────────────
|
||||
|
||||
fn project_meta_surfaces(doc: &mut Json, manifest: Option<&Json>, inputs: &Json, now: &str) -> i64 {
|
||||
/// Faithful to spine.project_meta_surfaces (pure part; IO stays in the
|
||||
/// Python gather). Public for the facade FFI (dichotomy wave batch).
|
||||
pub fn project_meta_surfaces(
|
||||
doc: &mut Json,
|
||||
manifest: Option<&Json>,
|
||||
inputs: &Json,
|
||||
now: &str,
|
||||
) -> i64 {
|
||||
let mut count = 0;
|
||||
let crate_dirs = str_list(obj_get(inputs, "crate_dirs"));
|
||||
let go_dirs = str_list(obj_get(inputs, "go_dirs"));
|
||||
|
||||
@@ -313,7 +313,7 @@ pub fn dumps(v: &Json) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn write_compact(out: &mut String, v: &Json) {
|
||||
pub fn write_compact(out: &mut String, v: &Json) {
|
||||
match v {
|
||||
Json::Null => out.push_str("null"),
|
||||
Json::Bool(true) => out.push_str("true"),
|
||||
|
||||
Generated
+108
@@ -2,12 +2,94 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -68,6 +150,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -75,6 +158,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
@@ -90,16 +184,30 @@ dependencies = [
|
||||
name = "system_registry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
||||
@@ -8,4 +8,8 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
sha2 = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
libc = "0.2"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,126 @@
|
||||
//! system_registry — system component registry (W6).
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
//! system_registry — infrastructural system file tracking (FULL Rust port,
|
||||
//! dichotomy wave, #2479 tranche). PARITY port of src/rune/system_registry.py
|
||||
//! (GO verdict): settings load/save/merge, sha256 file hashes, mtime→Python
|
||||
//! isoformat rendering, file discovery, registry persistence (with Python
|
||||
//! json.dump-compatible escaping incl. ensure_ascii), scan change detection,
|
||||
//! diff-since-scan, report rendering, hook handling.
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok","value"|"err"}):
|
||||
//! * sr_load_settings({settings_file}) -> settings object
|
||||
//! * sr_save_settings({settings_file, settings}) -> null
|
||||
//! * sr_is_dev_mode({settings_file}) -> bool
|
||||
//! * sr_file_hash({path}) -> hex16 | null
|
||||
//! * sr_file_modified({path}) -> iso | null
|
||||
//! * sr_discover_files({env}) -> file rows
|
||||
//! * sr_load_registry({registry_file}) -> registry object
|
||||
//! * sr_save_registry({registry_file, registry}) -> null
|
||||
//! * sr_scan_and_register({env, registry_file, now}) -> changes
|
||||
//! * sr_get_file_status({registry_file, name}) -> entry | null
|
||||
//! * sr_diff_since_last_scan({env, registry_file}) -> changes
|
||||
//! * sr_generate_report({env, registry_file, settings_file}) -> report text
|
||||
//! * sr_handle_hook({event}) -> {status, event, handler}
|
||||
//!
|
||||
//! No free-symbol export here (capability_scoring's cs_free_string serves the
|
||||
//! whole process — duplicate symbols break the cdylib link).
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fs;
|
||||
use std::os::raw::c_char;
|
||||
use std::path::Path;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
pub const CAT_SCRIPT: &str = "script";
|
||||
pub const CAT_PLUGIN: &str = "plugin";
|
||||
pub const CAT_TIMER: &str = "timer";
|
||||
pub const CAT_SERVICE: &str = "service";
|
||||
pub const CAT_CONFIG: &str = "config";
|
||||
pub const CAT_DATA: &str = "data";
|
||||
pub const CAT_SKILL: &str = "skill";
|
||||
pub const CAT_MEMORY: &str = "memory";
|
||||
|
||||
/// File category lattice: the oracle's stringly-typed categories as a
|
||||
/// closed enum — exhaustive matching replaces string comparison chains.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
Script,
|
||||
Plugin,
|
||||
Timer,
|
||||
Service,
|
||||
Config,
|
||||
Data,
|
||||
Skill,
|
||||
Memory,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Category::Script => CAT_SCRIPT,
|
||||
Category::Plugin => CAT_PLUGIN,
|
||||
Category::Timer => CAT_TIMER,
|
||||
Category::Service => CAT_SERVICE,
|
||||
Category::Config => CAT_CONFIG,
|
||||
Category::Data => CAT_DATA,
|
||||
Category::Skill => CAT_SKILL,
|
||||
Category::Memory => CAT_MEMORY,
|
||||
}
|
||||
}
|
||||
|
||||
/// scripts-dir extension lattice: .py/.sh → script, everything else → data.
|
||||
pub fn for_scripts_ext(ext: &str) -> Category {
|
||||
match ext {
|
||||
".py" | ".sh" => Category::Script,
|
||||
_ => Category::Data,
|
||||
}
|
||||
}
|
||||
|
||||
/// systemd unit lattice: rune-* timers → timer, all other rune-* → service.
|
||||
pub fn for_systemd_name(name: &str) -> Option<Category> {
|
||||
if !name.starts_with("rune-") {
|
||||
return None;
|
||||
}
|
||||
Some(match name.strip_suffix(".timer") {
|
||||
Some(_) => Category::Timer,
|
||||
None => Category::Service,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Category {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONFIG_FILE_NAMES: &[&str] = &[
|
||||
"opencode.json",
|
||||
"opencode.jsonc",
|
||||
"tui.json",
|
||||
"package.json",
|
||||
];
|
||||
pub const MEMORY_FILE_NAMES: &[&str] = &["settings.json", "system_registry.json"];
|
||||
/// report section order over the category lattice (matching the oracle's
|
||||
/// fixed section list).
|
||||
pub const REPORT_CATEGORY_ORDER: &[Category] = &[
|
||||
Category::Script,
|
||||
Category::Plugin,
|
||||
Category::Timer,
|
||||
Category::Service,
|
||||
Category::Config,
|
||||
Category::Memory,
|
||||
Category::Skill,
|
||||
Category::Data,
|
||||
];
|
||||
|
||||
// ── legacy W6 scaffolding (retained: registered components) ──────────────
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SystemComponent {
|
||||
pub name: String,
|
||||
pub component_type: String,
|
||||
@@ -11,13 +129,13 @@ pub struct SystemComponent {
|
||||
}
|
||||
|
||||
pub struct SystemRegistry {
|
||||
pub components: BTreeMap<String, SystemComponent>,
|
||||
pub components: std::collections::BTreeMap<String, SystemComponent>,
|
||||
}
|
||||
|
||||
impl SystemRegistry {
|
||||
pub fn new() -> Self {
|
||||
SystemRegistry {
|
||||
components: BTreeMap::new(),
|
||||
components: std::collections::BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn register(&mut self, name: &str, ctype: &str, location: &str, desc: &str) {
|
||||
@@ -39,6 +157,752 @@ impl Default for SystemRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// scanDiff — the pure change-detection core of scan_and_register: given
|
||||
/// current file rows and the stored registry, classify each file as
|
||||
/// modified (hash differs) or new (absent).
|
||||
pub fn scan_diff(files: &[Value], registry: &Value) -> Vec<Value> {
|
||||
let mut changes = Vec::new();
|
||||
for f in files {
|
||||
let name = f.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let fhash = f.get("hash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let fmod = f.get("modified").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let existing = registry.get(name);
|
||||
match existing {
|
||||
Some(e) => {
|
||||
let ehash = e.get("hash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if ehash != fhash {
|
||||
let emod = e.get("modified").and_then(|v| v.as_str()).unwrap_or("");
|
||||
changes.push(json!({
|
||||
"file": name, "type": "modified",
|
||||
"old_hash": ehash, "new_hash": fhash,
|
||||
"old_modified": emod, "new_modified": fmod,
|
||||
}));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
changes.push(json!({
|
||||
"file": name, "type": "new", "hash": fhash, "modified": fmod,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
changes
|
||||
}
|
||||
|
||||
// ── Python-oracle parity helpers ─────────────────────────────────────────
|
||||
|
||||
fn env_dir(env: &Value, key: &str) -> String {
|
||||
env.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn dir_join(dir: &str, name: &str) -> String {
|
||||
if dir.ends_with('/') {
|
||||
format!("{}{}", dir, name)
|
||||
} else {
|
||||
format!("{}/{}", dir, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// sorted(is.listdir(dir)) restricted to regular files.
|
||||
fn sorted_file_names(dir: &str) -> Vec<String> {
|
||||
let mut names: Vec<String> = match fs::read_dir(dir) {
|
||||
Ok(rd) => rd
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
names.sort();
|
||||
names
|
||||
.into_iter()
|
||||
.filter(|n| Path::new(&dir_join(dir, n)).is_file())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn epoch_parts(md: &fs::Metadata) -> (i64, u32) {
|
||||
match md.modified().expect("mtime unavailable") {
|
||||
t if t < UNIX_EPOCH => {
|
||||
let d = UNIX_EPOCH.duration_since(t).expect("time went backwards");
|
||||
let mut secs = -(d.as_secs() as i64);
|
||||
let mut nanos = d.subsec_nanos();
|
||||
if nanos > 0 {
|
||||
secs -= 1;
|
||||
nanos = 1_000_000_000 - nanos;
|
||||
}
|
||||
(secs, nanos)
|
||||
}
|
||||
t => {
|
||||
let d = t.duration_since(UNIX_EPOCH).expect("time went backwards");
|
||||
(d.as_secs() as i64, d.subsec_nanos())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Howard Hinnant's civil_from_days: days since 1970-01-01 → (y, m, d).
|
||||
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
let z = z + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||
let doe = z - era * 146_097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
||||
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
|
||||
(y + if m <= 2 { 1 } else { 0 }, m, d)
|
||||
}
|
||||
|
||||
/// datetime.fromtimestamp(mtime, tz=utc).isoformat() parity: float-double
|
||||
/// semantics (secs + nanos/1e9 → f64 → ties-even µs rounding), then
|
||||
/// RFC3339 with "+00:00" and a fraction only when µs ≠ 0.
|
||||
pub fn iso_utc(secs: i64, nanos: u32) -> String {
|
||||
let m = secs as f64 + nanos as f64 / 1e9;
|
||||
let flo = m.floor();
|
||||
let mut micro = {
|
||||
let t = (m - flo) * 1e6;
|
||||
let base = t.floor();
|
||||
let diff = t - base;
|
||||
// round half to even, mirroring CPython's µs conversion
|
||||
let r = if diff > 0.5 || (diff == 0.5 && (base as i64) % 2 != 0) {
|
||||
base + 1.0
|
||||
} else {
|
||||
base
|
||||
};
|
||||
r as i64
|
||||
};
|
||||
let mut secs = flo as i64;
|
||||
if micro >= 1_000_000 {
|
||||
micro -= 1_000_000;
|
||||
secs += 1;
|
||||
}
|
||||
let (y, mo, d) = civil_from_days(secs.div_euclid(86_400));
|
||||
let sod = secs.rem_euclid(86_400);
|
||||
let (h, mi, s) = (
|
||||
(sod / 3600) as u32,
|
||||
((sod % 3600) / 60) as u32,
|
||||
(sod % 60) as u32,
|
||||
);
|
||||
if micro > 0 {
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}+00:00",
|
||||
y, mo, d, h, mi, s, micro
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+00:00",
|
||||
y, mo, d, h, mi, s
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── ported functions (name-preserving) ───────────────────────────────────
|
||||
|
||||
/// load_settings port: defaults, overridden by the settings file (dict
|
||||
/// update semantics — file keys keep their file order but sit behind the
|
||||
/// defaults they override; unknown file keys append at the end).
|
||||
pub fn load_settings(settings_file: &str) -> Value {
|
||||
let mut defaults = Map::new();
|
||||
for (k, v) in [
|
||||
("mode", json!("dev")),
|
||||
("auto_scan", json!(true)),
|
||||
("verbose_logging", json!(true)),
|
||||
("experimental_features", json!(true)),
|
||||
("scan_on_startup", json!(true)),
|
||||
("track_changes", json!(true)),
|
||||
("notify_on_file_change", json!(true)),
|
||||
] {
|
||||
defaults.insert(k.to_string(), v);
|
||||
}
|
||||
if let Ok(text) = fs::read_to_string(settings_file) {
|
||||
if let Ok(settings) = serde_json::from_str::<Value>(&text) {
|
||||
if let Some(obj) = settings.as_object() {
|
||||
for (k, v) in obj {
|
||||
defaults.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Object(defaults)
|
||||
}
|
||||
|
||||
pub fn save_settings(settings_file: &str, settings: &Value) {
|
||||
if let Some(parent) = Path::new(settings_file).parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
fs::write(settings_file, py_json_pretty(settings)).expect("save_settings write failed");
|
||||
}
|
||||
|
||||
pub fn is_dev_mode(settings_file: &str) -> bool {
|
||||
load_settings(settings_file)
|
||||
.get("mode")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("dev")
|
||||
== "dev"
|
||||
}
|
||||
|
||||
/// file_hash port: SHA256 of file contents, first 16 hex chars; None on
|
||||
/// any read failure (best-effort degrade, matching the oracle).
|
||||
pub fn file_hash(path: &str) -> Option<String> {
|
||||
let bytes = fs::read(path).ok()?;
|
||||
let mut h = Sha256::new();
|
||||
h.update(&bytes);
|
||||
Some(format!("{:x}", h.finalize())[..16].to_string())
|
||||
}
|
||||
|
||||
pub fn file_modified(path: &str) -> Option<String> {
|
||||
let md = fs::metadata(path).ok()?;
|
||||
let (secs, nanos) = epoch_parts(&md);
|
||||
Some(iso_utc(secs, nanos))
|
||||
}
|
||||
|
||||
fn file_row(path: &str, name: &str, category: &str) -> Value {
|
||||
let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||
json!({
|
||||
"path": path,
|
||||
"name": name,
|
||||
"category": category,
|
||||
"modified": file_modified(path),
|
||||
"hash": file_hash(path),
|
||||
"size": size,
|
||||
})
|
||||
}
|
||||
|
||||
/// discover_files port: env carries the five roots
|
||||
/// (scripts_dir, plugin_dir, systemd_dir, config_dir, memory_dir).
|
||||
pub fn discover_files(env: &Value) -> Vec<Value> {
|
||||
let mut files = Vec::new();
|
||||
let scripts_dir = &env_dir(env, "scripts_dir");
|
||||
if Path::new(scripts_dir).is_dir() {
|
||||
for name in sorted_file_names(scripts_dir) {
|
||||
let ext = name
|
||||
.rfind('.')
|
||||
.filter(|i| *i > 0)
|
||||
.map(|i| &name[i..])
|
||||
.unwrap_or("");
|
||||
let cat = Category::for_scripts_ext(ext);
|
||||
files.push(file_row(&dir_join(scripts_dir, &name), &name, cat.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
let plugin_dir = &env_dir(env, "plugin_dir");
|
||||
if Path::new(plugin_dir).is_dir() {
|
||||
for name in sorted_file_names(plugin_dir) {
|
||||
files.push(file_row(
|
||||
&dir_join(plugin_dir, &name),
|
||||
&name,
|
||||
Category::Plugin.as_str(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let systemd_dir = &env_dir(env, "systemd_dir");
|
||||
if Path::new(systemd_dir).is_dir() {
|
||||
for name in sorted_file_names(systemd_dir) {
|
||||
if let Some(cat) = Category::for_systemd_name(&name) {
|
||||
let path = dir_join(systemd_dir, &name);
|
||||
files.push(file_row(&path, &name, cat.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config_dir = &env_dir(env, "config_dir");
|
||||
for f in CONFIG_FILE_NAMES {
|
||||
let path = dir_join(config_dir, f);
|
||||
if Path::new(&path).exists() {
|
||||
files.push(file_row(&path, f, Category::Config.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
let memory_dir = &env_dir(env, "memory_dir");
|
||||
for f in MEMORY_FILE_NAMES {
|
||||
let path = dir_join(memory_dir, f);
|
||||
if Path::new(&path).exists() {
|
||||
files.push(file_row(&path, f, Category::Memory.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
let skills_dir = dir_join(config_dir, "skills");
|
||||
if Path::new(&skills_dir).is_dir() {
|
||||
for name in sorted_file_names(&skills_dir) {
|
||||
files.push(file_row(
|
||||
&dir_join(&skills_dir, &name),
|
||||
&name,
|
||||
Category::Skill.as_str(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
pub fn load_registry(registry_file: &str) -> Value {
|
||||
if let Ok(text) = fs::read_to_string(registry_file) {
|
||||
if let Ok(v) = serde_json::from_str::<Value>(&text) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
json!({})
|
||||
}
|
||||
|
||||
pub fn save_registry(registry_file: &str, registry: &Value) {
|
||||
if let Some(parent) = Path::new(registry_file).parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
fs::write(registry_file, py_json_pretty(registry)).expect("save_registry write failed");
|
||||
}
|
||||
|
||||
/// scan_and_register port: discovery + change detection + registry update
|
||||
/// + persistence.
|
||||
///
|
||||
/// `now` is the RFC3339 last_seen stamp (the Python oracle stamps the
|
||||
/// current UTC time; the facade freezes it so parity is testable).
|
||||
/// Verbose CHANGED/NEW printing stays facade-side.
|
||||
pub fn scan_and_register(env: &Value, registry_file: &str, now: &str) -> Vec<Value> {
|
||||
let files = discover_files(env);
|
||||
let mut registry = load_registry(registry_file);
|
||||
let changes = scan_diff(&files, ®istry);
|
||||
|
||||
if let Some(obj) = registry.as_object_mut() {
|
||||
for f in &files {
|
||||
let name = f
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
obj.insert(
|
||||
name,
|
||||
json!({
|
||||
"path": f.get("path"),
|
||||
"category": f.get("category"),
|
||||
"modified": f.get("modified"),
|
||||
"hash": f.get("hash"),
|
||||
"size": f.get("size"),
|
||||
"last_seen": now,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
save_registry(registry_file, ®istry);
|
||||
changes
|
||||
}
|
||||
|
||||
pub fn get_file_status(registry_file: &str, name: &str) -> Option<Value> {
|
||||
load_registry(registry_file).get(name).cloned()
|
||||
}
|
||||
|
||||
/// diff_since_last_scan port: modified rows keep only
|
||||
/// {file, old_modified, new_modified, category}; new rows carry type=new.
|
||||
pub fn diff_since_last_scan(env: &Value, registry_file: &str) -> Vec<Value> {
|
||||
let registry = load_registry(registry_file);
|
||||
let current = discover_files(env);
|
||||
let mut changes = Vec::new();
|
||||
for f in ¤t {
|
||||
let name = f.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let fhash = f.get("hash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let fmod = f.get("modified").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let cat = f.get("category").and_then(|v| v.as_str()).unwrap_or("");
|
||||
match registry.get(name) {
|
||||
Some(e) => {
|
||||
let ehash = e.get("hash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if ehash != fhash {
|
||||
let emod = e.get("modified").and_then(|v| v.as_str()).unwrap_or("");
|
||||
changes.push(json!({
|
||||
"file": name,
|
||||
"old_modified": emod,
|
||||
"new_modified": fmod,
|
||||
"category": cat,
|
||||
}));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
changes.push(json!({
|
||||
"file": name, "type": "new", "modified": fmod, "category": cat,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
changes
|
||||
}
|
||||
|
||||
/// Python str() rendering of a settings value (report uses "{v}").
|
||||
fn py_str(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Bool(true) => "True".to_string(),
|
||||
Value::Bool(false) => "False".to_string(),
|
||||
Value::Null => "None".to_string(),
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// generate_report port: the exact report text the oracle prints.
|
||||
pub fn generate_report(env: &Value, registry_file: &str, settings_file: &str) -> String {
|
||||
let files = discover_files(env);
|
||||
// the oracle also load_registry()s here, but never observes it — the
|
||||
// load is unobservable, so the port skips it
|
||||
let settings = load_settings(settings_file);
|
||||
let changes = diff_since_last_scan(env, registry_file);
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str("╔══════════════════════════════════════════════════════════════╗\n");
|
||||
out.push_str("║ SYSTEM FILE REGISTRY ║\n");
|
||||
let mode = settings
|
||||
.get("mode")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("dev");
|
||||
out.push_str(&format!(
|
||||
"║ Mode: {:<8} ║\n",
|
||||
mode
|
||||
));
|
||||
out.push_str("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
out.push('\n');
|
||||
|
||||
out.push_str("━━━ SETTINGS ━━━\n");
|
||||
if let Some(obj) = settings.as_object() {
|
||||
for (k, v) in obj {
|
||||
out.push_str(&format!(" {:<25} = {}\n", k, py_str(v)));
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
out.push_str(&format!("━━━ FILES: {} total ━━━\n", files.len()));
|
||||
for cat in REPORT_CATEGORY_ORDER {
|
||||
let cat_files: Vec<&Value> = files
|
||||
.iter()
|
||||
.filter(|f| f.get("category").and_then(|v| v.as_str()) == Some(cat.as_str()))
|
||||
.collect();
|
||||
if cat_files.is_empty() {
|
||||
continue;
|
||||
}
|
||||
out.push_str(&format!(
|
||||
" {} ({}):\n",
|
||||
cat.as_str().to_uppercase(),
|
||||
cat_files.len()
|
||||
));
|
||||
for f in cat_files {
|
||||
let name = f.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let modified = f
|
||||
.get("modified")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let modified: String = modified.chars().take(19).collect();
|
||||
use std::fmt::Write as _;
|
||||
let _ = writeln!(out, " {name:<30} {modified}");
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
if !changes.is_empty() {
|
||||
out.push_str(&format!(
|
||||
"━━━ CHANGES: {} since last scan ━━━\n",
|
||||
changes.len()
|
||||
));
|
||||
for c in &changes {
|
||||
let file = c.get("file").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let cat = c.get("category").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
if c.get("type").and_then(|v| v.as_str()) == Some("new") {
|
||||
out.push_str(&format!(" NEW: {} ({})\n", file, cat));
|
||||
} else {
|
||||
out.push_str(&format!(" MODIFIED: {} ({})\n", file, cat));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push_str("━━━ CHANGES: none since last scan ━━━\n");
|
||||
}
|
||||
out.push('\n');
|
||||
out
|
||||
}
|
||||
|
||||
/// handle_hook port.
|
||||
pub fn handle_hook(event: &str) -> Value {
|
||||
json!({"status": "ok", "event": event, "handler": "system_registry"})
|
||||
}
|
||||
|
||||
// ── Python json.dump(indent=2)-compatible serialization ──────────────────
|
||||
// serde_json would differ in two ways: non-ASCII is not escaped
|
||||
// (Python's ensure_ascii=True emits \uXXXX) and object key order is
|
||||
// insertion-based (handled via preserve_order). This writer reproduces
|
||||
// json.dump byte-for-byte for the value domain in play.
|
||||
|
||||
fn py_escape_into(s: &str, out: &mut String) {
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{08}' => out.push_str("\\b"),
|
||||
'\u{0c}' => out.push_str("\\f"),
|
||||
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c if (c as u32) >= 0x80 => {
|
||||
let cp = c as u32;
|
||||
if cp > 0xFFFF {
|
||||
let v = cp - 0x10000;
|
||||
let hi = 0xD800 + (v >> 10);
|
||||
let lo = 0xDC00 + (v & 0x3FF);
|
||||
out.push_str(&format!("\\u{:04x}\\u{:04x}", hi, lo));
|
||||
} else {
|
||||
out.push_str(&format!("\\u{:04x}", cp));
|
||||
}
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
fn py_write(v: &Value, indent: usize, out: &mut String) {
|
||||
match v {
|
||||
Value::Object(m) if !m.is_empty() => {
|
||||
out.push_str("{\n");
|
||||
for (i, (k, val)) in m.iter().enumerate() {
|
||||
out.push_str(&" ".repeat(indent + 1));
|
||||
py_escape_into(k, out);
|
||||
out.push_str(": ");
|
||||
py_write(val, indent + 1, out);
|
||||
if i + 1 < m.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(&" ".repeat(indent));
|
||||
out.push('}');
|
||||
}
|
||||
Value::Object(_) => out.push_str("{}"),
|
||||
Value::Array(a) if !a.is_empty() => {
|
||||
out.push_str("[\n");
|
||||
for (i, val) in a.iter().enumerate() {
|
||||
out.push_str(&" ".repeat(indent + 1));
|
||||
py_write(val, indent + 1, out);
|
||||
if i + 1 < a.len() {
|
||||
out.push(',');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(&" ".repeat(indent));
|
||||
out.push(']');
|
||||
}
|
||||
Value::Array(_) => out.push_str("[]"),
|
||||
Value::String(s) => py_escape_into(s, out),
|
||||
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
||||
Value::Number(n) => out.push_str(&n.to_string()),
|
||||
Value::Null => out.push_str("null"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn py_json_pretty(v: &Value) -> String {
|
||||
let mut s = String::new();
|
||||
py_write(v, 0, &mut s);
|
||||
s
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope) ───────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// sr_load_settings({"settings_file"}) -> settings object
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_load_settings(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let f = a
|
||||
.get("settings_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("settings_file required")?;
|
||||
Ok(load_settings(f))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_save_settings({"settings_file", "settings"}) -> null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_save_settings(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let f = a
|
||||
.get("settings_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("settings_file required")?;
|
||||
let s = a.get("settings").cloned().ok_or("settings required")?;
|
||||
save_settings(f, &s);
|
||||
Ok(Value::Null)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_is_dev_mode({"settings_file"}) -> bool
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_is_dev_mode(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let f = a
|
||||
.get("settings_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("settings_file required")?;
|
||||
Ok(json!(is_dev_mode(f)))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_file_hash({"path"}) -> hex16 | null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_file_hash(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let p = a
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("path required")?;
|
||||
Ok(file_hash(p).map(Value::String).unwrap_or(Value::Null))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_file_modified({"path"}) -> iso | null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_file_modified(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let p = a
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("path required")?;
|
||||
Ok(file_modified(p).map(Value::String).unwrap_or(Value::Null))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_discover_files({"env"}) -> file rows
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_discover_files(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let env = a.get("env").cloned().ok_or("env required")?;
|
||||
Ok(Value::Array(discover_files(&env)))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_load_registry({"registry_file"}) -> registry object
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_load_registry(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let f = a
|
||||
.get("registry_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("registry_file required")?;
|
||||
Ok(load_registry(f))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_save_registry({"registry_file", "registry"}) -> null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_save_registry(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let f = a
|
||||
.get("registry_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("registry_file required")?;
|
||||
let reg = a.get("registry").cloned().ok_or("registry required")?;
|
||||
save_registry(f, ®);
|
||||
Ok(Value::Null)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_scan_and_register({"env", "registry_file", "now"}) -> changes
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_scan_and_register(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let env = a.get("env").cloned().ok_or("env required")?;
|
||||
let f = a
|
||||
.get("registry_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("registry_file required")?;
|
||||
let now = a.get("now").and_then(|v| v.as_str()).unwrap_or("");
|
||||
Ok(Value::Array(scan_and_register(&env, f, now)))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_get_file_status({"registry_file", "name"}) -> entry | null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_get_file_status(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let f = a
|
||||
.get("registry_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("registry_file required")?;
|
||||
let n = a
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("name required")?;
|
||||
Ok(get_file_status(f, n).unwrap_or(Value::Null))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_diff_since_last_scan({"env", "registry_file"}) -> changes
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_diff_since_last_scan(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let env = a.get("env").cloned().ok_or("env required")?;
|
||||
let f = a
|
||||
.get("registry_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("registry_file required")?;
|
||||
Ok(Value::Array(diff_since_last_scan(&env, f)))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_generate_report({"env", "registry_file", "settings_file"}) -> report text
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_generate_report(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let env = a.get("env").cloned().ok_or("env required")?;
|
||||
let f = a
|
||||
.get("registry_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("registry_file required")?;
|
||||
let s = a
|
||||
.get("settings_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("settings_file required")?;
|
||||
Ok(Value::String(generate_report(&env, f, s)))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sr_handle_hook({"event"}) -> {status, event, handler}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sr_handle_hook(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let e = a.get("event").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
handle_hook(e)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -54,47 +918,6 @@ mod tests {
|
||||
);
|
||||
assert!(reg.components.contains_key("event_bus"));
|
||||
}
|
||||
}
|
||||
|
||||
/// scanDiff — the pure change-detection core of scan_and_register (batch
|
||||
/// 153): given current file rows and the stored registry, classify each
|
||||
/// file as modified (hash differs) or new (absent). File discovery, hashing,
|
||||
/// and disk persistence stay Python (I/O — reactive surface).
|
||||
pub fn scan_diff(
|
||||
files: &[serde_json::Value],
|
||||
registry: &serde_json::Value,
|
||||
) -> Vec<serde_json::Value> {
|
||||
let mut changes = Vec::new();
|
||||
for f in files {
|
||||
let name = f.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let fhash = f.get("hash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let fmod = f.get("modified").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let existing = registry.get(name);
|
||||
match existing {
|
||||
Some(e) => {
|
||||
let ehash = e.get("hash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if ehash != fhash {
|
||||
let emod = e.get("modified").and_then(|v| v.as_str()).unwrap_or("");
|
||||
changes.push(serde_json::json!({
|
||||
"file": name, "type": "modified",
|
||||
"old_hash": ehash, "new_hash": fhash,
|
||||
"old_modified": emod, "new_modified": fmod,
|
||||
}));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
changes.push(serde_json::json!({
|
||||
"file": name, "type": "new", "hash": fhash, "modified": fmod,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
changes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod scan_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scan_diff_parity() {
|
||||
@@ -128,4 +951,22 @@ mod scan_battery {
|
||||
let got3 = scan_diff(files.as_array().unwrap(), ®_full);
|
||||
assert!(got3.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso_utc_python_parity() {
|
||||
// frozen-oracle probes: file_modified renders these exactly
|
||||
assert_eq!(iso_utc(1_700_000_000, 0), "2023-11-14T22:13:20+00:00");
|
||||
assert_eq!(
|
||||
iso_utc(1_234_567_890, 123_456_000),
|
||||
"2009-02-13T23:31:30.123456+00:00"
|
||||
);
|
||||
assert_eq!(
|
||||
iso_utc(1_500_000_001, 500_000_000),
|
||||
"2017-07-14T02:40:01.500000+00:00"
|
||||
);
|
||||
assert_eq!(
|
||||
iso_utc(1_600_000_000, 999_999_000),
|
||||
"2020-09-13T12:26:40.999999+00:00"
|
||||
);
|
||||
}
|
||||
}
|
||||
+126229
File diff suppressed because it is too large.
Load diff
+306
-266
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,96 @@
|
||||
"""Tests: contracts.py Rust runner-core port (dichotomy wave, #2479).
|
||||
|
||||
Delegation + fallback-pin discipline: the verdict and drift compare come
|
||||
from the Rust core when available; RUNE_CONTRACTS_CORE=python pins the
|
||||
Python oracle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import contracts as ct # noqa: E402
|
||||
from rune import contracts_loader # noqa: E402
|
||||
|
||||
CASE = ct.Contract(
|
||||
name="wiring: type mismatch probe",
|
||||
module="json",
|
||||
function="dumps",
|
||||
args=("x",),
|
||||
kwargs={},
|
||||
expect_type=dict,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def core_built():
|
||||
if not contracts_loader.available():
|
||||
pytest.skip("rust contracts core not built")
|
||||
return True
|
||||
|
||||
|
||||
def _fields(r):
|
||||
return (r.name, r.passed, r.error, r.actual_type, r.actual_value)
|
||||
|
||||
|
||||
def test_rust_core_delegates_verdict(core_built, monkeypatch):
|
||||
monkeypatch.delenv("RUNE_CONTRACTS_CORE", raising=False)
|
||||
rust = ct.run_contract(CASE)
|
||||
assert rust.passed is False
|
||||
assert rust.error == "expected dict, got str"
|
||||
assert rust.actual_type == "str"
|
||||
|
||||
|
||||
def test_python_pin_fallback(core_built, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_CONTRACTS_CORE", "python")
|
||||
pinned = ct.run_contract(CASE)
|
||||
assert _fields(pinned) == _fields(ct._run_contract_python(CASE))
|
||||
|
||||
|
||||
def test_pin_makes_core_probe_none(core_built, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_CONTRACTS_CORE", "python")
|
||||
assert contracts_loader.core() is None
|
||||
assert ct._ct_core() is None
|
||||
monkeypatch.delenv("RUNE_CONTRACTS_CORE", raising=False)
|
||||
assert ct._ct_core() is not None
|
||||
|
||||
|
||||
def test_drift_delegation_matches_oracle(core_built, monkeypatch):
|
||||
monkeypatch.delenv("RUNE_CONTRACTS_CORE", raising=False)
|
||||
rust = ct.check_signature_drift()
|
||||
monkeypatch.setenv("RUNE_CONTRACTS_CORE", "python")
|
||||
pinned = ct.check_signature_drift()
|
||||
assert rust == pinned
|
||||
|
||||
|
||||
def test_registry_matches_live_contracts(core_built, monkeypatch):
|
||||
monkeypatch.delenv("RUNE_CONTRACTS_CORE", raising=False)
|
||||
reg = contracts_loader.registry()
|
||||
assert len(reg) == len(ct.CONTRACTS)
|
||||
for spec, c in zip(reg, ct.CONTRACTS):
|
||||
assert spec["name"] == c.name
|
||||
assert spec["module"] == c.module
|
||||
assert spec["function"] == c.function
|
||||
assert spec["expect_not_none"] == c.expect_not_none
|
||||
assert spec["expect_callable"] == c.expect_callable
|
||||
|
||||
|
||||
def test_kernel_failure_falls_back_to_oracle(core_built, monkeypatch):
|
||||
"""A broken core (bad payload path) must degrade to the Python oracle,
|
||||
never crash the contract run."""
|
||||
monkeypatch.delenv("RUNE_CONTRACTS_CORE", raising=False)
|
||||
|
||||
class Boom:
|
||||
def verdict(self, payload):
|
||||
raise RuntimeError("simulated kernel failure")
|
||||
|
||||
monkeypatch.setattr(ct, "_ct_core", lambda: Boom())
|
||||
r = ct.run_contract(CASE)
|
||||
assert _fields(r) == _fields(ct._run_contract_python(CASE))
|
||||
@@ -6,6 +6,7 @@ the test SKIPS rather than falsifying coverage — the henxel rule is satisfied
|
||||
by the reference, and desktop runs exercise the full 41-check battery.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -24,8 +25,10 @@ CDYLIB = (
|
||||
reason="query_memory cdylib not built in this environment (CI has no cargo step)",
|
||||
)
|
||||
def test_ffi_query_memory_smoke_green():
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = str(ROOT / "src") + os.pathsep + env.get("PYTHONPATH", "")
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(SMOKE)], capture_output=True, text=True, timeout=120
|
||||
[sys.executable, str(SMOKE)], capture_output=True, text=True, timeout=120, env=env
|
||||
)
|
||||
assert r.returncode == 0, r.stdout[-500:] + r.stderr[-500:]
|
||||
assert "41/41 PASS" in r.stdout
|
||||
@@ -1499,3 +1499,43 @@ def test_backfill_runs_once_at_migration_not_every_connect(tmp_path):
|
||||
assert row["memory_type"] == "personality", "explicit classifications are never overwritten"
|
||||
finally:
|
||||
db2.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dichotomy wave: memory_selector FULL Rust core — fallback pin discipline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_mse_core_python_pin_honored(monkeypatch, _isolated_memory_env):
|
||||
"""RUNE_MSE_CORE=python pins the fallback: the probe returns None and the
|
||||
facade exercises the PYTHON oracle path (degrade-not-crash preserved)."""
|
||||
from rune import mse_loader
|
||||
from rune.memory_selector import MemorySelector, SelectionResult, _mse_core
|
||||
|
||||
monkeypatch.setenv("RUNE_MSE_CORE", "python")
|
||||
assert mse_loader.core() is None, "the pin must be honored in the loader probe"
|
||||
assert _mse_core() is None, "the facade must see the pinned-off core"
|
||||
sel = MemorySelector(db_path=ms.DB_PATH)
|
||||
result = sel.select_for_task("kubernetes pods restart")
|
||||
assert isinstance(result, SelectionResult), "pinned fallback still selects"
|
||||
|
||||
|
||||
def test_mse_core_delegates_when_unpinned(monkeypatch, _isolated_memory_env):
|
||||
"""Without the pin, the facade delegates the decision math to the Rust
|
||||
core — and the end-to-end outcome stays identical to the oracle's."""
|
||||
from rune import mse_loader
|
||||
from rune.memory_selector import MemorySelector, _mse_core
|
||||
|
||||
if not mse_loader.available():
|
||||
pytest.skip("memory_selector cdylib not built in this environment")
|
||||
monkeypatch.delenv("RUNE_MSE_CORE", raising=False)
|
||||
_no_embed(monkeypatch)
|
||||
assert _mse_core() is not None, "unpinned core must load"
|
||||
db = ms.connect()
|
||||
try:
|
||||
_add(db, "Kubernetes pods restart automatically on failure")
|
||||
finally:
|
||||
db.close()
|
||||
sel = MemorySelector(db_path=ms.DB_PATH)
|
||||
result = sel.select_for_task("kubernetes pods restart")
|
||||
assert result.abstained is False
|
||||
assert result.candidates_evaluated > 0
|
||||
Reference in new issue
Block a user