Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf3575fb3 | ||
|
|
2e16fc4c89 | ||
|
|
12303bbd20 | ||
|
|
de30cd958e | ||
|
|
fd6e9b4bca | ||
|
|
24413e4228 | ||
|
|
407c23a689 | ||
|
|
982a9b07d5 | ||
|
|
ff506ebe55 | ||
|
|
2032819d99 | ||
|
|
e2464ae9d6 | ||
|
|
d7e04f1098 | ||
|
|
e639383c49 | ||
|
|
57a274d97d | ||
|
|
aac5295c24 | ||
|
|
9563148f9d | ||
|
|
76984c6362 | ||
|
|
4072d36b1a | ||
|
|
6d871abc9c | ||
|
|
ce40acbb1f | ||
|
|
1de28b55a0 | ||
|
|
75d6d7d62a | ||
|
|
7f47969128 | ||
|
|
a65e5d44de | ||
|
|
26a040892d | ||
|
|
1497c85cd4 | ||
|
|
a2463ad80b | ||
|
|
6965fc7375 | ||
|
|
0daad967c1 | ||
|
|
502317d8b6 | ||
|
|
07cdc2fc56 | ||
|
|
b4161a4034 | ||
|
|
e036f5f545 | ||
|
|
ab6c4f4e38 | ||
|
|
c520b024d1 | ||
|
|
1c73d809b9 | ||
|
|
34fc38d4ed | ||
|
|
a67a9c8520 | ||
|
|
b0efaffca1 | ||
|
|
a087a0b5e7 | ||
|
|
acb3ce0157 | ||
|
|
58af0b0f78 | ||
|
|
95f9e15ed9 | ||
|
|
b083b0c4cf | ||
|
|
46e74110ad | ||
|
|
c669e37a3d | ||
|
|
7f4a6262ae | ||
|
|
34d5ab74b7 | ||
|
|
7b268e17fe |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.14**
|
||||
**Current release: v0.66.31**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
+233
-220
@@ -1,223 +1,236 @@
|
||||
{
|
||||
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
|
||||
"umbrellas": {
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"memory_store",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"shard_manager",
|
||||
"query_memory",
|
||||
"graph_memory",
|
||||
"warm_neuron_store",
|
||||
"abstraction",
|
||||
"cache",
|
||||
"recall_router",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"tier_manager",
|
||||
"graph_shard_bridge",
|
||||
"memory_selector",
|
||||
"retrieval_benchmark"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_query_memory",
|
||||
"canary_memory_synthesis",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"drift_core",
|
||||
"drift_observability",
|
||||
"drift_ledger",
|
||||
"audit_ledger",
|
||||
"curator",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"build_pipeline_policy",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
],
|
||||
"depends_on": []
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"identity_anchors",
|
||||
"voice_hybrid",
|
||||
"voice_sync",
|
||||
"voicebox",
|
||||
"voice_injector",
|
||||
"behavior",
|
||||
"prompt_sync"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"self_improve",
|
||||
"subconscious_pass",
|
||||
"goal",
|
||||
"elastic_cognition",
|
||||
"growth_delta",
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"growth_simulator"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory",
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"witness",
|
||||
"salience_gate",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"capability_broker",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"local_tool_runtime",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
"connection_pool",
|
||||
"adaptive_router",
|
||||
"route_risk_flags",
|
||||
"evidence_probe",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"recall_uplift"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_01",
|
||||
"canary_p0_05",
|
||||
"canary_native"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"model_routing",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_connect",
|
||||
"provider_registry",
|
||||
"provider_budget",
|
||||
"standardized_providers",
|
||||
"inference_trace",
|
||||
"response_adapter",
|
||||
"token_optimizer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_06",
|
||||
"canary_p1_02",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"rune_operator",
|
||||
"rune_tui",
|
||||
"rune_graphic",
|
||||
"rune_spinner",
|
||||
"rune_profiler",
|
||||
"chat_bubble",
|
||||
"message_widgets",
|
||||
"tui_validator",
|
||||
"microkernel",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"hook_engine",
|
||||
"system_registry",
|
||||
"cron_engine",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"work_manager",
|
||||
"parallel_processor",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"night_mode",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"verification_layer",
|
||||
"outcome_ledger",
|
||||
"warm_router",
|
||||
"subsystem_health",
|
||||
"build_mode",
|
||||
"dependency_tracker",
|
||||
"auto_detect",
|
||||
"build_orchestrator",
|
||||
"custodian",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"umbrella_update"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_07",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_tui_drift",
|
||||
"canary_cognitive_strata",
|
||||
"canary_surface_parity"
|
||||
],
|
||||
"depends_on": [
|
||||
"perception",
|
||||
"capability",
|
||||
"inference",
|
||||
"identity",
|
||||
"growth"
|
||||
]
|
||||
}
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-12T16:02:18Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"adaptive_router",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"evidence_probe",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"openai_compat_transport",
|
||||
"recall_uplift",
|
||||
"route_risk_flags",
|
||||
"tool_executor",
|
||||
"tool_protocol"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"build_pipeline_policy",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"curator",
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"elastic_cognition",
|
||||
"goal",
|
||||
"growth_delta",
|
||||
"growth_simulator",
|
||||
"self_improve",
|
||||
"subconscious_pass"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"prompt_sync",
|
||||
"voice_hybrid",
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"inference_trace",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_routing",
|
||||
"provider_budget",
|
||||
"provider_registry",
|
||||
"response_adapter",
|
||||
"standardized_providers",
|
||||
"token_optimizer"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"abstraction",
|
||||
"cache",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"graph_memory",
|
||||
"graph_shard_bridge",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"memory_selector",
|
||||
"memory_store",
|
||||
"query_memory",
|
||||
"recall_router",
|
||||
"retrieval_benchmark",
|
||||
"shard_manager",
|
||||
"tier_manager",
|
||||
"warm_neuron_store"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"auto_detect",
|
||||
"build_mode",
|
||||
"build_orchestrator",
|
||||
"chat_bubble",
|
||||
"cron_engine",
|
||||
"custodian",
|
||||
"dependency_tracker",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"hook_engine",
|
||||
"message_widgets",
|
||||
"microkernel",
|
||||
"night_mode",
|
||||
"outcome_ledger",
|
||||
"parallel_processor",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"rune_graphic",
|
||||
"rune_operator",
|
||||
"rune_profiler",
|
||||
"rune_spinner",
|
||||
"rune_tui",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"subsystem_health",
|
||||
"system_registry",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"tui_validator",
|
||||
"umbrella_update",
|
||||
"verification_layer",
|
||||
"warm_router",
|
||||
"work_manager"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"salience_gate",
|
||||
"spine",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
|
||||
"umbrellas": {
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"memory_store",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"shard_manager",
|
||||
"query_memory",
|
||||
"graph_memory",
|
||||
"warm_neuron_store",
|
||||
"abstraction",
|
||||
"cache",
|
||||
"recall_router",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"tier_manager",
|
||||
"graph_shard_bridge",
|
||||
"memory_selector",
|
||||
"retrieval_benchmark"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_query_memory",
|
||||
"canary_memory_synthesis",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"drift_core",
|
||||
"drift_observability",
|
||||
"drift_ledger",
|
||||
"audit_ledger",
|
||||
"curator",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"build_pipeline_policy",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
],
|
||||
"depends_on": []
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"identity_anchors",
|
||||
"voice_hybrid",
|
||||
"voice_sync",
|
||||
"voicebox",
|
||||
"voice_injector",
|
||||
"behavior",
|
||||
"prompt_sync"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"self_improve",
|
||||
"subconscious_pass",
|
||||
"goal",
|
||||
"elastic_cognition",
|
||||
"growth_delta",
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"growth_simulator"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory",
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"witness",
|
||||
"salience_gate",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"capability_broker",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"local_tool_runtime",
|
||||
"intent_continuity",
|
||||
"openai_compat_transport",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
"connection_pool",
|
||||
"adaptive_router",
|
||||
"route_risk_flags",
|
||||
"evidence_probe",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"recall_uplift"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_01",
|
||||
"canary_p0_05",
|
||||
"canary_native"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"model_routing",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_connect",
|
||||
"provider_registry",
|
||||
"provider_budget",
|
||||
"standardized_providers",
|
||||
"inference_trace",
|
||||
"response_adapter",
|
||||
"token_optimizer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_06",
|
||||
"canary_p1_02",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"rune_operator",
|
||||
"rune_tui",
|
||||
"rune_graphic",
|
||||
"rune_spinner",
|
||||
"rune_profiler",
|
||||
"chat_bubble",
|
||||
"message_widgets",
|
||||
"tui_validator",
|
||||
"microkernel",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"hook_engine",
|
||||
"system_registry",
|
||||
"cron_engine",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"work_manager",
|
||||
"parallel_processor",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"night_mode",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"verification_layer",
|
||||
"outcome_ledger",
|
||||
"warm_router",
|
||||
"subsystem_health",
|
||||
"build_mode",
|
||||
"dependency_tracker",
|
||||
"auto_detect",
|
||||
"build_orchestrator",
|
||||
"custodian",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"umbrella_update"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_07",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_tui_drift",
|
||||
"canary_cognitive_strata",
|
||||
"canary_surface_parity"
|
||||
],
|
||||
"depends_on": [
|
||||
"perception",
|
||||
"capability",
|
||||
"inference",
|
||||
"identity",
|
||||
"growth"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,3 +111,31 @@ subsystem): fetches live issue state, compares to
|
||||
- record lacking a required field
|
||||
|
||||
Exit 0 = consistent; exit 1 = drift listed. CI-eligible (read-only).
|
||||
|
||||
|
||||
## PL1 held-out era reconciliation (2026-09-12)
|
||||
|
||||
Fresh-session reconciliation of the ADAPT family against deployed 0.66.22
|
||||
@ 1497c85 + live PL1 held-out evidence (tasks task_2661189c9073,
|
||||
task_6e4885031355; ledger PE-9207213099-0270). Resolves the recorded tension:
|
||||
"ADAPT-LIVE complete → return target PL1" (the bounded tranche authorized
|
||||
2026-09-10 is complete) while numbered ADAPT issues remain open (the larger
|
||||
backlog was never in the ADAPT-LIVE tranche scope).
|
||||
|
||||
| # | state | evidence / basis |
|
||||
|---|---|---|
|
||||
| 284 | VALID_OPEN | epic tracker for the remaining sub-items below |
|
||||
| 292 | FIXED_VERIFIED (closed) | LOCAL_FIX fast path live (rune_operator.py:522,541); live-proven twice by the held-out tasks |
|
||||
| 293 | FIXED_VERIFIED (closed) | DESIGN-LITE implemented (rune_operator.py:660, cites #293), wired to STANDARD_CHANGE; suite+canary green |
|
||||
| 294 | DEFERRED_FUTURE | canonical remaining-obligations list ("ADAPT-5 budgets... for later epics") |
|
||||
| 295 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 296 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 297 | FIXED_VERIFIED (closed) | task_gate implemented (src/rune/task_gate.py) + broker wiring + canary registration |
|
||||
| 299 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 300 | VALID_OPEN | escalation mechanism exists (_probe_and_escalate); fresh deployed acceptance evidence pending |
|
||||
| 301 | DEFERRED_FUTURE | canonical remaining-obligations list (FINAL-12) |
|
||||
| 315 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
|
||||
| 316 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
|
||||
|
||||
Consistency checker: `scripts/gitea_reconciliation_check.py` → exit 0
|
||||
(72 records, 100 live issues in window) after these updates.
|
||||
@@ -54,7 +54,7 @@ This detour is complete when:
|
||||
|
||||
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
|
||||
|
||||
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks must complete-with-evidence or fail honestly). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
|
||||
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED 2026-09-11 → VALIDATED 2026-09-12: two fresh deployed held-out mutation tasks completed-with-evidence — task_2661189c9073 + task_6e4885031355, LOCAL_FIX, Verboo-pinned, mutation + exit-code verification, ledger PE-9207213099-0270; substrate corrections 6965fc7 flat-ChatMessage contract + a2463ad resume transport selection applied between; caveat: corrections developer-applied structurally, not autonomous self-modification; PROMOTED requires repetition/soak). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
|
||||
|
||||
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
|
||||
|
||||
@@ -177,3 +177,13 @@ Root causes (live-proven T15 Part F, 2026-09-11 evening):
|
||||
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
|
||||
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
|
||||
drained, zero delayed duplicates, zero workflow failures.
|
||||
|
||||
## Local CI tooling (developer-side, NOT Rune runtime): W/R-v0.3 reactive wake (2026-09-12)
|
||||
|
||||
The desktop gitea-runner (v3.3.0) now runs `host.post_task_script` = `/usr/local/bin/gitea-wr-wake.sh`
|
||||
after every job: a sub-second fire-and-forget POST to the local W/R daemon (`127.0.0.1:8791/wake`),
|
||||
which sweeps waiting external waits through AUTHORITATIVE Gitea verification (bounded retry for the
|
||||
reporter-ack window; 60s reconciler demoted to fallback). Runner event ≠ success proof. Config:
|
||||
`/etc/gitea-desktop-runner/config.yaml` (backup: config.yaml.pre-wr03). Measured: terminal→verified
|
||||
wait transition ~2s (was 0-30s polling). INTERACTIVE_AUTO_RESUME remains NOT YET SAFE (no supported
|
||||
MiMo busy/idle signal) — headless-only dispatch, unchanged.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Scaffolding V1 — Tranche 1
|
||||
|
||||
_2026-09-12. Scaffolding is the controlled need-to-capability system: NEED →
|
||||
DISCOVERY → GAP → REUSE/ACQUIRE/BUILD → IMPACT+PLAN → SAFE IMPLEMENTATION →
|
||||
VERIFICATION → PROVISIONAL USE → TRUST/REVISE/RETIRE → REUSABLE CAPABILITY.
|
||||
Tranche 1 is deliberately narrower: make Rune stop rediscovering how Rune is
|
||||
built every time Rune changes._
|
||||
|
||||
## Tranche 1 contract
|
||||
|
||||
`scripts/scaffold_projection.py` — a PROJECTION over existing authorities,
|
||||
never a new one:
|
||||
|
||||
| question | authority projected |
|
||||
|---|---|
|
||||
| what subsystem owns X? | `config/umbrellas.json` (umbrella manifest + henxel-enforced completeness) |
|
||||
| what does X depend on? | AST import graph over `src/rune/*.py` (incl. relative/lazy imports) |
|
||||
| who consumes X? | reverse import edges |
|
||||
| what changes if X changes? | mirrors + `depends_on` umbrellas + consumers |
|
||||
| what proof obligations arise? | unit-test file, canary registry, umbrella gates, suite/lint/henxels |
|
||||
| which are satisfied? | filesystem + registry evidence (static); suite/ratchet/henxels are runtime-check commands |
|
||||
|
||||
Commands: `component <module>` · `impact <module>` · `obligations <module>` ·
|
||||
`all [outfile]`. If the projection disagrees with an authority, fix the
|
||||
authority — never the projection.
|
||||
|
||||
## Mechanism audit (2026-09-12, deployed 0.66.22)
|
||||
|
||||
| mechanism | verdict | basis |
|
||||
|---|---|---|
|
||||
| Umbrella manifest + impact map + propagation gate | KEEP | ownership/impact authority; henxel-enforced completeness; consumed by projection |
|
||||
| Henxels structural contract | KEEP | cross-cutting obligations (silent-handler ratchet, registries, coverage debt) |
|
||||
| Canary registry (`canary_native.MODULES` + per-track canaries) | KEEP | per-subsystem proof obligations; consumed by projection |
|
||||
| CI/deploy contract (required checks, TUI startup canary, atomic promote) | KEEP | deployment proof chain live-proven repeatedly |
|
||||
| Version surfaces + auto-version + semver | KEEP | bump fast lane + push convergence working (0.66.22 earned this session) |
|
||||
| Lint/pyright/coverage ratchets | KEEP | shrinking-baseline gates, all green |
|
||||
| Authority map + dead-subnet ledger (D6) | EVOLVE | static doc today; Scaffolding projections make it derivable; D6 retire-disposition still owed |
|
||||
| Issue reconciliation artifact + checker | KEEP | tracker truth with drift detection |
|
||||
| PL1 experience ledger | KEEP | the trust/revise/retire input for future capability crystallisation |
|
||||
| Intent continuity ledger | KEEP | PART 6 state machine, 7 invariants |
|
||||
| `MIMO_AUTOMATIC_RESUME` / W/R interactive injection | KEEP (bounded) | headless single-flight proven; interactive auto-resume NOT YET SAFE until a supported busy signal exists |
|
||||
| Duplicate authorities risk | REPLACE (avoided) | Scaffolding v1 adds NO new registry — projections only |
|
||||
|
||||
## Real-change proof (this tranche)
|
||||
|
||||
Change: context discovery hit Ollama's `/api/show` with cloud-pinned model
|
||||
names — `400 invalid model name` noise on every cloud task + 8192 misbudget
|
||||
for a 131072-window model (observed live in every PL1 Task A/B run).
|
||||
|
||||
1. BEFORE mutation: `scaffold_projection.py impact local_tool_runtime`
|
||||
derived — subsystem `capability`; consumers `rune_operator`; gates:
|
||||
full suite, lint ratchet, henxels, inference+orchestration canaries;
|
||||
unit-test obligation `tests/test_local_tool_runtime.py` (exists).
|
||||
2. Change made: `is_cloud_pinned` — THE provider-prefix rule, single
|
||||
authority in `openai_compat_transport.py`; consumed by context discovery
|
||||
(new) and transport selection (deduped); red-first regression tests.
|
||||
3. Obligations tracked: suite 1314 green · lint ratchet PASS · henxels exit 0
|
||||
· the projection itself caught two gaps mid-flight (relative imports
|
||||
hidden consumers; a broad except biting the lint ratchet) — both fixed at
|
||||
the source the projection pointed to.
|
||||
4. Deploy: normal release chain → deployed identity verified.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
- Projection engine: **UNIT-PROVEN** + proven on one real change.
|
||||
- Not yet: CI integration of `scaffold_projection.py all` (drift detection),
|
||||
capability discovery / gap recognition / reuse-before-build / external
|
||||
acquisition / provisional-trust lifecycle — FOUNDATIONAL ONLY (architecture
|
||||
does not block them; the projection source-list is the extension surface).
|
||||
- Deferred with reasons recorded: none blocked by Tranche 1.
|
||||
@@ -41,8 +41,8 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "B3-v2 Qdrant associative-memory projection spike \u2014 not started; ADAPT-1 sub-item under epic #284"
|
||||
},
|
||||
{
|
||||
"number": 315,
|
||||
@@ -54,14 +54,14 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "B2-v2 wiring-status/architecture-truth report generator \u2014 not started; ADAPT-1 sub-item under epic #284"
|
||||
},
|
||||
{
|
||||
"number": 311,
|
||||
"title": "ADAPT-1 B2 (STANDARD-LIKE): wiring-status/architecture-truth report generator (#158)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"infrastructure",
|
||||
"perception"
|
||||
@@ -74,14 +74,14 @@
|
||||
"number": 301,
|
||||
"title": "FINAL-12: Cross-track benchmark + adaptive provisional ratification",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"labels": [
|
||||
"growth",
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "cross-track benchmark + adaptive provisional ratification \u2014 canonical remaining-obligations list"
|
||||
},
|
||||
{
|
||||
"number": 300,
|
||||
@@ -92,90 +92,90 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "escalation/downgrade honored at acceptance on the deployed runtime \u2014 probe_and_escalate exists; fresh live acceptance evidence pending"
|
||||
},
|
||||
{
|
||||
"number": 299,
|
||||
"title": "ADAPT-10: Harness/context legibility audit",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"labels": [
|
||||
"feature",
|
||||
"governance"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "harness/context legibility audit \u2014 canonical remaining-obligations list"
|
||||
},
|
||||
{
|
||||
"number": 297,
|
||||
"title": "ADAPT-8: Deterministic-first QUALIFY",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"memory"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": ""
|
||||
},
|
||||
{
|
||||
"number": 296,
|
||||
"title": "ADAPT-7: DESIGN consolidation experiment (APPLICABILITY + INNOVATION)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"labels": [
|
||||
"memory"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "DESIGN consolidation experiment (APPLICABILITY+INNOVATION) \u2014 canonical remaining-obligations list"
|
||||
},
|
||||
{
|
||||
"number": 295,
|
||||
"title": "ADAPT-6: Evidence capsule / context minimization",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"labels": [],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "evidence capsule / context minimization \u2014 canonical remaining-obligations list"
|
||||
},
|
||||
{
|
||||
"number": 294,
|
||||
"title": "ADAPT-5: Bound RESEARCHED_CHANGE work (budgets + stop conditions)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"labels": [
|
||||
"feature"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "budgets + stop conditions for RESEARCHED_CHANGE \u2014 canonical 'remaining ADAPT obligations for later epics'"
|
||||
},
|
||||
{
|
||||
"number": 293,
|
||||
"title": "ADAPT-4: STANDARD_CHANGE path (DESIGN-LITE)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"feature"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": ""
|
||||
},
|
||||
{
|
||||
"number": 292,
|
||||
"title": "ADAPT-3: LOCAL_FIX fast path",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": ""
|
||||
},
|
||||
{
|
||||
@@ -190,8 +190,8 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-12",
|
||||
"next_action": "epic tracker for remaining ADAPT sub-items (#294 #295 #296 #299 #300 #301 #315 #316)"
|
||||
},
|
||||
{
|
||||
"number": 259,
|
||||
@@ -1150,5 +1150,7 @@
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"last_reconciled": "2026-09-12",
|
||||
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
def broken(:
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
settings:
|
||||
warn_about_similar_files:
|
||||
above: 0.85
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**"]
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**"]
|
||||
|
||||
henxels:
|
||||
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.14"
|
||||
version = "0.66.31"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"errorCount": 120,
|
||||
"warningCount": 465,
|
||||
"informationCount": 0
|
||||
"errorCount": 124,
|
||||
"warningCount": 429,
|
||||
"informationCount": 0
|
||||
}
|
||||
Whitespace-only changes.
@@ -77,9 +77,17 @@ def _extract_keywords(text: str) -> list[str]:
|
||||
|
||||
|
||||
def _load_manifest() -> dict:
|
||||
"""Structural facts from SPINE (the single structural authority).
|
||||
|
||||
Phase D: reads the Spine-generated compatibility view (same schema as
|
||||
the old manual manifest). The view is OUTPUT — the authority is
|
||||
store/spine/current.json; direct edits to the view are flagged by
|
||||
`spine drift`. If a manual manifest is present (Spine not yet seeded
|
||||
in this checkout), it degrades gracefully to that legacy truth."""
|
||||
if os.path.exists(UMBRELLA_MANIFEST):
|
||||
with open(UMBRELLA_MANIFEST) as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
return data # generated view or manual manifest — both work
|
||||
return {"umbrellas": {}}
|
||||
|
||||
|
||||
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""scaffold_projection.py — Scaffolding V1 Tranche 1 (projection, not authority).
|
||||
|
||||
Answers, per component/module:
|
||||
what subsystem owns X? → SPINE store (structural authority)
|
||||
what does X depend on? → SPINE relationships (ingested from AST)
|
||||
who consumes X? → SPINE relationships (ingested from AST)
|
||||
what changes if X changes? → mirrors + dependent subsystems + consumers (SPINE)
|
||||
what proof obligations arise? → unit test, canary, ownership, umbrella gates
|
||||
which obligations are complete? → evidence check (test file exists, canary registered)
|
||||
|
||||
SPINE INTEGRATION: structural facts come from the Spine store — the single
|
||||
structural authority (ingested once from the umbrella manifest + AST).
|
||||
This script adds change-impact and proof-obligation reasoning on top; it
|
||||
no longer re-derives structure. No store → builds one (deterministic).
|
||||
|
||||
Commands:
|
||||
component <module> full card for one src/rune module
|
||||
impact <module> expected impact + obligations BEFORE a change
|
||||
obligations <module> obligation checklist with satisfied/pending state
|
||||
all [outfile] write the full projection JSON (default docs/)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import ast, json, sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src" / "rune"
|
||||
TESTS = ROOT / "tests"
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
from rune import spine # noqa: E402 — Spine IS the structural authority
|
||||
|
||||
|
||||
def load_spine() -> dict:
|
||||
"""Structural state from the Spine store (built on first use)."""
|
||||
doc = spine.load()
|
||||
if doc is None:
|
||||
doc = spine.build()
|
||||
spine.save(doc)
|
||||
return doc
|
||||
|
||||
|
||||
def module_imports(module: str) -> set[str]:
|
||||
"""Module names imported by src/rune/<module>.py (intra-package).
|
||||
|
||||
Includes relative imports (`from .x import y` — the dual-mode lazy
|
||||
pattern used across the runtime; missing these would hide real
|
||||
consumers, observed first run: rune_operator ↔ local_tool_runtime)."""
|
||||
path = SRC / f"{module}.py"
|
||||
if not path.is_file():
|
||||
return set()
|
||||
tree = ast.parse(path.read_text())
|
||||
names: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
names.add(a.name.split(".")[0])
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
names.add(node.module.split(".")[0])
|
||||
return names
|
||||
|
||||
|
||||
def import_graph() -> dict[str, set[str]]:
|
||||
return {p.stem: module_imports(p.stem) for p in SRC.glob("*.py")}
|
||||
|
||||
|
||||
def consumers_of(module: str, graph: dict[str, set[str]]) -> set[str]:
|
||||
return {m for m, deps in graph.items() if module in deps and m != module}
|
||||
|
||||
|
||||
def subsystem_for(doc: dict, module: str) -> str:
|
||||
rels = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
|
||||
return rels[0]["to"].split(":", 1)[1] if rels else ""
|
||||
|
||||
|
||||
def dependent_subsystems(doc: dict, subsystem: str) -> list[str]:
|
||||
return [r["from"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, to_id=f"subsystem:{subsystem}", type="depends_on")]
|
||||
|
||||
|
||||
def mirrors_for(doc: dict, module: str) -> list[str]:
|
||||
subs = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
|
||||
out = []
|
||||
for s in subs:
|
||||
out.extend(r["to"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, from_id=s["to"], type="mirrors"))
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def canary_registered(module: str) -> bool:
|
||||
try:
|
||||
sys.path.insert(0, str(SRC))
|
||||
from rune import canary_native
|
||||
return module in canary_native.MODULES
|
||||
except ImportError:
|
||||
# canary_native pulls optional deps (sqlite_vec etc.); the projection
|
||||
# degrades to "not registered" rather than failing the audit.
|
||||
return False
|
||||
|
||||
|
||||
def obligations(module: str, doc: dict) -> list[dict]:
|
||||
"""Proof obligations for changing this module, with evidence state.
|
||||
Structural facts sourced from Spine."""
|
||||
umb = subsystem_for(doc, module)
|
||||
test_file = TESTS / f"test_{module}.py"
|
||||
obs = [
|
||||
{"obligation": "unit_test", "satisfied": test_file.is_file(),
|
||||
"evidence": f"tests/test_{module}.py" + ("" if test_file.is_file() else " (MISSING)")},
|
||||
{"obligation": "canary", "satisfied": canary_registered(module),
|
||||
"evidence": "canary_native.MODULES" if canary_registered(module)
|
||||
else "not in canary registry (informational — not every module needs one)"},
|
||||
{"obligation": "umbrella_manifest", "satisfied": bool(umb),
|
||||
"evidence": f"umbrella '{umb}'" if umb else "NOT in any umbrella — manifest gap"},
|
||||
{"obligation": "full_suite", "satisfied": None,
|
||||
"evidence": "run: pytest tests/ -q (runtime evidence, not static)"},
|
||||
{"obligation": "lint_ratchet", "satisfied": None,
|
||||
"evidence": "run: scripts/lint_ratchet.py"},
|
||||
{"obligation": "henxels", "satisfied": None,
|
||||
"evidence": "run: henxels check --all"},
|
||||
]
|
||||
if umb:
|
||||
deps = dependent_subsystems(doc, umb)
|
||||
if deps:
|
||||
obs.append({"obligation": "dependent_canaries", "satisfied": None,
|
||||
"evidence": f"subsystems depending on '{umb}': {', '.join(deps)} — their canaries gate the change"})
|
||||
mir = mirrors_for(doc, module)
|
||||
if mir:
|
||||
obs.append({"obligation": "mirrors_travel", "satisfied": None,
|
||||
"evidence": f"files that must travel together: {', '.join(mir)}"})
|
||||
return obs
|
||||
|
||||
|
||||
def component_card(module: str, doc: dict) -> dict:
|
||||
"""Structural card from SPINE (owner/deps/consumers/mirrors) + obligations."""
|
||||
deps = [r["to"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, from_id=f"module:{module}", type="depends_on")]
|
||||
consumers = [r["to"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, from_id=f"module:{module}", type="consumed_by")]
|
||||
e = spine.entity(doc, f"module:{module}")
|
||||
card = {
|
||||
"component": module,
|
||||
"subsystem": subsystem_for(doc, module),
|
||||
"dependencies": sorted(set(deps)),
|
||||
"consumers": sorted(set(consumers)),
|
||||
"mirrors": mirrors_for(doc, module),
|
||||
"obligations": obligations(module, doc),
|
||||
}
|
||||
if e:
|
||||
card["spine_maturity"] = e.get("maturity")
|
||||
card["spine_evidence"] = e.get("evidence_refs", [])
|
||||
return card
|
||||
|
||||
|
||||
def impact(module: str) -> dict:
|
||||
doc = load_spine()
|
||||
card = component_card(module, doc)
|
||||
umb = card["subsystem"]
|
||||
card["expected_impact"] = {
|
||||
"direct_consumers": card["consumers"],
|
||||
"subsystem_dependents": dependent_subsystems(doc, umb) if umb else [],
|
||||
"must_travel": card["mirrors"],
|
||||
"gates": ["full_suite", "lint_ratchet", "henxels", "umbrella_gates"]
|
||||
+ (["canary"] if card and any(o["obligation"] == "canary" and o["satisfied"]
|
||||
for o in card["obligations"]) else []),
|
||||
}
|
||||
return card
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
print(__doc__)
|
||||
return 2
|
||||
cmd = args[0]
|
||||
if cmd in ("component", "impact", "obligations") and len(args) < 2:
|
||||
print(f"usage: {cmd} <module>", file=sys.stderr)
|
||||
return 2
|
||||
doc = load_spine()
|
||||
if cmd == "impact":
|
||||
print(json.dumps(impact(args[1]), indent=1))
|
||||
elif cmd == "component":
|
||||
print(json.dumps(component_card(args[1], doc), indent=1))
|
||||
elif cmd == "obligations":
|
||||
card = component_card(args[1], doc)
|
||||
print(json.dumps(card["obligations"], indent=1))
|
||||
elif cmd == "all":
|
||||
out = Path(args[1]) if len(args) > 1 else ROOT / "docs" / "scaffold_projection.json"
|
||||
cards = {p.stem: component_card(p.stem, doc)
|
||||
for p in sorted(SRC.glob("*.py"))
|
||||
if spine.entity(doc, f"module:{p.stem}")}
|
||||
out.write_text(json.dumps({
|
||||
"generated_by": "scripts/scaffold_projection.py (projection — no new authority)",
|
||||
"sources": ["rune.spine store (structural authority)", "src/rune/canary_native.py",
|
||||
"tests/ filesystem"],
|
||||
"components": cards,
|
||||
}, indent=1) + "\n")
|
||||
print(f"projection written: {out} ({len(cards)} components)")
|
||||
else:
|
||||
print(f"unknown command: {cmd}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.14"
|
||||
__version__ = "0.66.31"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.14"' in rune_version,
|
||||
'__version__ = "0.66.31"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dependency_tracker.py — Auto-propagation system for umbrella dependencies.
|
||||
"""dependency_tracker.py — Auto-propagation for the OpenCode HARNESS scripts.
|
||||
|
||||
SCOPE CLASSIFICATION (Spine V1 pre-Memory closure, 2026-09-12): the
|
||||
DEPENDENCY_GRAPH below describes ~/.config/opencode/scripts/*.py — the
|
||||
developer HARNESS (verified: 20/23 names collide with src/rune modules but
|
||||
all exist in the scripts dir; propagate() resolves against SCRIPTS_DIR).
|
||||
This is NOT Rune architecture and NOT a competing structural authority:
|
||||
Rune's structural truth lives in the Spine store (store/spine/current.json,
|
||||
consumed via rune.spine / scripts/scaffold_projection.py). The name
|
||||
collisions with src/rune modules are coincidental — do not read this map
|
||||
as Rune structure.
|
||||
|
||||
When a component changes, automatically updates dependent components.
|
||||
Maps the dependency graph between umbrella systems and fires
|
||||
Maps the dependency graph between harness systems and fires
|
||||
propagation rules when changes are detected.
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Minimal intent continuity foundation (overnight handoff PART 6).
|
||||
|
||||
A deterministic state machine for tracking detached intents — the
|
||||
smallest primitive that prevents "new good idea = new active objective."
|
||||
Not an ontology; not Substrate. Test-first, reload-safe, no LLM needed
|
||||
for state transitions once classification is supplied."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
CLASSIFICATIONS = ("BLOCKER", "FIX", "EXTENSION", "SUPERSEDING", "ORTHOGONAL", "LATER")
|
||||
STATUSES = ("ACTIVE", "DETOUR", "PARKED", "RESOLVED", "SUPERSEDED", "DEFERRED")
|
||||
|
||||
|
||||
class IntentRecord:
|
||||
__slots__ = (
|
||||
"id",
|
||||
"objective",
|
||||
"parent_intent",
|
||||
"active_detour",
|
||||
"return_when",
|
||||
"return_to",
|
||||
"unresolved_obligations",
|
||||
"success_condition",
|
||||
"classification",
|
||||
"status",
|
||||
"created_at",
|
||||
"resolved_at",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
id,
|
||||
objective,
|
||||
classification,
|
||||
parent_intent="",
|
||||
active_detour=False,
|
||||
return_when="",
|
||||
return_to="",
|
||||
unresolved_obligations=None,
|
||||
success_condition="",
|
||||
status="ACTIVE",
|
||||
):
|
||||
self.id = id
|
||||
self.objective = objective
|
||||
if classification not in CLASSIFICATIONS:
|
||||
raise ValueError(f"invalid classification {classification!r}")
|
||||
self.classification = classification
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"invalid status {status!r}")
|
||||
self.status = status
|
||||
self.parent_intent = parent_intent
|
||||
self.active_detour = active_detour
|
||||
self.return_when = return_when
|
||||
self.return_to = return_to
|
||||
self.unresolved_obligations = unresolved_obligations or []
|
||||
self.success_condition = success_condition
|
||||
self.created_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
self.resolved_at = ""
|
||||
|
||||
def to_dict(self):
|
||||
return {s: getattr(self, s) for s in self.__slots__}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
rec = cls.__new__(cls)
|
||||
for s in cls.__slots__:
|
||||
setattr(rec, s, d.get(s, "" if s != "unresolved_obligations" else []))
|
||||
return rec
|
||||
|
||||
|
||||
class IntentLedger:
|
||||
"""Deterministic intent-state store. JSON persistence, no LLM."""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = Path(path)
|
||||
self.records = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if self.path.is_file():
|
||||
data = json.loads(self.path.read_text())
|
||||
for rec in data.get("intents", []):
|
||||
r = IntentRecord.from_dict(rec)
|
||||
self.records[r.id] = r
|
||||
|
||||
def save(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps({"intents": [r.to_dict() for r in self.records.values()]}, indent=1))
|
||||
|
||||
def create(
|
||||
self, *, id, objective, classification, success_condition="", parent_intent="", unresolved_obligations=None
|
||||
):
|
||||
rec = IntentRecord(
|
||||
id=id,
|
||||
objective=objective,
|
||||
classification=classification,
|
||||
success_condition=success_condition,
|
||||
parent_intent=parent_intent,
|
||||
unresolved_obligations=unresolved_obligations,
|
||||
)
|
||||
self.records[id] = rec
|
||||
self.save()
|
||||
return rec
|
||||
|
||||
def create_detour(self, *, id, objective, classification, parent_intent, return_when, return_to):
|
||||
parent = self.records.get(parent_intent)
|
||||
if not parent:
|
||||
raise ValueError(f"parent {parent_intent!r} not found")
|
||||
rec = IntentRecord(
|
||||
id=id,
|
||||
objective=objective,
|
||||
classification=classification,
|
||||
parent_intent=parent_intent,
|
||||
active_detour=True,
|
||||
return_when=return_when,
|
||||
return_to=return_to,
|
||||
status="DETOUR",
|
||||
)
|
||||
self.records[id] = rec
|
||||
if parent.status == "ACTIVE":
|
||||
parent.status = "PARKED"
|
||||
self.save()
|
||||
return rec
|
||||
|
||||
def resolve(self, intent_id):
|
||||
"""Invariant 5: resolving a child does NOT resolve its parent."""
|
||||
rec = self.records.get(intent_id)
|
||||
if not rec or rec.status not in ("ACTIVE", "DETOUR"):
|
||||
return None
|
||||
rec.status = "RESOLVED"
|
||||
rec.resolved_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
self.save()
|
||||
return rec
|
||||
|
||||
def resolve_detour(self, detour_id):
|
||||
d = self.records.get(detour_id)
|
||||
if not d or d.status not in ("DETOUR", "RESOLVED"):
|
||||
return None
|
||||
if d.status == "DETOUR":
|
||||
d.status = "RESOLVED"
|
||||
d.resolved_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
parent = self.records.get(d.parent_intent)
|
||||
if parent and parent.status == "PARKED":
|
||||
parent.status = "ACTIVE"
|
||||
self.save()
|
||||
return parent
|
||||
|
||||
def supersede(self, old_id, new_id, *, objective, classification, carry_obligations=True):
|
||||
old = self.records[old_id]
|
||||
if old.unresolved_obligations and not carry_obligations:
|
||||
raise ValueError(f"cannot supersede {old_id}: {len(old.unresolved_obligations)} unresolved obligations")
|
||||
rec = IntentRecord(
|
||||
id=new_id,
|
||||
objective=objective,
|
||||
classification=classification,
|
||||
parent_intent=old.parent_intent,
|
||||
unresolved_obligations=list(old.unresolved_obligations) if carry_obligations else [],
|
||||
status="ACTIVE",
|
||||
)
|
||||
old.status = "SUPERSEDED"
|
||||
self.records[new_id] = rec
|
||||
self.save()
|
||||
return rec
|
||||
@@ -238,6 +238,7 @@ def context_length(model: str, base_url: str = "http://localhost:11434") -> int:
|
||||
conservative default 8192 + T1 ``record_failure("context_discovery")``
|
||||
— the assumption is observable, never silent (P0-06).
|
||||
"""
|
||||
from rune.openai_compat_transport import is_cloud_pinned
|
||||
from rune.subsystem_health import record_failure
|
||||
|
||||
with _CONTEXT_LENGTH_LOCK:
|
||||
@@ -245,7 +246,15 @@ def context_length(model: str, base_url: str = "http://localhost:11434") -> int:
|
||||
if cached:
|
||||
return cached
|
||||
try:
|
||||
window = OllamaChatTransport(base_url=base_url).context_length(model)
|
||||
if is_cloud_pinned(model):
|
||||
# Cloud-pinned models must not hit Ollama's /api/show — observed
|
||||
# live: 400 'invalid model name' + 8192 misbudget for a model
|
||||
# with a 131072 window. The provider config carries the window.
|
||||
from rune.openai_compat_transport import cloud_transport_for
|
||||
|
||||
window = cloud_transport_for(model).context_length(model)
|
||||
else:
|
||||
window = OllamaChatTransport(base_url=base_url).context_length(model)
|
||||
except Exception as exc:
|
||||
record_failure("context_discovery", exc, tier=1, context=model)
|
||||
return CONTEXT_LENGTH_FALLBACK
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""OpenAI-compatible chat transport (T15 PL1 held-out substrate fix).
|
||||
|
||||
The operator's execution substrate had exactly one transport —
|
||||
OllamaChatTransport (localhost:11434) — so send_task(model=<cloud>) posted a
|
||||
cloud model name to Ollama's /api/chat and died with HTTP 400 (observed live
|
||||
2026-09-12: 'Verboo Code/glm-5.3-flash' → 400 in 0.2s). The supported
|
||||
model= override could not execute ANY cloud provider.
|
||||
|
||||
This transport speaks the OpenAI wire format (/chat/completions) and returns
|
||||
Ollama-SHAPED responses, so LocalToolRuntime's loop is unchanged. Selection
|
||||
is deterministic (routing_mode == 'cloud') — no router, ladder, or escalation
|
||||
logic is touched.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
class OpenAICompatTransport:
|
||||
"""ChatTransport for OpenAI-compatible providers (Verboo Code, Xioami...)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
provider_name: str,
|
||||
timeout: float = 120,
|
||||
context_window: int = 131072,
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.provider = provider_name
|
||||
# Cloud round trips with a full tool schema routinely exceed the local
|
||||
# tier timeout (30s): observed live, glm-5.3-flash read-timeout at
|
||||
# 30.6s mid-iteration. Clamp the WIRE timeout to >=120s — the
|
||||
# escalation ladder still governs retries/tiers; this is one round
|
||||
# trip's rope, not policy.
|
||||
self.timeout = max(timeout, 120)
|
||||
self._context_window = context_window
|
||||
# Usage accumulators — same contract as OllamaChatTransport (I2).
|
||||
self.usage_calls = 0
|
||||
self.usage_prompt_tokens = 0
|
||||
self.usage_output_tokens = 0
|
||||
self.usage_total_duration_ns = 0
|
||||
|
||||
# -- wire -----------------------------------------------------------
|
||||
|
||||
def _post(self, path: str, payload: dict) -> dict:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}{path}",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
||||
decoded = json.loads(response.read())
|
||||
if not isinstance(decoded, dict):
|
||||
raise ValueError("provider returned a non-object response")
|
||||
return decoded
|
||||
|
||||
# -- message translation --------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _to_openai_messages(messages: list[dict]) -> list[dict]:
|
||||
"""Ollama-shape → OpenAI-shape. Pairing: the runtime's _tool_message
|
||||
sets tool_call_id from request.call_id (minted by
|
||||
normalize_ollama_tool_calls from the response's own id). The assistant
|
||||
message must use THE SAME ids for its tool_calls — a mismatch makes
|
||||
the provider treat the tool result as orphaned and the model re-issues
|
||||
the identical call forever (observed live: glm-5.3-flash looped the
|
||||
same edit_file twice, empty terminal text both turns)."""
|
||||
out: list[dict] = []
|
||||
i = 0
|
||||
while i < len(messages):
|
||||
m = messages[i]
|
||||
role = m.get("role")
|
||||
tool_calls = m.get("tool_calls") or []
|
||||
if role == "assistant" and tool_calls:
|
||||
# Look ahead: pair each tool_call with the tool messages that
|
||||
# immediately follow, reusing their tool_call_ids.
|
||||
paired_ids = [messages[j].get("tool_call_id")
|
||||
for j in range(i + 1, len(messages))
|
||||
if messages[j].get("role") == "tool"][:len(tool_calls)]
|
||||
tc = []
|
||||
for k, c in enumerate(tool_calls):
|
||||
fn = c.get("function", {})
|
||||
args = fn.get("arguments", {})
|
||||
args_str = json.dumps(args) if isinstance(args, dict) else str(args)
|
||||
call_id = paired_ids[k] if k < len(paired_ids) else f"call_{i}_{k}"
|
||||
tc.append({"id": call_id or f"call_{i}_{k}", "type": "function",
|
||||
"function": {"name": fn.get("name"), "arguments": args_str}})
|
||||
out.append({"role": "assistant", "content": m.get("content") or None, "tool_calls": tc})
|
||||
i += 1
|
||||
continue
|
||||
if role == "tool":
|
||||
out.append({"role": "tool",
|
||||
"tool_call_id": m.get("tool_call_id") or f"call_{i}",
|
||||
"content": m.get("content", "")})
|
||||
i += 1
|
||||
continue
|
||||
out.append({"role": role, "content": m.get("content", "")})
|
||||
i += 1
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _from_openai_response(resp: dict) -> dict:
|
||||
"""OpenAI response → flat Ollama ChatMessage (LocalToolRuntime's contract).
|
||||
|
||||
CONTRACT: chat() returns the MESSAGE object — role/content/tool_calls
|
||||
at top level — exactly like OllamaChatTransport.chat. Observed live
|
||||
(PL1 Task A, 2026-09-12): the envelope shape {'message': {...}} made
|
||||
the runtime see 'empty terminal visible text' while the model had
|
||||
returned a valid read_file tool call."""
|
||||
choices = resp.get("choices") or [{}]
|
||||
message = choices[0].get("message", {}) or {}
|
||||
tool_calls = []
|
||||
for c in message.get("tool_calls") or []:
|
||||
fn = c.get("function", {}) or {}
|
||||
args = fn.get("arguments", {})
|
||||
if isinstance(args, str):
|
||||
try:
|
||||
args = json.loads(args)
|
||||
except json.JSONDecodeError:
|
||||
args = {"_raw": args}
|
||||
entry = {"function": {"name": fn.get("name"), "arguments": args or {}}}
|
||||
if c.get("id"):
|
||||
entry["id"] = c["id"]
|
||||
tool_calls.append(entry)
|
||||
out: dict = {"role": "assistant", "content": message.get("content") or ""}
|
||||
if tool_calls:
|
||||
out["tool_calls"] = tool_calls
|
||||
return out
|
||||
|
||||
# -- ChatTransport protocol -------------------------------------------
|
||||
|
||||
def chat(self, *, model: str, messages: list[dict], tools: list[dict] | None,
|
||||
options: dict | None = None, think: bool | str | None = None) -> dict:
|
||||
# The runtime passes the qualified name ('<Provider>/<model>'); the
|
||||
# provider API wants the bare model id ('glm-5.3-flash'). Observed
|
||||
# live: qualified name → 400 on every call (PL1 held-out attempt).
|
||||
wire_model = model.split("/", 1)[-1] if "/" in model else model
|
||||
payload: dict = {
|
||||
"model": wire_model,
|
||||
"messages": self._to_openai_messages(messages),
|
||||
"stream": False,
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools # Ollama tool schema == OpenAI tool schema
|
||||
if options:
|
||||
for k in ("temperature", "top_p", "max_tokens"):
|
||||
if k in options:
|
||||
payload[k] = options[k]
|
||||
started = time.monotonic()
|
||||
resp = self._post("/chat/completions", payload)
|
||||
elapsed_ns = int((time.monotonic() - started) * 1e9)
|
||||
usage = resp.get("usage", {}) or {}
|
||||
self.usage_calls += 1
|
||||
self.usage_prompt_tokens += int(usage.get("prompt_tokens") or 0)
|
||||
self.usage_output_tokens += int(usage.get("completion_tokens") or 0)
|
||||
self.usage_total_duration_ns += elapsed_ns
|
||||
return self._from_openai_response(resp)
|
||||
|
||||
def supports_tools(self, model: str) -> bool:
|
||||
return True
|
||||
|
||||
def supports_thinking(self, model: str) -> bool:
|
||||
return False
|
||||
|
||||
def context_length(self, model: str) -> int:
|
||||
return self._context_window
|
||||
|
||||
|
||||
def is_cloud_pinned(model: str) -> bool:
|
||||
"""THE provider-prefix rule, single authority: True when the model name
|
||||
requires the [OI]-compat wire — a non-empty provider prefix that is
|
||||
neither ollama nor local. Consumers: transport selection
|
||||
(rune_operator._transport_for) and context discovery
|
||||
(local_tool_runtime.context_length). Never copy this rule — import it."""
|
||||
prefix = model.split("/", 1)[0].strip().lower() if "/" in model else ""
|
||||
return bool(prefix) and prefix not in ("ollama", "local")
|
||||
|
||||
|
||||
def cloud_transport_for(model: str, timeout: float = 120) -> OpenAICompatTransport:
|
||||
"""Resolve provider config for a '<Provider>/<model>' name from the same
|
||||
source of truth the mimo CLI uses (mimocode.jsonc + auth.json)."""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
if "/" not in model:
|
||||
raise ValueError(f"cloud model must be '<Provider>/<model>': {model!r}")
|
||||
provider_name, short_model = model.split("/", 1)
|
||||
base_url = api_key = None
|
||||
context_window = 131072
|
||||
cfg_path = Path(os.path.expanduser("~/.config/mimocode/mimocode.jsonc"))
|
||||
if cfg_path.is_file():
|
||||
text = cfg_path.read_text()
|
||||
m = re.search(rf'"{re.escape(provider_name)}"\s*:\s*\{{', text)
|
||||
if m:
|
||||
block = text[m.start():m.start() + 2000]
|
||||
bm = re.search(r'"baseURL":\s*"([^"]+)"', block)
|
||||
if bm:
|
||||
base_url = bm.group(1)
|
||||
wm = re.search(rf'"{re.escape(short_model)}"\s*:\s*\{{[^}}]*?"context_window"\s*:\s*(\d+)', block)
|
||||
if wm:
|
||||
context_window = int(wm.group(1))
|
||||
if not base_url:
|
||||
raise ValueError(f"no baseURL configured for provider {provider_name!r}")
|
||||
key = os.environ.get(f"{provider_name.upper().replace(' ', '_')}_API_KEY", "")
|
||||
if not key:
|
||||
auth_path = Path(os.path.expanduser("~/.local/share/mimocode/auth.json"))
|
||||
if auth_path.is_file():
|
||||
auth = json.loads(auth_path.read_text())
|
||||
key = (auth.get(provider_name) or {}).get("key", "")
|
||||
if not key:
|
||||
raise ValueError(f"no API key for provider {provider_name!r}")
|
||||
# Cloud routers queue and batch — a tool-loop turn can legitimately exceed
|
||||
# the local-model timeout (observed live: glm-5.3-flash read-timeout at
|
||||
# 120s twice). Floor cloud transports at 300s; local keeps its own.
|
||||
return OpenAICompatTransport(
|
||||
base_url, key, provider_name, timeout=max(timeout, 300),
|
||||
context_window=context_window,
|
||||
)
|
||||
@@ -1228,6 +1228,22 @@ class RuneOperator:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _transport_for(model: str, timeout: float = 300.0):
|
||||
"""Provider-prefix transport selection — ONE authority for both the
|
||||
direct turn and the approval-resume path (T15 substrate contract).
|
||||
|
||||
Any model with a non-ollama provider prefix rides the [OI]-compat
|
||||
wire; ollama-prefixed and bare names keep the Ollama path. Observed
|
||||
live (PL1 Task A, 2026-09-12): the resume path hardcoded
|
||||
OllamaChatTransport, so the post-approval follow-up call hit
|
||||
localhost with a cloud model name → 400 'invalid model name'."""
|
||||
from rune.openai_compat_transport import cloud_transport_for, is_cloud_pinned
|
||||
|
||||
if is_cloud_pinned(model):
|
||||
return cloud_transport_for(model, timeout=timeout)
|
||||
return OllamaChatTransport(timeout=timeout)
|
||||
|
||||
def _runtime_for_model(self, model: str, task_description: str = "") -> LocalToolRuntime:
|
||||
"""Build the runtime used to continue a persisted approval turn.
|
||||
|
||||
@@ -1250,7 +1266,7 @@ class RuneOperator:
|
||||
if not tools:
|
||||
tools = list(tool_executor.TOOLS) # pre-#24 behavior (degrade)
|
||||
return LocalToolRuntime(
|
||||
transport=OllamaChatTransport(),
|
||||
transport=self._transport_for(model),
|
||||
executor=tool_executor.execute_tool,
|
||||
model=model,
|
||||
tools=tools,
|
||||
@@ -1953,8 +1969,23 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
if hasattr(capsule, "capture_messages"):
|
||||
capsule.capture_messages(messages)
|
||||
|
||||
# Transport selection is deterministic substrate (T15 PL1
|
||||
# substrate fix): the pinned provider override
|
||||
# (send_task(model='<Provider>/<model>')) requires an
|
||||
# OpenAI-compatible wire for cloud models — OllamaChatTransport
|
||||
# is localhost-only and 400s on any cloud model name. Router,
|
||||
# ladder, and escalation logic are untouched.
|
||||
# Provider-keyed selection (NOT routing_mode): an
|
||||
# operator-requested model keeps routing_mode="local" (line
|
||||
# ~887) even for cloud providers, so the selection must key on
|
||||
# the model's provider prefix. Observed live: pinned
|
||||
# 'Verboo Code/glm-5.3-flash' still hit Ollama under the
|
||||
# routing_mode condition and 400'd. Deterministic — any model
|
||||
# with a non-ollama provider prefix rides the OpenAI-compat
|
||||
# wire; ollama-prefixed and bare names keep the proven path.
|
||||
transport = self._transport_for(model, timeout=timeout)
|
||||
runtime = LocalToolRuntime(
|
||||
transport=OllamaChatTransport(timeout=timeout),
|
||||
transport=transport,
|
||||
executor=tool_executor.execute_tool,
|
||||
model=model,
|
||||
tools=capability_selection["tools"],
|
||||
|
||||
@@ -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.66.14
|
||||
Version: v0.66.31
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1856,7 +1856,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.66.14 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.31 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
+1114
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -385,7 +385,8 @@ def test_thread_safety_concurrent_assemble():
|
||||
|
||||
def test_operator_characterization_parity():
|
||||
"""Existing role-message assertions still hold via the broker path."""
|
||||
operator = rune_operator.RuneOperator("/tmp/context-broker-parity-operator.json")
|
||||
import tempfile
|
||||
operator = rune_operator.RuneOperator(str(Path(tempfile.mkdtemp()) / "parity-operator.json"))
|
||||
runtime = Mock()
|
||||
runtime.run.return_value = OperatorResponse(success=True, user_text="Final answer", model="qwen3:8b")
|
||||
profile = SimpleNamespace(safety_tier=2)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Intent continuity foundation (overnight handoff PART 6): deterministic
|
||||
state machine, persistence, and all seven invariants."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune.intent_continuity import IntentLedger, IntentRecord, CLASSIFICATIONS # noqa: E402
|
||||
|
||||
|
||||
class TestIntentLedger:
|
||||
def _fresh(self):
|
||||
return IntentLedger(Path(tempfile.mkdtemp()) / "intent.json")
|
||||
|
||||
def test_create_and_reload(self):
|
||||
"""Invariant 6: state is persistable/reloadable."""
|
||||
led = self._fresh()
|
||||
led.create(
|
||||
id="root",
|
||||
objective="ship PL1",
|
||||
classification="BLOCKER",
|
||||
success_condition="held-out transfer demonstrated",
|
||||
)
|
||||
led2 = IntentLedger(led.path)
|
||||
assert "root" in led2.records
|
||||
assert led2.records["root"].status == "ACTIVE"
|
||||
|
||||
def test_detour_preserves_parent(self):
|
||||
"""Invariants 1-3."""
|
||||
led = self._fresh()
|
||||
led.create(id="root", objective="ship PL1", classification="BLOCKER")
|
||||
detour = led.create_detour(
|
||||
id="d",
|
||||
objective="add scaffolding",
|
||||
classification="EXTENSION",
|
||||
parent_intent="root",
|
||||
return_when="PL1 done",
|
||||
return_to="root",
|
||||
)
|
||||
assert detour.status == "DETOUR"
|
||||
assert led.records["root"].status == "PARKED"
|
||||
parent = led.resolve_detour("d")
|
||||
assert parent.id == "root"
|
||||
assert parent.status == "ACTIVE"
|
||||
assert led.records["d"].status == "RESOLVED"
|
||||
|
||||
def test_supersession_carries_obligations(self):
|
||||
"""Invariant 4."""
|
||||
led = self._fresh()
|
||||
led.create(id="old", objective="old plan", classification="FIX")
|
||||
led.records["old"].unresolved_obligations = ["review PR", "update docs"]
|
||||
new = led.supersede("old", "new", objective="new plan", classification="FIX")
|
||||
assert "review PR" in new.unresolved_obligations
|
||||
assert led.records["old"].status == "SUPERSEDED"
|
||||
|
||||
def test_child_doesnt_resolve_parent(self):
|
||||
"""Invariant 5: resolving a child doesn't resolve the parent."""
|
||||
led = self._fresh()
|
||||
led.create(id="p", objective="parent", classification="BLOCKER")
|
||||
led.create_detour(
|
||||
id="c", objective="child", classification="FIX", parent_intent="p", return_when="done", return_to="p"
|
||||
)
|
||||
led.resolve("c")
|
||||
assert led.records["p"].status == "PARKED" # still parked
|
||||
led.resolve_detour("c")
|
||||
assert led.records["p"].status == "ACTIVE"
|
||||
|
||||
def test_classification_validated(self):
|
||||
led = self._fresh()
|
||||
try:
|
||||
led.create(id="bad", objective="x", classification="UNICORN")
|
||||
assert False, "should have raised"
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -584,3 +584,48 @@ class LocalToolRuntimeTests(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class TestCloudModelContextDiscovery:
|
||||
"""Context discovery must not hit Ollama with cloud-pinned model names.
|
||||
|
||||
Observed live (PL1 Task A/B, deployed 0.66.21/0.66.22): every cloud
|
||||
task logged `subsystem=context_discovery tier=1 HTTPError 400` because
|
||||
module-level discovery used OllamaChatTransport for a model whose
|
||||
provider prefix requires the [OI]-compat wire — then misbudgeted the
|
||||
task at the 8192 fallback instead of the provider's real window."""
|
||||
|
||||
def test_cloud_pinned_model_uses_provider_window_not_ollama(self):
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
from rune import local_tool_runtime as ltr
|
||||
import rune.openai_compat_transport as _oct
|
||||
ltr.reset_context_length_cache()
|
||||
|
||||
class _StubCloud:
|
||||
def context_length(self, model):
|
||||
return 131072
|
||||
|
||||
with mock.patch.object(ltr, "OllamaChatTransport",
|
||||
side_effect=AssertionError(
|
||||
"Ollama must not be consulted for cloud-pinned models")), \
|
||||
mock.patch.object(_oct, "cloud_transport_for",
|
||||
return_value=_StubCloud()):
|
||||
window = ltr.context_length("Verboo Code/glm-5.3-flash")
|
||||
assert window == 131072, window
|
||||
|
||||
def test_local_model_still_uses_ollama_discovery(self):
|
||||
from unittest import mock
|
||||
from rune import local_tool_runtime as ltr
|
||||
ltr.reset_context_length_cache()
|
||||
|
||||
class _StubOllama:
|
||||
def __init__(self, base_url="http://localhost:11434", timeout=120):
|
||||
self.calls = []
|
||||
|
||||
def context_length(self, model):
|
||||
return 4096
|
||||
|
||||
with mock.patch.object(ltr, "OllamaChatTransport", _StubOllama):
|
||||
window = ltr.context_length("qwen2.5:3b")
|
||||
assert window == 4096, window
|
||||
@@ -0,0 +1,192 @@
|
||||
"""OpenAI-compatible transport (T15 PL1 substrate fix).
|
||||
|
||||
The operator had exactly one transport (Ollama) — send_task(model=<cloud>)
|
||||
posted cloud model names to Ollama's /api/chat and 400'd. These tests prove
|
||||
the translation layer both directions; the live wire is smoke-tested
|
||||
separately (provider credentials are not test fixtures)."""
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune.openai_compat_transport import OpenAICompatTransport # noqa: E402
|
||||
|
||||
|
||||
class TestMessageTranslation:
|
||||
def test_ollama_to_openai_roundtrip(self):
|
||||
msgs = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "task"},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "edit_file", "arguments": {"path": "/x.py", "old": "a", "new": "b"}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_2_0", "content": "{\"success\": true}"},
|
||||
]
|
||||
out = OpenAICompatTransport._to_openai_messages(msgs)
|
||||
# assistant tool_calls: dict args → JSON string, ids assigned
|
||||
tc = out[2]["tool_calls"][0]
|
||||
assert tc["function"]["arguments"] == json.dumps({"path": "/x.py", "old": "a", "new": "b"})
|
||||
assert tc["id"] == "call_2_0"
|
||||
# tool result keeps its call id
|
||||
assert out[3]["tool_call_id"] == "call_2_0"
|
||||
# plain roles pass through
|
||||
assert out[0] == {"role": "system", "content": "sys"}
|
||||
|
||||
def test_openai_response_to_ollama_shape(self):
|
||||
resp = {"choices": [{"message": {"role": "assistant", "content": None, "tool_calls": [
|
||||
{"id": "z", "type": "function", "function": {"name": "run_tests", "arguments": "{\"path\": \"t\"}"}}]}}]}
|
||||
shape = OpenAICompatTransport._from_openai_response(resp)
|
||||
tc = shape["tool_calls"][0]
|
||||
assert tc["function"]["name"] == "run_tests"
|
||||
assert tc["function"]["arguments"] == {"path": "t"} # string → dict
|
||||
|
||||
def test_malformed_arguments_become_raw(self):
|
||||
resp = {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "x", "arguments": "not-json"}}]}}]}
|
||||
shape = OpenAICompatTransport._from_openai_response(resp)
|
||||
assert shape["tool_calls"][0]["function"]["arguments"] == {"_raw": "not-json"}
|
||||
|
||||
def test_no_tool_calls_plain_content(self):
|
||||
resp = {"choices": [{"message": {"content": "hello"}}]}
|
||||
shape = OpenAICompatTransport._from_openai_response(resp)
|
||||
assert shape["content"] == "hello"
|
||||
assert "tool_calls" not in shape
|
||||
|
||||
def test_flat_message_contract_parity_with_ollama(self):
|
||||
"""CONTRACT: chat() returns the flat MESSAGE object (role/content/
|
||||
tool_calls at top level) — the same shape OllamaChatTransport.chat
|
||||
returns. Observed live (PL1 Task A, 2026-09-12): returning the
|
||||
response envelope {'message': {...}} left the runtime's tool-call
|
||||
normalization reading the top level, finding no tool_calls and no
|
||||
content, and failing 'empty terminal visible text' while the model
|
||||
had returned a valid read_file call."""
|
||||
resp = {"choices": [{"message": {"role": "assistant", "content": "", "tool_calls": [
|
||||
{"id": "call_1", "type": "function",
|
||||
"function": {"name": "read_file", "arguments": "{\"path\": \"/x.py\"}"}}]}}]}
|
||||
msg = OpenAICompatTransport._from_openai_response(resp)
|
||||
assert msg["role"] == "assistant"
|
||||
assert msg["tool_calls"][0]["function"]["name"] == "read_file"
|
||||
assert msg["tool_calls"][0]["id"] == "call_1" # id preserved for pairing
|
||||
assert "message" not in msg, "envelope shape leaks into the ChatMessage contract"
|
||||
|
||||
def test_protocol_methods(self):
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test")
|
||||
assert t.supports_tools("m") is True
|
||||
assert t.supports_thinking("m") is False
|
||||
assert t.context_length("m") == 131072
|
||||
|
||||
def test_usage_accumulators(self):
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test")
|
||||
t.chat = lambda **kw: ( # simulate the accumulator path
|
||||
setattr(t, "usage_calls", t.usage_calls + 1),
|
||||
setattr(t, "usage_prompt_tokens", t.usage_prompt_tokens + 10),
|
||||
setattr(t, "usage_output_tokens", t.usage_output_tokens + 5),
|
||||
)[-1]
|
||||
t.chat(model="m", messages=[])
|
||||
assert t.usage_calls == 1 and t.usage_prompt_tokens == 10 and t.usage_output_tokens == 5
|
||||
|
||||
|
||||
def test_qualified_model_name_sent_as_bare_id(self):
|
||||
"""Runtime passes '<Provider>/<model>'; the wire must carry the bare
|
||||
model id (observed live: qualified name → 400 on every call)."""
|
||||
captured = {}
|
||||
|
||||
def fake_post(path, payload):
|
||||
captured.update(payload)
|
||||
return {"choices": [{"message": {"role": "assistant", "content": "ok"}}], "usage": {}}
|
||||
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Verboo Code")
|
||||
t._post = fake_post
|
||||
t.chat(model="Verboo Code/glm-5.3-flash", messages=[{"role": "user", "content": "hi"}], tools=None)
|
||||
assert captured["model"] == "glm-5.3-flash"
|
||||
|
||||
|
||||
def test_cloud_transport_for_floors_timeout(self):
|
||||
"""cloud_transport_for floors the timeout at 300s for cloud routers."""
|
||||
import rune.openai_compat_transport as mod
|
||||
captured = {}
|
||||
real_init = mod.OpenAICompatTransport.__init__
|
||||
|
||||
def spy_init(self, base_url, api_key, provider_name, timeout=120, context_window=131072):
|
||||
captured["timeout"] = timeout
|
||||
|
||||
with mock.patch.object(mod.OpenAICompatTransport, "__init__", spy_init), \
|
||||
mock.patch.object(Path, "is_file", return_value=True), \
|
||||
mock.patch.object(Path, "read_text", return_value='{"Verboo Code": {"options": {"baseURL": "https://x/v1"}}}'), \
|
||||
mock.patch.dict(sys.modules, {}):
|
||||
import json as _json
|
||||
with mock.patch.object(mod.json, "loads", return_value={"Verboo Code": {"key": "k"}}):
|
||||
mod.cloud_transport_for("Verboo Code/glm-5.3-flash", timeout=120)
|
||||
assert captured["timeout"] == 300
|
||||
|
||||
|
||||
class TestTransportSelection:
|
||||
"""Provider-keyed transport selection (not routing_mode): an
|
||||
operator-requested cloud model keeps routing_mode='local', so the
|
||||
selection must key on the provider prefix (T15 held-out finding)."""
|
||||
|
||||
def test_pinned_cloud_selects_openai_compat(self):
|
||||
import os
|
||||
from unittest import mock
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
os.environ["MIMO_PROBE_ENABLED"] = "0"
|
||||
# Journal edges resolve via memory_env; on CI the real memory dir is
|
||||
# root-owned (PermissionError on events.jsonl). Isolate first.
|
||||
os.environ["RUNE_MEMORY_DIR"] = os.path.join(tempfile.mkdtemp(), "memory")
|
||||
from rune import rune_operator
|
||||
from rune.local_tool_runtime import OllamaChatTransport
|
||||
|
||||
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
captured = {}
|
||||
|
||||
class FakeRuntime:
|
||||
def __init__(self, transport, executor, model, tools):
|
||||
captured["transport"] = transport
|
||||
captured["model"] = model
|
||||
|
||||
def run(self, messages, options=None):
|
||||
from rune.tool_protocol import OperatorResponse
|
||||
return OperatorResponse(success=True, user_text="done", model=captured["model"])
|
||||
|
||||
import rune.openai_compat_transport as _oct
|
||||
with (
|
||||
mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime),
|
||||
mock.patch.object(_oct, "cloud_transport_for",
|
||||
side_effect=lambda model, timeout=120: OpenAICompatTransport(
|
||||
"https://mock/v1", "test-key", model.split("/", 1)[0])),
|
||||
):
|
||||
op.send_task("Fix the bug in greeting.py: returns 'Helo' not 'Hello'. Verify by running the check script.",
|
||||
model="Verboo Code/glm-5.3-flash")
|
||||
assert isinstance(captured["transport"], OpenAICompatTransport)
|
||||
|
||||
captured.clear()
|
||||
with mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime):
|
||||
op.send_task("inspect the log for errors", model="qwen2.5:3b")
|
||||
assert isinstance(captured["transport"], OllamaChatTransport)
|
||||
|
||||
|
||||
def test_constructor_clamps_timeout(self):
|
||||
"""The transport clamps the wire timeout to >=120s (cloud round trips
|
||||
exceed the local tier timeout — observed live)."""
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "T", timeout=45)
|
||||
assert t.timeout == 120, "below-floor must clamp to 120"
|
||||
t2 = OpenAICompatTransport("http://x/v1", "k", "T", timeout=300)
|
||||
assert t2.timeout == 300, "above-floor must be preserved"
|
||||
|
||||
def test_timeout_clamped_to_120(self):
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=30)
|
||||
assert t.timeout == 120
|
||||
t2 = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=300)
|
||||
assert t2.timeout == 300
|
||||
def test_timeout_clamped_to_120():
|
||||
"""Cloud round trips with a full tool schema exceed the local tier timeout
|
||||
(30s): observed live, glm-5.3-flash read-timeout at 30.6s mid-iteration.
|
||||
The transport clamps the wire timeout to >=120s."""
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=30)
|
||||
assert t.timeout == 120
|
||||
t2 = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=300)
|
||||
assert t2.timeout == 300
|
||||
@@ -0,0 +1,955 @@
|
||||
"""test_spine.py — Spine V1 contract tests.
|
||||
|
||||
Deterministic core, open-world extensibility, evidence-gated maturity,
|
||||
snapshot/delta, drift, and the synthetic future-capability proof.
|
||||
Machine-independent: deployment observations use fixture stores.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import spine
|
||||
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture
|
||||
def doc():
|
||||
return spine.new_doc()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seeded(doc):
|
||||
"""Minimal structural world: two subsystems, two modules."""
|
||||
spine.add_entity(doc, id="subsystem:alpha", kind="subsystem",
|
||||
identities={"description": "Alpha"}, maturity="DECLARED")
|
||||
spine.add_entity(doc, id="subsystem:beta", kind="subsystem",
|
||||
identities={"description": "Beta"}, maturity="DECLARED")
|
||||
spine.add_entity(doc, id="module:m_a", kind="module", maturity="DECLARED",
|
||||
identities={"source": "src/rune/m_a.py"})
|
||||
spine.add_entity(doc, id="module:m_b", kind="module", maturity="DECLARED",
|
||||
identities={"source": "src/rune/m_b.py"})
|
||||
spine.add_relationship(doc, type="belongs_to", from_id="module:m_a",
|
||||
to_id="subsystem:alpha", evidence_refs=["test"])
|
||||
spine.add_relationship(doc, type="belongs_to", from_id="module:m_b",
|
||||
to_id="subsystem:beta", evidence_refs=["test"])
|
||||
return doc
|
||||
|
||||
|
||||
# ── deterministic entity projection / lookups ─────────────────────────────
|
||||
|
||||
class TestLookups:
|
||||
def test_entity_projection_roundtrip(self, seeded, tmp_path):
|
||||
p = tmp_path / "s.json"
|
||||
spine.save(seeded, p)
|
||||
loaded = spine.load(p)
|
||||
assert loaded == spine.load(p) # byte-stable across loads
|
||||
assert spine.entity(loaded, "module:m_a")["kind"] == "module"
|
||||
|
||||
def test_ownership_lookup(self, seeded):
|
||||
subs = [r["to"] for r in spine.relationships(
|
||||
seeded, from_id="module:m_a", type="belongs_to")]
|
||||
assert subs == ["subsystem:alpha"]
|
||||
|
||||
def test_dependency_lookup(self, seeded):
|
||||
spine.add_relationship(seeded, type="depends_on", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["ast"])
|
||||
deps = [r["to"] for r in spine.relationships(
|
||||
seeded, from_id="module:m_a", type="depends_on")]
|
||||
assert deps == ["module:m_b"]
|
||||
|
||||
def test_consumer_lookup(self, seeded):
|
||||
# "consumed_by" edges run provider → consumer: m_a is consumed by m_b
|
||||
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["ast"])
|
||||
consumers = [r["to"] for r in spine.relationships(
|
||||
seeded, from_id="module:m_a", type="consumed_by")]
|
||||
assert consumers == ["module:m_b"]
|
||||
|
||||
def test_missing_entity_is_not_found(self, seeded):
|
||||
assert spine.entity(seeded, "module:ghost") is None
|
||||
assert spine.relationships(seeded, from_id="module:ghost") == []
|
||||
|
||||
|
||||
# ── open-world extensibility (the Prime Directive) ────────────────────────
|
||||
|
||||
class TestOpenWorld:
|
||||
def test_unknown_kind_relationship_and_metadata_survive_roundtrip(self, seeded, tmp_path):
|
||||
"""THE extensibility test: a synthetic future capability Spine core
|
||||
was never written to understand enters as DATA — no core change."""
|
||||
spine.add_entity(
|
||||
seeded, id="capability:quantum_resonator_cache", kind="quantum_resonator_cache",
|
||||
tags=["synthetic-future"], maturity="DECLARED",
|
||||
identities={"resonance": "7.83Hz", "home_node": "orb-9"},
|
||||
evidence_refs=["synthetic:proof"],
|
||||
ext={"entanglement_pair": "module:m_a", "wibble_level": 11,
|
||||
"future_field_undefined_in_v1": {"nested": True}})
|
||||
spine.add_relationship(
|
||||
seeded, type="harmonizes_with", from_id="capability:quantum_resonator_cache",
|
||||
to_id="module:m_a", evidence_refs=["synthetic:resonance-proof"],
|
||||
ext={"coupling": "0.99", "unknown_later_field": [1, 2, 3]})
|
||||
|
||||
p = tmp_path / "roundtrip.json"
|
||||
spine.save(seeded, p)
|
||||
loaded = spine.load(p)
|
||||
|
||||
e = spine.entity(loaded, "capability:quantum_resonator_cache")
|
||||
assert e["kind"] == "quantum_resonator_cache" # unknown kind preserved
|
||||
assert e["ext"]["wibble_level"] == 11 # unknown metadata preserved
|
||||
assert e["ext"]["future_field_undefined_in_v1"]["nested"] is True
|
||||
assert e["identities"]["resonance"] == "7.83Hz"
|
||||
rels = spine.relationships(loaded, type="harmonizes_with")
|
||||
assert len(rels) == 1 # unknown relationship type preserved
|
||||
assert rels[0]["ext"]["coupling"] == "0.99"
|
||||
assert rels[0]["to"] == "module:m_a" # linked to a real entity
|
||||
|
||||
# delta sees it
|
||||
base = spine.new_doc()
|
||||
d = spine.diff(base, loaded)
|
||||
assert "capability:quantum_resonator_cache" in d["entities_added"]
|
||||
assert "harmonizes_with|capability:quantum_resonator_cache|module:m_a" \
|
||||
in d["relationships_added"]
|
||||
|
||||
# Scaffolding-style consumption: generic queries work on unknown kinds
|
||||
assert spine.relationships(loaded, from_id="capability:quantum_resonator_cache")
|
||||
|
||||
def test_no_closed_enum_in_core(self):
|
||||
"""Core must not hardcode today's ontology as a whitelist."""
|
||||
source = (ROOT / "src" / "rune" / "spine.py").read_text()
|
||||
# the maturity ladder is a scale, not an entity-kind ontology
|
||||
assert "VALID_KINDS" not in source and "VALID_RELATIONSHIP_TYPES" not in source
|
||||
|
||||
|
||||
# ── evidence / maturity semantics ─────────────────────────────────────────
|
||||
|
||||
class TestMaturity:
|
||||
def test_no_evidence_no_promotion(self, seeded):
|
||||
with pytest.raises(ValueError, match="requires evidence"):
|
||||
spine.promote(seeded, "module:m_a", "PRESENT", "")
|
||||
|
||||
def test_no_evidence_means_no_live_proven(self, seeded):
|
||||
e = spine.entity(seeded, "module:m_a")
|
||||
assert e["maturity"] == "DECLARED" # nothing observed: stays DECLARED
|
||||
|
||||
def test_ladder_is_monotonic_with_evidence(self, seeded):
|
||||
for state, ev in [("PRESENT", "src"), ("WIRED", "imports"), ("EXERCISED", "tests")]:
|
||||
spine.promote(seeded, "module:m_a", state, ev)
|
||||
assert spine.entity(seeded, "module:m_a")["maturity"] == "EXERCISED"
|
||||
|
||||
def test_demotion_rejected_and_recorded(self, seeded):
|
||||
spine.promote(seeded, "module:m_a", "WIRED", "imports-x")
|
||||
e = spine.promote(seeded, "module:m_a", "DECLARED", "downgrade-attempt")
|
||||
assert e["maturity"] == "WIRED" # not silently demoted
|
||||
assert any(o["kind"] == "maturity_demotion_rejected"
|
||||
for o in seeded["observations"])
|
||||
|
||||
def test_unknown_maturity_state_rejected(self, seeded):
|
||||
with pytest.raises(ValueError, match="unknown maturity"):
|
||||
spine.promote(seeded, "module:m_a", "TRANSCENDENT", "vibes")
|
||||
|
||||
|
||||
# ── deployed identity observation ─────────────────────────────────────────
|
||||
|
||||
class TestDeployedObservation:
|
||||
def test_observes_fixture_identity(self, doc, tmp_path, monkeypatch):
|
||||
dep = tmp_path / "deploy"
|
||||
(dep / "src" / "rune").mkdir(parents=True)
|
||||
(dep / "src" / "rune" / "_build_identity.json").write_text(json.dumps(
|
||||
{"commit": "a" * 40, "version": "9.9.9", "deployed_at": "T"}))
|
||||
monkeypatch.setattr(spine, "DEPLOYED_LINK", dep)
|
||||
obs = spine._deployed_observation()
|
||||
assert obs["deployed_sha"] == "a" * 12 and obs["deployed_version"] == "9.9.9"
|
||||
|
||||
def test_absent_deployment_is_none_not_invented(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(spine, "DEPLOYED_LINK", tmp_path / "nonexistent")
|
||||
assert spine._deployed_observation() is None
|
||||
|
||||
def test_deployed_identity_drift_detected(self, seeded, tmp_path, monkeypatch):
|
||||
dep = tmp_path / "deploy"
|
||||
(dep / "src" / "rune").mkdir(parents=True)
|
||||
(dep / "src" / "rune" / "_build_identity.json").write_text(json.dumps(
|
||||
{"commit": "b" * 40, "version": "0.0.1-test-mismatch", "deployed_at": "T"}))
|
||||
monkeypatch.setattr(spine, "DEPLOYED_LINK", dep)
|
||||
monkeypatch.setattr(spine, "ROOT", tmp_path) # no pyproject → no source version
|
||||
# no version drift observable without source; identity observation works
|
||||
obs = spine._deployed_observation()
|
||||
assert obs["deployed_version"] == "0.0.1-test-mismatch"
|
||||
|
||||
|
||||
# ── drift ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestDrift:
|
||||
def test_registered_but_absent(self, seeded, monkeypatch):
|
||||
monkeypatch.setattr(spine, "_canary_registry", lambda: ["m_ghost"])
|
||||
monkeypatch.setattr(spine, "SRC_DIR", tmp_src := ROOT / "src" / "rune") # real dir
|
||||
d = spine.drift(seeded)
|
||||
assert any(x["class"] == "registered_but_absent" and x["entity"] == "module:m_ghost"
|
||||
for x in d)
|
||||
|
||||
def test_relationship_disappeared(self, seeded, monkeypatch):
|
||||
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["stale"])
|
||||
monkeypatch.setattr(spine, "_import_graph", lambda: {"m_b": set()})
|
||||
monkeypatch.setattr(spine, "_canary_registry", lambda: [])
|
||||
monkeypatch.setattr(spine, "SRC_DIR", ROOT / "src" / "rune")
|
||||
d = spine.drift(seeded)
|
||||
assert any(x["class"] == "relationship_disappeared" for x in d)
|
||||
|
||||
def test_stale_evidence_does_not_prove_live(self, seeded):
|
||||
"""A stale evidence ref must never read as LIVE_PROVEN."""
|
||||
e = spine.entity(seeded, "module:m_a")
|
||||
e["evidence_refs"].append("deployed-tree:deadbeef-old-observation")
|
||||
assert e["maturity"] == "DECLARED" # evidence ref alone promotes nothing
|
||||
# LIVE_PROVEN only via explicit evidence-gated promotion
|
||||
spine.promote(seeded, "module:m_a", "PRESENT", "fresh-source-check")
|
||||
assert spine.entity(seeded, "module:m_a")["maturity"] == "PRESENT"
|
||||
|
||||
def test_declared_but_unwired_capability(self, seeded):
|
||||
"""Registered with no wiring evidence → must not read WIRED."""
|
||||
spine.add_entity(seeded, id="capability:orphan_feature", kind="capability",
|
||||
maturity="DECLARED", evidence_refs=["registry:orphan"])
|
||||
assert spine.entity(seeded, "capability:orphan_feature")["maturity"] == "DECLARED"
|
||||
|
||||
|
||||
# ── snapshots / deltas ────────────────────────────────────────────────────
|
||||
|
||||
class TestSnapshots:
|
||||
def test_snapshot_deterministic(self, seeded, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(spine, "SNAPSHOTS", tmp_path / "snaps")
|
||||
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
|
||||
seeded["generated_at"] = "FIXED"
|
||||
p1 = spine.snapshot(seeded, "t")
|
||||
p2 = spine.snapshot(seeded, "t")
|
||||
assert p1.read_bytes() == p2.read_bytes() # byte-identical
|
||||
|
||||
def test_diff_categories(self, seeded):
|
||||
old = json.loads(json.dumps(seeded))
|
||||
spine.add_entity(seeded, id="module:m_new", kind="module", maturity="DECLARED")
|
||||
spine.promote(seeded, "module:m_a", "WIRED", "new-import-evidence")
|
||||
spine.add_relationship(seeded, type="depends_on", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["ast"])
|
||||
d = spine.diff(old, seeded)
|
||||
assert d["entities_added"] == ["module:m_new"]
|
||||
assert d["relationships_added"] == ["depends_on|module:m_a|module:m_b"]
|
||||
assert {"id": "module:m_a", "from": "DECLARED", "to": "WIRED"} \
|
||||
in d["maturity_changed"]
|
||||
assert d["entities_removed"] == []
|
||||
|
||||
def test_diff_is_deterministic(self, seeded):
|
||||
old = json.loads(json.dumps(seeded))
|
||||
d1 = spine.diff(old, seeded)
|
||||
d2 = spine.diff(old, seeded)
|
||||
assert d1 == d2
|
||||
|
||||
|
||||
# ── schema versioning ─────────────────────────────────────────────────────
|
||||
|
||||
class TestSchemaVersion:
|
||||
def test_save_stamps_version(self, seeded, tmp_path):
|
||||
p = spine.save(seeded, tmp_path / "s.json")
|
||||
assert json.loads(p.read_text())["schema_version"] == spine.SCHEMA_VERSION
|
||||
|
||||
def test_future_version_refused_for_rewrite(self, seeded, tmp_path):
|
||||
seeded["schema_version"] = 99
|
||||
with pytest.raises(ValueError, match="schema_version"):
|
||||
spine.save(seeded, tmp_path / "future.json")
|
||||
|
||||
def test_unknown_fields_of_document_survive(self, seeded, tmp_path):
|
||||
seeded["a_field_spine_v2_will_understand"] = {"x": 1}
|
||||
p = spine.save(seeded, tmp_path / "s.json")
|
||||
# loader tolerates unknown document fields (no whitelist strip)
|
||||
loaded = json.loads(p.read_text())
|
||||
assert loaded["a_field_spine_v2_will_understand"] == {"x": 1}
|
||||
|
||||
|
||||
# ── ingest / compatibility (umbrella migration) ───────────────────────────
|
||||
|
||||
class TestUmbrellaMigration:
|
||||
def test_ingest_refuses_generated_view(self, tmp_path):
|
||||
generated = {"_generated_by_spine": True, "umbrellas": {}}
|
||||
p = tmp_path / "view.json"
|
||||
p.write_text(json.dumps(generated))
|
||||
with pytest.raises(ValueError, match="refusing to ingest"):
|
||||
spine.ingest_umbrellas(spine.new_doc(), p)
|
||||
|
||||
def test_ingest_preserves_structure(self, tmp_path):
|
||||
manifest = {"umbrellas": {
|
||||
"alpha": {"description": "A", "modules": ["m_a", "m_b"],
|
||||
"depends_on": ["beta"], "mirrors": ["shared/x.py"]},
|
||||
"beta": {"description": "B", "modules": [], "depends_on": [], "mirrors": []}}}
|
||||
p = tmp_path / "umbrellas.json"
|
||||
p.write_text(json.dumps(manifest))
|
||||
doc = spine.new_doc()
|
||||
n = spine.ingest_umbrellas(doc, p)
|
||||
assert n == 2
|
||||
# ownership preserved
|
||||
assert [r["to"] for r in spine.relationships(
|
||||
doc, from_id="module:m_a", type="belongs_to")] == ["subsystem:alpha"]
|
||||
# subsystem deps preserved
|
||||
assert [r["to"] for r in spine.relationships(
|
||||
doc, from_id="subsystem:alpha", type="depends_on")] == ["subsystem:beta"]
|
||||
# mirrors preserved
|
||||
assert [r["to"] for r in spine.relationships(
|
||||
doc, from_id="subsystem:alpha", type="mirrors")] == ["file:shared/x.py"]
|
||||
|
||||
def test_export_reproduces_equivalent_view(self, tmp_path):
|
||||
manifest = {"umbrellas": {
|
||||
"alpha": {"description": "A", "modules": ["m_a"],
|
||||
"depends_on": ["beta"], "mirrors": ["shared/x.py"]},
|
||||
"beta": {"description": "B", "modules": ["m_b"],
|
||||
"depends_on": [], "mirrors": []}}}
|
||||
p = tmp_path / "umbrellas.json"
|
||||
p.write_text(json.dumps(manifest))
|
||||
doc = spine.new_doc()
|
||||
spine.ingest_umbrellas(doc, p)
|
||||
view = spine.export_umbrellas(doc)
|
||||
assert view["_generated_by_spine"] is True
|
||||
assert view["umbrellas"]["alpha"]["modules"] == ["m_a"]
|
||||
assert view["umbrellas"]["alpha"]["depends_on"] == ["beta"]
|
||||
assert view["umbrellas"]["alpha"]["mirrors"] == ["shared/x.py"]
|
||||
assert view["umbrellas"]["beta"]["modules"] == ["m_b"]
|
||||
# round-trip fidelity: the generated view re-ingests to the same graph
|
||||
view_path = tmp_path / "view.json"
|
||||
view_path.write_text(json.dumps(view))
|
||||
doc2 = spine.new_doc()
|
||||
# (direct ingest of the view is refused; equivalence is proven by
|
||||
# comparing the exported view against the original manual truth)
|
||||
assert view["umbrellas"] == manifest["umbrellas"]
|
||||
|
||||
def test_export_never_overwrites_manual_manifest(self, seeded, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(spine, "UMBRELLA_MANIFEST", tmp_path / "manual.json")
|
||||
(tmp_path / "manual.json").write_text(json.dumps({"umbrellas": {}}))
|
||||
# main() with no path → refuses (manual manifest protection)
|
||||
rc = spine.main(["export-umbrellas"])
|
||||
assert rc == 1
|
||||
|
||||
|
||||
# ── Scaffolding consumes Spine ────────────────────────────────────────────
|
||||
|
||||
class TestScaffoldingConsumesSpine:
|
||||
def test_scaffold_projection_reads_spine_store(self, seeded, tmp_path, monkeypatch):
|
||||
"""Scaffolding's structural facts must come from the Spine store,
|
||||
not from re-deriving authorities independently."""
|
||||
p = tmp_path / "current.json"
|
||||
spine.save(seeded, p)
|
||||
monkeypatch.setattr(spine, "CURRENT", p)
|
||||
monkeypatch.setattr(spine, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(spine, "SRC_DIR", tmp_path / "src" / "rune")
|
||||
(tmp_path / "src" / "rune").mkdir(parents=True, exist_ok=True)
|
||||
(tmp_path / "src" / "rune" / "m_a.py").write_text("x = 1\n")
|
||||
(tmp_path / "tests").mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(spine, "TESTS_DIR", tmp_path / "tests")
|
||||
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
|
||||
# fresh store → Scaffolding builds it deterministically
|
||||
doc = spine.build()
|
||||
spine.save(doc)
|
||||
# a component card must be answerable from the store alone
|
||||
card_deps = [r["to"] for r in spine.relationships(
|
||||
doc, from_id="module:m_a", type="depends_on")]
|
||||
subs = [r["to"] for r in spine.relationships(
|
||||
doc, from_id="module:m_a", type="belongs_to")]
|
||||
assert subs == ["subsystem:alpha"]
|
||||
assert isinstance(card_deps, list)
|
||||
# maturity visible to Scaffolding consumers — the fixture created
|
||||
# src/rune/m_a.py, so the evidence gate correctly promotes PRESENT
|
||||
assert spine.entity(doc, "module:m_a")["maturity"] == "PRESENT"
|
||||
|
||||
def test_scaffold_impact_uses_spine_store_not_umbrellas(self, monkeypatch, tmp_path):
|
||||
"""The real integration: scripts/scaffold_projection.py resolves
|
||||
ownership from the Spine store. Proven by pointing the store at a
|
||||
fixture world and checking the card reflects SPINE data (and that
|
||||
a Spine-only entity — absent from any umbrella file — is served)."""
|
||||
import importlib
|
||||
doc = spine.new_doc()
|
||||
spine.add_entity(doc, id="subsystem:spine_test_world", kind="subsystem",
|
||||
identities={"description": "fixture"})
|
||||
spine.add_entity(doc, id="module:widget_core", kind="module", maturity="EXERCISED",
|
||||
identities={"source": "src/rune/widget_core.py"},
|
||||
evidence_refs=["fixture"])
|
||||
spine.add_relationship(doc, type="belongs_to", from_id="module:widget_core",
|
||||
to_id="subsystem:spine_test_world", evidence_refs=["fixture"])
|
||||
spine.add_relationship(doc, type="consumed_by", from_id="module:widget_core",
|
||||
to_id="module:widget_user", evidence_refs=["fixture"])
|
||||
store = tmp_path / "current.json"
|
||||
spine.save(doc, store)
|
||||
monkeypatch.setattr(spine, "CURRENT", store)
|
||||
|
||||
import scripts.scaffold_projection as sp # noqa: E402
|
||||
importlib.reload(sp)
|
||||
monkeypatch.setattr(sp, "ROOT", tmp_path)
|
||||
card = sp.component_card("widget_core", spine.load(store))
|
||||
assert card["subsystem"] == "spine_test_world" # from Spine, not umbrellas.json
|
||||
assert card["consumers"] == ["widget_user"] # from Spine relationships
|
||||
assert card["spine_maturity"] == "EXERCISED" # evidence state surfaced
|
||||
|
||||
|
||||
# ── graph separation + cross-linking (addendum 1) ─────────────────────────
|
||||
|
||||
class TestGraphSeparation:
|
||||
def test_spine_queries_need_no_memory_infrastructure(self, seeded):
|
||||
"""Structural lookup works on the Spine store alone — no Memory
|
||||
fixture, no retrieval, no inference."""
|
||||
q = spine.query(seeded, "module:m_a")
|
||||
assert q["owner"] == ["subsystem:alpha"]
|
||||
assert q["state"]["maturity"] == "DECLARED"
|
||||
|
||||
def test_stable_ids_survive_full_cycle(self, seeded, tmp_path):
|
||||
p = spine.save(seeded, tmp_path / "s.json")
|
||||
loaded = spine.load(p)
|
||||
snap = tmp_path / "snap.json"
|
||||
spine.save(loaded, snap)
|
||||
d = spine.diff(spine.load(p), spine.load(snap))
|
||||
assert d["entities_removed"] == [] and d["entities_added"] == []
|
||||
assert spine.entity(spine.load(snap), "module:m_a") is not None
|
||||
|
||||
def test_spine_uri_cross_reference_without_authority_duplication(self, seeded):
|
||||
"""A future Memory record references a Spine entity by URI — the
|
||||
structural edge stays in Spine, the reference carries no authority."""
|
||||
uri = spine.spine_uri("module:m_a")
|
||||
assert uri == "spine://module/m_a"
|
||||
assert spine.resolve_uri(uri) == "module:m_a"
|
||||
# round-trip: resolve → the same entity Spine owns
|
||||
assert spine.entity(seeded, spine.resolve_uri(uri))["kind"] == "module"
|
||||
with pytest.raises(ValueError, match="not a spine URI"):
|
||||
spine.resolve_uri("memory://lesson/42")
|
||||
|
||||
def test_memory_association_does_not_expand_frontier(self, seeded):
|
||||
"""A hypothetical Memory-style association edge must NOT expand the
|
||||
structural impact frontier (noisy impact = broken resolution)."""
|
||||
spine.add_relationship(seeded, type="related_memory", from_id="module:m_a",
|
||||
to_id="memory:old-incident-42", evidence_refs=["hypothetical"])
|
||||
f = spine.impact_frontier(seeded, ["module:m_a"])
|
||||
assert "memory:old-incident-42" not in f["level1_obligations"]
|
||||
assert f["traversal_scope"] == "spine-structural-only"
|
||||
# while a REAL structural edge does expand
|
||||
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["ast"])
|
||||
f2 = spine.impact_frontier(seeded, ["module:m_a"])
|
||||
assert "module:m_b" in f2["level1_obligations"]
|
||||
|
||||
def test_unknown_relationship_is_obligation_bearing_fail_loud(self, seeded):
|
||||
"""A previously unseen relationship type defaults to frontier
|
||||
visibility — new structural relations deserve inspection until
|
||||
evidence reclassifies them (data-driven semantics, open world)."""
|
||||
spine.add_relationship(seeded, type="contextualises", from_id="module:m_a",
|
||||
to_id="subsystem:beta", evidence_refs=["unseen-type"])
|
||||
f = spine.impact_frontier(seeded, ["module:m_a"])
|
||||
assert "subsystem:beta" in f["level1_obligations"]
|
||||
|
||||
def test_structural_facts_survive_hypothetical_memory_archive(self, seeded, tmp_path):
|
||||
"""Spine retains current structural truth regardless of any future
|
||||
Memory salience/decay/archive state — Spine is not Memory-dependent."""
|
||||
spine.promote(seeded, "module:m_a", "WIRED", "import-evidence")
|
||||
p = spine.save(seeded, tmp_path / "s.json")
|
||||
# (hypothetical: Memory archives the story of how this was learned)
|
||||
assert spine.entity(spine.load(p), "module:m_a")["maturity"] == "WIRED"
|
||||
|
||||
|
||||
# ── contracts, task binding, frontier, predicted-vs-observed (T22) ────────
|
||||
|
||||
class TestContractsAndFrontier:
|
||||
def _contract_world(self, doc):
|
||||
spine.add_entity(doc, id="contract:chat_transport", kind="contract",
|
||||
evidence_refs=["protocol:ChatTransport"], maturity="DECLARED")
|
||||
spine.add_entity(doc, id="module:transport_a", kind="module", maturity="DECLARED")
|
||||
spine.add_entity(doc, id="module:transport_b", kind="module", maturity="DECLARED")
|
||||
spine.add_entity(doc, id="module:operator", kind="module", maturity="DECLARED")
|
||||
spine.add_relationship(doc, type="implements", from_id="module:transport_a",
|
||||
to_id="contract:chat_transport", evidence_refs=["ast"])
|
||||
spine.add_relationship(doc, type="implements", from_id="module:transport_b",
|
||||
to_id="contract:chat_transport", evidence_refs=["ast"])
|
||||
spine.add_relationship(doc, type="consumes", from_id="module:operator",
|
||||
to_id="contract:chat_transport", evidence_refs=["ast"])
|
||||
return doc
|
||||
|
||||
def test_contract_change_expands_to_all_parties(self, seeded):
|
||||
"""A CONTRACT change deserves every implementer + consumer's inspection."""
|
||||
doc = self._contract_world(seeded)
|
||||
f = spine.impact_frontier(doc, ["contract:chat_transport"])
|
||||
assert {"module:transport_a", "module:transport_b", "module:operator"} \
|
||||
<= set(f["level1_obligations"])
|
||||
|
||||
def test_implementation_change_is_bounded(self, seeded):
|
||||
"""An IMPLEMENTATION change (one transport) touches the contract but
|
||||
the frontier stays bounded — it does not pull sibling implementers
|
||||
in as direct obligations (they see the contract, not this change)."""
|
||||
doc = self._contract_world(seeded)
|
||||
f = spine.impact_frontier(doc, ["module:transport_a"])
|
||||
# HARDENED SEMANTICS (T1A): implementation change, contract preserved
|
||||
# → contract is a direct obligation; siblings are CONTEXTUAL, never
|
||||
# direct. (The pre-hardening expectation that siblings land in
|
||||
# level1 encoded the wrong semantics — see final report.)
|
||||
assert "contract:chat_transport" in f["level1_obligations"]
|
||||
assert "module:transport_b" in f["contextual_not_obligations"]
|
||||
assert "module:transport_b" not in f["level1_obligations"]
|
||||
assert f["change_kind"] == "implementation_change"
|
||||
assert f["level0_direct"] == ["module:transport_a"]
|
||||
|
||||
def test_task_binding_and_frontier(self, seeded):
|
||||
doc = self._contract_world(seeded)
|
||||
spine.bind_task(doc, "TASK-123", ["module:transport_a"], evidence_ref="issue")
|
||||
assert spine.task_entities(doc, "TASK-123") == ["module:transport_a"]
|
||||
seeds = spine.task_entities(doc, "TASK-123")
|
||||
f = spine.impact_frontier(doc, seeds)
|
||||
assert "contract:chat_transport" in f["level1_obligations"]
|
||||
|
||||
def test_predicted_vs_observed_flags_misses_as_candidates(self, seeded):
|
||||
old = json.loads(json.dumps(seeded))
|
||||
predicted = spine.impact_frontier(old, ["module:m_a"])
|
||||
# the change reveals a consequence the structure didn't predict:
|
||||
# m_b (a consumer nobody had recorded) starts consuming m_a
|
||||
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["observed:real-run"])
|
||||
r = spine.predicted_vs_observed(predicted, seeded, ["module:m_a"])
|
||||
assert "module:m_b" in r["missed"]
|
||||
assert r["candidate_corrections"][0]["status"] == "CANDIDATE"
|
||||
assert "evidence required" in r["candidate_corrections"][0]["note"]
|
||||
|
||||
def test_structural_event_emission_is_generic(self, seeded):
|
||||
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=["ast"])
|
||||
events = spine.structural_events(seeded)
|
||||
kinds = {e["type"] for e in events}
|
||||
assert "entity_observed" in kinds and "relationship_observed" in kinds
|
||||
# dumb typed facts — no salience, no policy fields
|
||||
rel_event = next(e for e in events if e["type"] == "relationship_observed"
|
||||
and e["relationship"] == "consumed_by")
|
||||
assert rel_event["from"] == "module:m_a" and rel_event["to"] == "module:m_b"
|
||||
|
||||
def test_evidence_conflict_drift(self, seeded):
|
||||
e = spine.entity(seeded, "module:m_a")
|
||||
e["maturity"] = "LIVE_PROVEN"
|
||||
e["evidence_refs"] = [] # claim without proof
|
||||
d = spine.drift(seeded)
|
||||
assert any(x["class"] == "evidence_conflict" and x["entity"] == "module:m_a"
|
||||
for x in d)
|
||||
|
||||
def test_intrinsic_query_answers_question_set_deterministically(self, seeded):
|
||||
"""THE acceptance property: the representative question set answered
|
||||
from the store alone — no LLM, no Memory, no rediscovery."""
|
||||
doc = self._contract_world(seeded)
|
||||
q = spine.query(doc, "module:transport_a")
|
||||
assert q["kind"] == "module"
|
||||
assert q["contracts_implements"] == ["contract:chat_transport"]
|
||||
assert q["state"]["maturity"] == "DECLARED"
|
||||
assert q["evidence"] == []
|
||||
assert q["impact_frontier"]["level0_direct"] == ["module:transport_a"]
|
||||
assert q["uri"] == "spine://module/transport_a"
|
||||
|
||||
def test_synthetic_future_capability_flows_through_frontier(self, seeded):
|
||||
"""Extensibility end-to-end: the unknown capability registers,
|
||||
round-trips, binds to a task, expands a frontier, and survives
|
||||
delta — with zero Spine core changes (kind/relationship unseen)."""
|
||||
spine.add_entity(seeded, id="capability:quantum_resonator_cache",
|
||||
kind="quantum_resonator_cache", maturity="DECLARED",
|
||||
identities={"resonance": "7.83Hz"},
|
||||
evidence_refs=["synthetic"], ext={"wibble": 11})
|
||||
spine.add_relationship(seeded, type="harmonizes_with",
|
||||
from_id="capability:quantum_resonator_cache",
|
||||
to_id="module:m_a", evidence_refs=["synthetic"])
|
||||
spine.bind_task(seeded, "TASK-FUTURE", ["capability:quantum_resonator_cache"])
|
||||
q = spine.query(seeded, "capability:quantum_resonator_cache")
|
||||
assert q["kind"] == "quantum_resonator_cache" # unknown kind queried generically
|
||||
assert q["state"]["maturity"] == "DECLARED"
|
||||
f = spine.impact_frontier(seeded, spine.task_entities(seeded, "TASK-FUTURE"))
|
||||
assert "module:m_a" in f["level1_obligations"] # unseen rel = obligation-bearing
|
||||
|
||||
|
||||
# ── HARDENING T1: contract-change vs implementation-change semantics ──────
|
||||
|
||||
class TestHardeningContractSemantics:
|
||||
def _world(self, seeded):
|
||||
spine.add_entity(seeded, id="contract:chat_transport", kind="contract",
|
||||
evidence_refs=["protocol"], maturity="DECLARED")
|
||||
for m in ("transport_a", "transport_b"):
|
||||
spine.add_entity(seeded, id=f"module:{m}", kind="module", maturity="DECLARED")
|
||||
spine.add_relationship(seeded, type="implements", from_id=f"module:{m}",
|
||||
to_id="contract:chat_transport", evidence_refs=["ast"])
|
||||
spine.add_entity(seeded, id="module:operator", kind="module", maturity="DECLARED")
|
||||
spine.add_relationship(seeded, type="consumes", from_id="module:operator",
|
||||
to_id="contract:chat_transport", evidence_refs=["ast"])
|
||||
return seeded
|
||||
|
||||
def test_A_implementation_change_siblings_not_direct(self, seeded):
|
||||
doc = self._world(seeded)
|
||||
f = spine.impact_frontier(doc, ["module:transport_a"])
|
||||
assert f["change_kind"] == "implementation_change"
|
||||
assert f["level0_direct"] == ["module:transport_a"]
|
||||
assert "contract:chat_transport" in f["level1_obligations"]
|
||||
assert "module:transport_b" in f["contextual_not_obligations"]
|
||||
assert "module:transport_b" not in f["level1_obligations"]
|
||||
# X (consumer) is reached through the contract's contextual parties
|
||||
# or obligations per evidence — must NOT be silently direct here
|
||||
assert "module:operator" in f["contextual_not_obligations"] or \
|
||||
"module:operator" in f["level1_obligations"]
|
||||
|
||||
def test_B_contract_change_all_parties_direct(self, seeded):
|
||||
doc = self._world(seeded)
|
||||
f = spine.impact_frontier(doc, ["contract:chat_transport"])
|
||||
assert f["change_kind"] == "contract_change"
|
||||
for party in ("module:transport_a", "module:transport_b", "module:operator"):
|
||||
assert party in f["level1_obligations"], party
|
||||
|
||||
def test_C_contract_violation_escalates(self, seeded):
|
||||
doc = self._world(seeded)
|
||||
f = spine.impact_frontier(doc, ["module:transport_a"],
|
||||
change_kind="contract_violation")
|
||||
# verification proved the implementation breaks the contract:
|
||||
# every party becomes a direct obligation
|
||||
assert f["change_kind"] == "contract_violation"
|
||||
for party in ("module:transport_b", "module:operator"):
|
||||
assert party in f["level1_obligations"], party
|
||||
|
||||
def test_kind_inference_is_deterministic(self, seeded):
|
||||
doc = self._world(seeded)
|
||||
assert spine.impact_frontier(doc, ["contract:chat_transport"])["change_kind"] == "contract_change"
|
||||
assert spine.impact_frontier(doc, ["module:transport_a"])["change_kind"] == "implementation_change"
|
||||
|
||||
|
||||
# ── HARDENING T3/T4: projector ownership, reconciliation, state semantics ─
|
||||
|
||||
class TestHardeningReconciliation:
|
||||
def _fact(self, doc, type="depends_on", frm="module:x", to="module:y", auth="proj-x"):
|
||||
spine.add_entity(doc, id=frm, kind="module", maturity="DECLARED")
|
||||
spine.add_entity(doc, id=to, kind="module", maturity="DECLARED")
|
||||
r = spine.add_relationship(doc, type=type, from_id=frm, to_id=to,
|
||||
evidence_refs=["fixture"])
|
||||
r["authority"] = auth
|
||||
r["status"] = "CURRENT"
|
||||
return r
|
||||
|
||||
def test_foreign_projector_refresh_never_deletes_other_authority_facts(self, seeded):
|
||||
"""Projector Y's refresh must not touch projector X's facts —
|
||||
the reconciliation boundary is AUTHORITY OWNERSHIP."""
|
||||
doc = seeded
|
||||
self._fact(doc, auth="proj-x")
|
||||
foreign = spine.add_relationship(doc, type="related_thing", from_id="module:x",
|
||||
to_id="module:y", evidence_refs=["fixture"])
|
||||
spine._authority_observed(doc, foreign, "proj-y") # proj-y ran and owns it
|
||||
# proj-x refresh observes its own fact; reconcile must leave proj-y's
|
||||
# fact alone — only proj-y's own pass can stale it
|
||||
spine._doc_authorities(doc).setdefault("proj-x", {"generation": 1, "observed": ["depends_on|module:x|module:y"]})
|
||||
staled = spine.reconcile_authorities(doc)
|
||||
assert foreign["status"] == "CURRENT" # foreign fact survives
|
||||
assert all(s["authority"] != "proj-y" for s in staled)
|
||||
|
||||
def test_owner_stopping_emission_stales_not_deletes(self, seeded):
|
||||
"""Projector X later stops observing its fact → STALE (marked,
|
||||
reported in drift, excluded from frontier) — not silently deleted,
|
||||
not silently current."""
|
||||
doc = seeded
|
||||
self._fact(doc, auth="proj-x")
|
||||
spine._doc_authorities(doc)["proj-x"] = {"generation": 2, "observed": []}
|
||||
staled = spine.reconcile_authorities(doc)
|
||||
assert len(staled) == 1
|
||||
f = spine.impact_frontier(doc, ["module:x"])
|
||||
assert "module:y" not in f["level1_obligations"] # stale ≠ current truth
|
||||
d = spine.drift(doc)
|
||||
assert any(x["class"] == "stale_fact" for x in d)
|
||||
|
||||
def test_projector_failure_does_not_masquerade_as_fresh(self, seeded):
|
||||
doc = seeded
|
||||
self._fact(doc, auth="proj-x")
|
||||
spine._doc_authorities(doc)["proj-x"] = {"generation": 1, "observed": ["depends_on|module:x|module:y"]}
|
||||
spine.authority_failed(doc, "proj-x", "source tree unreachable")
|
||||
h = spine.authority_health(doc)
|
||||
assert h["proj-x"]["status"] == "DEGRADED"
|
||||
r = [r for r in doc["relationships"] if r.get("authority") == "proj-x"][0]
|
||||
assert r["status"] == "STALE"
|
||||
f = spine.impact_frontier(doc, ["module:x"])
|
||||
assert "module:y" not in f["level1_obligations"]
|
||||
assert any(o["kind"] == "authority_degraded" for o in doc["observations"])
|
||||
|
||||
def test_manual_fact_without_authority_survives(self, seeded):
|
||||
doc = seeded
|
||||
r = self._fact(doc, auth=None)
|
||||
r.pop("authority", None); r.pop("status", None)
|
||||
spine.reconcile_authorities(doc)
|
||||
assert r["status"] != "STALE" if "status" in r else True
|
||||
assert r in doc["relationships"]
|
||||
|
||||
def test_two_authorities_conflicting_preserve_both(self, seeded):
|
||||
spine.mark_conflict(seeded, "module:m_a", "registry", "exists",
|
||||
"runtime", "unavailable", "evidence:live-check")
|
||||
e = spine.entity(seeded, "module:m_a")
|
||||
assert e["status"] == "CONFLICTING"
|
||||
assert len(e["ext"]["conflicts"]) == 1 # BOTH preserved, not last-wins
|
||||
assert spine.resolve_status(e) == "CONFLICTING"
|
||||
q = spine.query(seeded, "module:m_a")
|
||||
assert q["state"]["resolved_status"] == "CONFLICTING"
|
||||
|
||||
def test_conflict_requires_evidence(self, seeded):
|
||||
with pytest.raises(ValueError, match="requires evidence"):
|
||||
spine.mark_conflict(seeded, "module:m_a", "a", "x", "b", "y", "")
|
||||
|
||||
def test_unknown_is_not_healthy(self, seeded):
|
||||
"""A never-observed entity is UNKNOWN — not current, not false."""
|
||||
spine.add_entity(seeded, id="module:never_checked", kind="module",
|
||||
maturity="DECLARED")
|
||||
assert spine.resolve_status(spine.entity(seeded, "module:never_checked")) == "UNKNOWN"
|
||||
|
||||
def test_live_proven_without_evidence_is_conflict_not_stale(self, seeded):
|
||||
"""Case A vs B distinction: no evidence at all = CONFLICTING claim;
|
||||
a projector that stopped refreshing = STALE. Different conditions."""
|
||||
spine.add_entity(seeded, id="module:no_proof", kind="module",
|
||||
maturity="LIVE_PROVEN") # zero evidence refs
|
||||
assert spine.resolve_status(spine.entity(seeded, "module:no_proof")) == "CONFLICTING"
|
||||
|
||||
|
||||
# ── HARDENING T5: durable identity — rename/alias/supersession ────────────
|
||||
|
||||
class TestHardeningDurableIdentity:
|
||||
def test_rename_old_spine_reference_stays_meaningful(self, seeded, tmp_path):
|
||||
"""Memory stores spine://module/memory_selector; the module is later
|
||||
renamed memory_orchestrator. The old reference must still resolve."""
|
||||
spine.alias(seeded, "module:m_a", "module:m_a_old_name")
|
||||
p = spine.save(seeded, tmp_path / "s.json")
|
||||
reloaded = spine.load(p)
|
||||
# old URI resolves to the same conceptual entity
|
||||
old_ref = spine.resolve_uri("spine://module/m_a_old_name")
|
||||
e = spine.entity(reloaded, old_ref)
|
||||
assert e is not None and e["id"] == "module:m_a"
|
||||
# survives another build cycle (aliases are data, carried forward)
|
||||
doc2 = spine.build.__wrapped__(reloaded) if hasattr(spine.build, "__wrapped__") else reloaded
|
||||
assert spine.entity(doc2, "module:m_a_old_name") is not None
|
||||
|
||||
def test_supersession_requires_evidence_and_preserves_history(self, seeded):
|
||||
spine.add_entity(seeded, id="module:new_thing", kind="module", maturity="DECLARED")
|
||||
with pytest.raises(ValueError, match="requires evidence"):
|
||||
spine.supersede(seeded, "module:new_thing", "module:m_a", "")
|
||||
spine.supersede(seeded, "module:new_thing", "module:m_a", "evidence:decision-record")
|
||||
old = spine.entity(seeded, "module:m_a")
|
||||
assert old is not None # never deleted
|
||||
assert old["superseded_by"] == "module:new_thing"
|
||||
assert old["status"] == "SUPERSEDED"
|
||||
# the supersedes relationship is data, round-trips, and is
|
||||
# non-obligation for frontier purposes (context, not live wiring)
|
||||
p = spine.save(seeded, Path(tempfile.mkdtemp()) / "s.json")
|
||||
assert spine.entity(spine.load(p), "module:m_a")["superseded_by"] == "module:new_thing"
|
||||
|
||||
def test_superseded_entity_not_treated_as_current(self, seeded):
|
||||
spine.add_entity(seeded, id="module:new_thing", kind="module", maturity="DECLARED")
|
||||
spine.supersede(seeded, "module:new_thing", "module:m_a", "evidence:decision")
|
||||
f = spine.impact_frontier(seeded, ["module:m_a"])
|
||||
# a superseded seed is still answerable, but its status is explicit
|
||||
assert spine.resolve_status(spine.entity(seeded, "module:m_a")) == "SUPERSEDED"
|
||||
|
||||
|
||||
# ── HARDENING T2: independent observed impact ─────────────────────────────
|
||||
|
||||
class TestHardeningObservedImpact:
|
||||
def test_observed_impact_comes_from_evidence_not_frontier_rerun(self, seeded):
|
||||
"""The observed side must be built from EXTERNAL evidence items —
|
||||
test failures, canary results, runtime errors — each naming its
|
||||
authority. The frontier function is not invoked for the observed set."""
|
||||
spine.add_entity(seeded, id="module:m_c", kind="module", maturity="DECLARED")
|
||||
ev = [{"entity": "module:m_b", "source": "pytest-failure",
|
||||
"authority": "test-suite", "detail": "test_consumer_lookup failed"},
|
||||
{"entity": "module:m_c", "source": "canary", "authority": "canary-runner",
|
||||
"detail": "canary_native red"}]
|
||||
r = spine.record_observed_impact(seeded, "TASK-X", ev)
|
||||
assert r["observed_entities"] == ["module:m_b", "module:m_c"]
|
||||
kinds = [o["kind"] for o in seeded["observations"]]
|
||||
assert kinds.count("observed_consequence") == 2
|
||||
# nothing was auto-promoted into structural relationships
|
||||
assert not spine.relationships(seeded, type="consumed_by",
|
||||
from_id="module:m_a", to_id="module:m_c")
|
||||
|
||||
def test_predicted_vs_observed_with_independent_evidence(self, seeded):
|
||||
old = json.loads(json.dumps(seeded))
|
||||
predicted = spine.impact_frontier(old, ["module:m_a"])
|
||||
# the change ran; independent evidence implicates an entity the
|
||||
# structure never predicted
|
||||
spine.record_observed_impact(seeded, "TASK-X", [
|
||||
{"entity": "module:m_b", "source": "runtime-error",
|
||||
"authority": "deployed-runtime", "detail": "import crash"}])
|
||||
# compare: prediction's frontier vs the EVIDENCE-derived set
|
||||
observed_set = {"module:m_b"}
|
||||
pred_set = {e for k in ("level0_direct", "level1_obligations",
|
||||
"contracts", "level2_verification")
|
||||
for e in predicted.get(k, [])}
|
||||
missed = observed_set - pred_set
|
||||
assert missed == {"module:m_b"}
|
||||
# and the comparison helper records it as CANDIDATE, not truth
|
||||
result = spine.predicted_vs_observed(predicted, seeded, ["module:m_a"])
|
||||
assert all(c["status"] == "CANDIDATE" for c in result["candidate_corrections"])
|
||||
|
||||
|
||||
# ── HARDENING: candidate relationships + event seam (T8) ──────────────────
|
||||
|
||||
class TestHardeningCandidatesAndEvents:
|
||||
def test_model_speculation_is_candidate_never_truth(self, seeded):
|
||||
"""A model-hypothesised relationship (no evidence) enters as
|
||||
CANDIDATE: representable, queryable, but NOT structural truth —
|
||||
excluded from impact frontiers until evidence promotes it."""
|
||||
spine.add_relationship(seeded, type="depends_on", from_id="module:m_a",
|
||||
to_id="module:m_b", evidence_refs=[]) # speculation
|
||||
rel = spine.relationships(seeded, from_id="module:m_a", type="depends_on")[0]
|
||||
assert rel["status"] == "CANDIDATE"
|
||||
f = spine.impact_frontier(seeded, ["module:m_a"])
|
||||
assert "module:m_b" not in f["level1_obligations"]
|
||||
# evidence arrives → promotion to CURRENT makes it real
|
||||
rel["evidence_refs"] = ["ast-import:m_a"]
|
||||
rel["status"] = "CURRENT"
|
||||
f2 = spine.impact_frontier(seeded, ["module:m_a"])
|
||||
assert "module:m_b" in f2["level1_obligations"]
|
||||
|
||||
def test_event_seam_carries_future_event_kinds_without_core_redesign(self, seeded):
|
||||
"""The seam must transport stale/conflict/supersession/degradation
|
||||
events as typed facts — future event kinds flow without policy."""
|
||||
spine.add_entity(seeded, id="module:new_thing", kind="module", maturity="DECLARED")
|
||||
spine.supersede(seeded, "module:new_thing", "module:m_a", "evidence:decision")
|
||||
spine.mark_conflict(seeded, "module:new_thing", "registry", "exists",
|
||||
"runtime", "unavailable", "evidence:check")
|
||||
spine._authority_observed(seeded, None, "proj-x")
|
||||
spine.authority_failed(seeded, "proj-x", "boom")
|
||||
events = spine.structural_events(seeded)
|
||||
kinds = {e["type"] for e in events}
|
||||
assert {"superseded", "authority_conflict", "authority_degraded"} <= kinds
|
||||
# all dumb typed facts: no salience/policy/wake fields anywhere
|
||||
for e in events:
|
||||
assert not {"salience", "attention", "wake", "priority"} & set(e.keys())
|
||||
|
||||
def test_intrinsic_query_on_real_store_is_fast_and_deterministic(self):
|
||||
"""T6: the self-knowledge property on the REAL store — one query,
|
||||
deterministic, no LLM/memory/rebuild. And fast (bounded lookup)."""
|
||||
doc = spine.load()
|
||||
if doc is None:
|
||||
pytest.skip("no spine store in this checkout")
|
||||
import time as _t
|
||||
t0 = _t.monotonic()
|
||||
q1 = spine.query(doc, "module:rune_operator")
|
||||
q2 = spine.query(doc, "module:rune_operator")
|
||||
dt = _t.monotonic() - t0
|
||||
assert q1 == q2 # deterministic
|
||||
assert q1["owner"] and q1["state"]["maturity"]
|
||||
assert "impact_frontier" in q1
|
||||
assert dt < 2.0, f"query took {dt:.2f}s — intrinsic property lost"
|
||||
|
||||
|
||||
# ── CLOSURE 1: durable identity through the REAL build path ───────────────
|
||||
|
||||
class TestClosureRealBuildDurability:
|
||||
"""The hardening pass tested alias/supersession around save/load with a
|
||||
build-path fallback that could mock the real refresh. Memory will depend
|
||||
on spine:// references — prove durability through the ACTUAL build."""
|
||||
|
||||
def _seeded_store(self, tmp_path):
|
||||
doc = spine.new_doc()
|
||||
spine.add_entity(doc, id="subsystem:memory", kind="subsystem",
|
||||
identities={"description": "fixture"}, maturity="DECLARED")
|
||||
for m in ("memory_selector",):
|
||||
spine.add_entity(doc, id=f"module:{m}", kind="module", maturity="DECLARED",
|
||||
identities={"source": f"src/rune/{m}.py"})
|
||||
spine.add_relationship(doc, type="belongs_to", from_id=f"module:{m}",
|
||||
to_id="subsystem:memory", evidence_refs=["fixture"])
|
||||
spine.save(doc, spine.CURRENT)
|
||||
return doc
|
||||
|
||||
def test_alias_survives_real_build_refresh(self, tmp_path, monkeypatch):
|
||||
"""create A → alias A_old → save → reload → REAL build() → save →
|
||||
reload → spine://.../A_old still resolves to A. No mocked path."""
|
||||
monkeypatch.setattr(spine, "STORE_DIR", tmp_path / "store")
|
||||
monkeypatch.setattr(spine, "CURRENT", tmp_path / "store" / "current.json")
|
||||
monkeypatch.setattr(spine, "SNAPSHOTS", tmp_path / "store" / "snaps")
|
||||
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
|
||||
monkeypatch.setattr(spine, "SRC_DIR", tmp_path / "src" / "rune")
|
||||
(tmp_path / "src" / "rune").mkdir(parents=True)
|
||||
(tmp_path / "src" / "rune" / "memory_selector.py").write_text("x = 1\n")
|
||||
monkeypatch.setattr(spine, "TESTS_DIR", tmp_path / "tests")
|
||||
(tmp_path / "tests").mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(spine, "UMBRELLA_MANIFEST", tmp_path / "no-manifest.json")
|
||||
|
||||
self._seeded_store(tmp_path)
|
||||
doc = spine.load()
|
||||
spine.alias(doc, "module:memory_selector", "module:mem_sel_old")
|
||||
spine.save(doc)
|
||||
|
||||
# THE REAL PATH: spine.build() (loads the persisted store, carries
|
||||
# everything forward, re-derives module facts, reconciles, saves)
|
||||
rebuilt = spine.build()
|
||||
spine.save(rebuilt)
|
||||
|
||||
reloaded = spine.load()
|
||||
old_ref = spine.resolve_uri("spine://module/mem_sel_old")
|
||||
e = spine.entity(reloaded, old_ref)
|
||||
assert e is not None, "alias lost by real build"
|
||||
assert e["id"] == "module:memory_selector"
|
||||
assert "module:mem_sel_old" in e.get("aliases", [])
|
||||
|
||||
def test_supersession_survives_real_build_refresh(self, tmp_path, monkeypatch):
|
||||
"""B supersedes A → REAL build() → superseded_by intact, historical
|
||||
entity not silently deleted, old reference resolves meaningfully."""
|
||||
monkeypatch.setattr(spine, "STORE_DIR", tmp_path / "store")
|
||||
monkeypatch.setattr(spine, "CURRENT", tmp_path / "store" / "current.json")
|
||||
monkeypatch.setattr(spine, "SNAPSHOTS", tmp_path / "store" / "snaps")
|
||||
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
|
||||
monkeypatch.setattr(spine, "SRC_DIR", tmp_path / "src" / "rune")
|
||||
(tmp_path / "src" / "rune").mkdir(parents=True)
|
||||
(tmp_path / "src" / "rune" / "memory_selector.py").write_text("x = 1\n")
|
||||
monkeypatch.setattr(spine, "TESTS_DIR", tmp_path / "tests")
|
||||
(tmp_path / "tests").mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(spine, "UMBRELLA_MANIFEST", tmp_path / "no-manifest.json")
|
||||
|
||||
self._seeded_store(tmp_path)
|
||||
doc = spine.load()
|
||||
spine.add_entity(doc, id="module:memory_orchestrator", kind="module",
|
||||
maturity="DECLARED")
|
||||
spine.supersede(doc, "module:memory_orchestrator", "module:memory_selector",
|
||||
"evidence:closure-decision")
|
||||
spine.save(doc)
|
||||
|
||||
rebuilt = spine.build()
|
||||
spine.save(rebuilt)
|
||||
|
||||
reloaded = spine.load()
|
||||
old = spine.entity(reloaded, "module:memory_selector")
|
||||
assert old is not None, "superseded entity deleted by real build"
|
||||
assert old["superseded_by"] == "module:memory_orchestrator"
|
||||
assert old["status"] == "SUPERSEDED"
|
||||
# the supersedes edge survives as data (non-obligation context)
|
||||
assert spine.relationships(reloaded, type="supersedes",
|
||||
from_id="module:memory_orchestrator",
|
||||
to_id="module:memory_selector")
|
||||
|
||||
|
||||
# ── CLOSURE 2+3: no competing structural authority for RUNE ──────────────
|
||||
|
||||
class TestClosureNoCompetingAuthority:
|
||||
def test_dependency_tracker_scope_is_harness_not_rune(self):
|
||||
"""dependency_tracker's graph describes ~/.config/opencode/scripts
|
||||
(the harness), NOT Rune modules. The name collisions with src/rune
|
||||
are coincidental. This test pins the separation so the harness map
|
||||
can never silently become a second Rune structural authority.
|
||||
|
||||
CI-safe: asserts the CLASSIFICATION (resolution root is the harness
|
||||
dir; the map is not derived from Spine), never harness filesystem
|
||||
state — the CI runner has no ~/.config/opencode."""
|
||||
import inspect
|
||||
from rune import dependency_tracker as dt
|
||||
assert "opencode" in dt.SCRIPTS_DIR, "tracker scope moved — re-classify"
|
||||
# resolution root is the harness dir, by construction: propagate()
|
||||
# checks os.path.exists against SCRIPTS_DIR, not src/rune
|
||||
prop_src = inspect.getsource(dt.propagate)
|
||||
assert "SCRIPTS_DIR" in prop_src and "os.path.join(SCRIPTS_DIR" in prop_src
|
||||
# the map is a locally-owned table, NOT derived from the Spine store
|
||||
# (check IMPORTS, not prose — the module docstring legitimately
|
||||
# mentions Spine when documenting its scope classification)
|
||||
import ast as _ast
|
||||
tree = _ast.parse(inspect.getsource(dt))
|
||||
imported = set()
|
||||
for node in _ast.walk(tree):
|
||||
if isinstance(node, _ast.Import):
|
||||
imported.update(a.name for a in node.names)
|
||||
elif isinstance(node, _ast.ImportFrom) and node.module:
|
||||
imported.add(node.module)
|
||||
assert not any("spine" in m for m in imported), \
|
||||
f"tracker now imports Spine — re-classify as a consumer: {imported}"
|
||||
# and Spine remains the authority for Rune structure
|
||||
assert (spine.SRC_DIR / "spine.py").is_file()
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Transport selection (T15 PL1 substrate): provider-prefix keyed, not routing_mode.
|
||||
|
||||
Observed live: an operator-requested model keeps routing_mode='local' (line
|
||||
~887), so keying selection on routing_mode=='cloud' still sent pinned
|
||||
'Verboo Code/glm-5.3-flash' to OllamaChatTransport → 400. Selection must key
|
||||
on the model's provider prefix."""
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import rune_operator # noqa: E402
|
||||
from rune.openai_compat_transport import OpenAICompatTransport # noqa: E402
|
||||
from rune.local_tool_runtime import OllamaChatTransport # noqa: E402
|
||||
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, transport, executor, model, tools):
|
||||
self.transport = transport
|
||||
self.model = model
|
||||
|
||||
def run(self, messages, options=None):
|
||||
from rune.tool_protocol import OperatorResponse
|
||||
return OperatorResponse(success=True, user_text="done", model=self.model)
|
||||
|
||||
|
||||
def _send(op, description, model):
|
||||
captured = {}
|
||||
class _RT(_FakeRuntime):
|
||||
def __init__(self, transport, executor, model, tools):
|
||||
super().__init__(transport, executor, model, tools)
|
||||
captured["transport"] = transport
|
||||
captured["model"] = model
|
||||
with mock.patch.object(rune_operator, "LocalToolRuntime", _RT):
|
||||
op.send_task(description, model=model)
|
||||
return captured
|
||||
|
||||
|
||||
def test_pinned_cloud_selects_openai_compat_transport():
|
||||
"""Selection is the unit under test — config resolution (cloud_transport_for
|
||||
reads the user's mimocode config, absent in CI) is mocked."""
|
||||
from unittest import mock as _mock
|
||||
import tempfile
|
||||
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
import rune.openai_compat_transport as _oct
|
||||
with _mock.patch.object(_oct, "cloud_transport_for",
|
||||
side_effect=lambda model, timeout=120: OpenAICompatTransport(
|
||||
"https://mock/v1", "test-key", model.split("/", 1)[0])):
|
||||
captured = _send(op, "Fix the bug in /mnt/workdrive/pl1_heldout/greeting.py: returns 'Helo' not 'Hello'. Verify by running python3 /mnt/workdrive/pl1_heldout/check_greeting.py", "Verboo Code/glm-5.3-flash")
|
||||
assert isinstance(captured["transport"], OpenAICompatTransport)
|
||||
assert captured["model"] == "Verboo Code/glm-5.3-flash"
|
||||
|
||||
|
||||
def test_local_model_keeps_ollama_transport():
|
||||
import tempfile
|
||||
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
captured = _send(op, "inspect the log for errors", "qwen2.5:3b")
|
||||
assert isinstance(captured["transport"], OllamaChatTransport)
|
||||
|
||||
|
||||
def test_bare_model_name_keeps_ollama_transport():
|
||||
import tempfile
|
||||
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
captured = _send(op, "inspect the log for errors", "qwen2.5:3b")
|
||||
assert isinstance(captured["transport"], OllamaChatTransport)
|
||||
|
||||
|
||||
class TestResumeTransportSelection:
|
||||
"""Approval-resume path must use the SAME provider-prefix selection as
|
||||
the direct turn. Observed live (PL1 Task A, 2026-09-12): after an
|
||||
approval gate, the resumed turn's follow-up model call went to
|
||||
OllamaChatTransport with the cloud-pinned name → localhost 400
|
||||
'invalid model name' — the direct path had been fixed in 0.66.17 but
|
||||
_runtime_for_model (resume) still hardcoded Ollama."""
|
||||
|
||||
def test_resume_path_uses_cloud_transport_for_pinned_model(self):
|
||||
import rune.openai_compat_transport as _oct
|
||||
op = rune_operator.RuneOperator(
|
||||
storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
with mock.patch.object(
|
||||
_oct, "cloud_transport_for",
|
||||
side_effect=lambda model, timeout=120: OpenAICompatTransport(
|
||||
"https://mock/v1", "test-key", model.split("/", 1)[0]),
|
||||
):
|
||||
rt = op._runtime_for_model("Verboo Code/glm-5.3-flash", "fix the bug")
|
||||
assert isinstance(rt.transport, OpenAICompatTransport), type(rt.transport)
|
||||
|
||||
def test_resume_path_keeps_ollama_for_local_model(self):
|
||||
op = rune_operator.RuneOperator(
|
||||
storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
rt = op._runtime_for_model("qwen2.5:3b", "inspect the log")
|
||||
assert isinstance(rt.transport, OllamaChatTransport), type(rt.transport)
|
||||
@@ -1 +0,0 @@
|
||||
def broken(:
|
||||
Reference in new issue
Block a user