Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f82fed98fb | ||
|
|
55ad089c3f | ||
|
|
c8426bdddb | ||
|
|
27716e0243 | ||
|
|
caeaab3c1a | ||
|
|
a95c6ce129 | ||
|
|
9efb7d8697 |
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.196.0**
|
||||
**Current release: v0.199.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: 25%** (620/2444 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 | 11 | 2175 | 80%+ of functions ported |
|
||||
| PARTIAL | 81 | 39948 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 122 | 30041 | no name-preserving implementation |
|
||||
| Total runtime | 214 | 72164 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -17,19 +17,15 @@ _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 |
|
||||
| spine | 965 | 35 | 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 +38,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,14 +63,13 @@ _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 |
|
||||
@@ -84,10 +78,13 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| inference_trace | 164 | 6 | RUST |
|
||||
| ms_loader | 163 | 16 | RUST |
|
||||
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
|
||||
| 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 |
|
||||
@@ -95,7 +92,6 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| 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 |
|
||||
@@ -104,13 +100,25 @@ _Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL
|
||||
| concept_topology | 90 | 7 | unclassified |
|
||||
| cs_loader | 90 | 5 | RUST |
|
||||
| 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 +131,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 +142,46 @@ _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 |
|
||||
| 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 |
|
||||
| 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 +199,43 @@ _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 |
|
||||
|---|---|---|---|
|
||||
| event_wait | 518 | 14/14 | GO |
|
||||
| tool_protocol | 461 | 22/27 | 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 |
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.196.0"
|
||||
version = "0.199.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,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,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.196.0"
|
||||
__version__ = "0.199.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.196.0"' in rune_version,
|
||||
'__version__ = "0.199.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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.196.0
|
||||
Version: v0.199.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.196.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.199.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,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"),
|
||||
|
||||
Reference in new issue
Block a user