Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce330d3ac0 | ||
|
|
16b90fd8be | ||
|
|
c7a28e1db3 | ||
|
|
2731db03f4 | ||
|
|
eb2f1a3e0e | ||
|
|
e3b459dfdf | ||
|
|
b8d030e6c4 | ||
|
|
20a47fac86 | ||
|
|
36ee604646 | ||
|
|
20f493596d | ||
|
|
faf3575fb3 | ||
|
|
2e16fc4c89 | ||
|
|
12303bbd20 | ||
|
|
de30cd958e | ||
|
|
fd6e9b4bca | ||
|
|
24413e4228 | ||
|
|
407c23a689 | ||
|
|
982a9b07d5 | ||
|
|
ff506ebe55 | ||
|
|
2032819d99 | ||
|
|
e2464ae9d6 | ||
|
|
d7e04f1098 | ||
|
|
e639383c49 | ||
|
|
57a274d97d | ||
|
|
aac5295c24 | ||
|
|
9563148f9d | ||
|
|
76984c6362 | ||
|
|
4072d36b1a | ||
|
|
6d871abc9c | ||
|
|
ce40acbb1f | ||
|
|
1de28b55a0 |
No files matched your search
+35
-8
@@ -286,12 +286,39 @@ jobs:
|
||||
run: |
|
||||
NEW="$(echo "${{ gitea.event.pull_request.head.ref }}" | sed 's|chore/auto-bump-||')"
|
||||
python3 scripts/check_version_surfaces.py check --root . --version "$NEW"
|
||||
- name: Publish protected-branch gate (full pipeline path)
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
- name: Publish protected-branch gate (full pipeline path — all needs green)
|
||||
# RELEASE-SAFETY FIX (2026-09-13): the previous version of this step
|
||||
# read /tmp/need_* files that no step ever writes and echoed success
|
||||
# UNCONDITIONALLY — required-checks published green while type-check
|
||||
# and unit-tests were red, and deploy promoted the red commits
|
||||
# (observed live: 24413e4, fd6e9b4, de30cd9 promoted 0.66.28/29 with
|
||||
# failing type-check). The verdict now comes from the needs context
|
||||
# itself: this step runs ONLY when every required job succeeded.
|
||||
if: >-
|
||||
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs['unit-tests'].result == 'success'
|
||||
&& needs.canary.result == 'success'
|
||||
&& needs['best-practices'].result == 'success'
|
||||
&& needs.documentation.result == 'success'
|
||||
&& needs['type-check'].result == 'success'
|
||||
&& needs['gitea-offline'].result == 'success' }}
|
||||
run: echo "All required CI checks passed"
|
||||
- name: Refuse promotion when any required check failed (fail-closed)
|
||||
# Companion to the step above: if anything is not exactly success
|
||||
# (failure, cancelled, skipped on a non-bump path), the required-checks
|
||||
# context reports FAILURE — deploy_qualify then refuses promotion.
|
||||
# Fail-closed: not verified → not deployable.
|
||||
if: >-
|
||||
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
|
||||
&& (needs.lint.result != 'success'
|
||||
|| needs['unit-tests'].result != 'success'
|
||||
|| needs.canary.result != 'success'
|
||||
|| needs['best-practices'].result != 'success'
|
||||
|| needs.documentation.result != 'success'
|
||||
|| needs['type-check'].result != 'success'
|
||||
|| needs['gitea-offline'].result != 'success') }}
|
||||
run: |
|
||||
# All needs must have succeeded (skipped deps = this is a bump PR, handled above)
|
||||
FAILED=""
|
||||
for job in lint unit-tests canary best-practices documentation type-check gitea-offline; do
|
||||
RESULT=$(cat /tmp/need_$job 2>/dev/null || echo "")
|
||||
done
|
||||
echo "All required CI checks passed"
|
||||
echo "::error::Required CI checks not green — refusing to publish required-checks success"
|
||||
echo "lint=${{ needs.lint.result }} unit-tests=${{ needs['unit-tests'].result }} canary=${{ needs.canary.result }} best-practices=${{ needs['best-practices'].result }} documentation=${{ needs.documentation.result }} type-check=${{ needs['type-check'].result }} gitea-offline=${{ needs['gitea-offline'].result }}"
|
||||
exit 1
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.23**
|
||||
**Current release: v0.66.34**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
+235
-222
@@ -1,225 +1,238 @@
|
||||
{
|
||||
"_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"
|
||||
]
|
||||
}
|
||||
"_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-13T02:20:57Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Tool selection and execution authority",
|
||||
"mirrors": [],
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
],
|
||||
"depends_on": [],
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"build_pipeline_policy",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"curator",
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"description": "Learning and improvement",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"elastic_cognition",
|
||||
"goal",
|
||||
"growth_delta",
|
||||
"growth_simulator",
|
||||
"self_improve",
|
||||
"subconscious_pass"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"prompt_sync",
|
||||
"voice_hybrid",
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"description": "Model routing and outbound requests",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"inference_trace",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_routing",
|
||||
"provider_budget",
|
||||
"provider_registry",
|
||||
"response_adapter",
|
||||
"standardized_providers",
|
||||
"token_optimizer"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"mirrors": [],
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"mirrors": [],
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Input processing and context assembly",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"structural_store",
|
||||
"witness"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -68,3 +68,81 @@ for a 131072-window model (observed live in every PL1 Task A/B run).
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding V1 — Tranche 2: Construction Substrate (2026-09-13)
|
||||
|
||||
_Tranche 2 (contract SCAFFOLD-TRANCHE-001) built the construction substrate
|
||||
itself: a shared structural-store primitive, the Project Scaffold layer over
|
||||
it, and durable Construction Contracts — Rune's first build tracked by the
|
||||
system it builds._
|
||||
|
||||
## Architecture
|
||||
|
||||
src/rune/structural_store.py shared generic primitive — NO Rune constants
|
||||
↓ ↓
|
||||
src/rune/spine.py src/rune/scaffold.py
|
||||
(Rune Spine — canonical (Project Scaffold — canonical
|
||||
structural self-model) construction/structural truth
|
||||
about EXTERNAL projects)
|
||||
|
||||
Authority map (no duplicates):
|
||||
|
||||
| fact | authority | others |
|
||||
|---|---|---|
|
||||
| Rune's structure | `spine.py` + `store/spine/current.json` | scaffold only references |
|
||||
| external project structure | `scaffold.py` store | Spine holds bridge facts only |
|
||||
| content history / rollback | Git | both reference commits |
|
||||
| coordination (issues/PRs/CI) | Gitea | scaffold stores observations |
|
||||
| why work exists | Task/Intent | contracts reference task ids |
|
||||
|
||||
Construction Contract = ONE durable object (`contract:<id>`, kind
|
||||
`construction_contract`) with plain `phase`/`status` fields governed by a
|
||||
small legal-transition map (draft → approved → building → verifying →
|
||||
migrating → superseding → closed; failed/abandoned terminal-or-retry). No
|
||||
workflow engine. Resumability = store durability, proven across real
|
||||
process boundaries.
|
||||
|
||||
## Mechanics
|
||||
|
||||
- **Repo binding** (`bind_repo`): forge/owner/name/branch/root/placement as
|
||||
scaffold DATA. No secrets — schema rejects secret-named fields AND every
|
||||
`save()` recursively redacts credential-named keys (defense in depth in
|
||||
the shared core, so Spine inherits it too).
|
||||
- **Scope check** (`check_scope`): changed files vs approved scope paths →
|
||||
green / red / **unknown** (an unscoped contract is never silently green).
|
||||
Out-of-scope change = RED until explicitly amended; amendments recorded,
|
||||
original RED preserved in history.
|
||||
- **Predicted vs observed**: frontier snapshot at approval; observed impact
|
||||
captured at closure from INDEPENDENT evidence (git, tests) — never from
|
||||
re-walking the prediction's own graph. Missed entities become CANDIDATE
|
||||
corrections awaiting evidence.
|
||||
- **Drift**: authority-owned facts that fail re-observation go STALE —
|
||||
never silently fresh, never force-reverted. Reality wins.
|
||||
- **Bridge** (`spine_bridge_entity`): identity + repo surface only. Project
|
||||
internals NEVER leak into Rune Spine (test-enforced). Contract evidence
|
||||
may cite `spine://` URIs — citations are inert on the referenced
|
||||
authority.
|
||||
|
||||
## Real proof (this tranche on itself)
|
||||
|
||||
SCAFFOLD-TRANCHE-001 migrated from its pre-scaffold JSON into the new
|
||||
store, advanced through phases with evidence checkpoints, closed with
|
||||
predicted-vs-observed from git/test evidence, scope check caught the
|
||||
store's own runtime artifacts (RED → explicit amendment → green, history
|
||||
kept), and construction provenance attached into Rune Spine without
|
||||
duplicating authority. A fresh interpreter answers what/why/state/
|
||||
evidence/changed/next from the store alone — no transcripts.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
| component | state |
|
||||
|---|---|
|
||||
| structural_store core | EXERCISED (unit-proven + real use) |
|
||||
| Rune Spine after refactor | LIVE_PROVEN behavior-neutral (68 tests + identical rebuild) |
|
||||
| Project Scaffold | EXERCISED |
|
||||
| Construction Contract | EXERCISED (resumability proven cross-process) |
|
||||
| repo binding (identity model) | UNIT-PROVEN (local git fixture) |
|
||||
| external-repo live proof | BLOCKED ON USER REPO SELECTION |
|
||||
| Scaffolding overall | substrate only — capability-fit discovery, gap decisions, toolchain selection, patterns, maintenance TUI all NOT BUILT (data seams exist) |
|
||||
+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.23"
|
||||
version = "0.66.34"
|
||||
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": 117,
|
||||
"warningCount": 443,
|
||||
"informationCount": 0
|
||||
}
|
||||
Whitespace-only changes.
+115
-18
@@ -6,6 +6,16 @@ has no embedded multi-line scripts — a YAML block-scalar indentation hazard).
|
||||
|
||||
Prints one of: success | failure | pending
|
||||
Exit code 0 always (the shell decides what to do with the verdict).
|
||||
|
||||
RELEASE-SAFETY HARDENING (2026-09-13, after red-CI promotions 0.66.28/29):
|
||||
the verdict is the AND of two independent legs —
|
||||
1. required-checks context (newest observation for this exact SHA);
|
||||
2. the latest COMPLETED ci.yml@refs/heads/master run for this exact SHA,
|
||||
every job concluded success (branch+tag fan-out resolved by path;
|
||||
jobs envelope unwrapped — no aggregate-commit-status fallback, the
|
||||
exact bug class verify.py hit in W/R-v0.3).
|
||||
Any disagreement, pending, or missing evidence → pending/failure.
|
||||
FAIL-CLOSED: not verified → not deployable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,6 +30,102 @@ if _SCRIPT_DIR not in _sys.path:
|
||||
_sys.path.insert(0, _SCRIPT_DIR)
|
||||
import gitea_auth
|
||||
|
||||
REQUIRED_CONTEXT = "CI Pipeline / required-checks"
|
||||
CI_WORKFLOW_PATH = "ci.yml@refs/heads/master"
|
||||
|
||||
|
||||
# ── pure verdict logic (unit-tested; fixtures from the real incidents) ────
|
||||
|
||||
def required_check_verdict(statuses: list[dict]) -> str:
|
||||
"""Newest non-skipped required-checks observation for the SHA.
|
||||
Missing → pending. Newest wins (stale success never overrides a newer
|
||||
failure, and vice versa)."""
|
||||
candidates = [
|
||||
s for s in statuses
|
||||
if str(s.get("context", "")).startswith(REQUIRED_CONTEXT)
|
||||
and s.get("status") != "skipped"
|
||||
]
|
||||
if not candidates:
|
||||
return "pending"
|
||||
newest = max(candidates, key=lambda s: str(s.get("created_at", "")))
|
||||
return str(newest.get("status", "pending"))
|
||||
|
||||
|
||||
def independent_jobs_verdict(runs: list[dict], jobs_fetch, sha: str = "") -> str:
|
||||
"""Latest COMPLETED ci.yml@refs/heads/master run for the exact SHA:
|
||||
every job must conclude success. Fan-out-safe (tag runs and sibling
|
||||
workflows are excluded by path); the run is resolved by head_sha, never
|
||||
by 'latest branch state'. Missing run → pending. Any non-success
|
||||
conclusion → failure."""
|
||||
mine = [r for r in runs if r.get("path") == CI_WORKFLOW_PATH]
|
||||
if sha:
|
||||
mine = [r for r in mine if str(r.get("head_sha", "")) == sha]
|
||||
completed = [r for r in mine if r.get("status") == "completed"]
|
||||
if not completed:
|
||||
return "pending"
|
||||
latest = max(completed, key=lambda r: str(r.get("created_at", "") or r.get("run_started_at", "")))
|
||||
jobs = jobs_fetch(latest.get("id"))
|
||||
if jobs is None:
|
||||
return "pending" # jobs probe failed → not verified
|
||||
if not jobs:
|
||||
return "pending" # no jobs visible → cannot verify
|
||||
for j in jobs:
|
||||
if j.get("conclusion") != "success":
|
||||
return "failure"
|
||||
return "success"
|
||||
|
||||
|
||||
def combine(a: str, b: str) -> str:
|
||||
"""Fail-closed conjunction: any failure → failure; any pending → pending;
|
||||
success only when BOTH legs are success."""
|
||||
if "failure" in (a, b):
|
||||
return "failure"
|
||||
if "pending" in (a, b):
|
||||
return "pending"
|
||||
return "success"
|
||||
|
||||
|
||||
def qualify(statuses: list[dict], runs: list[dict], jobs_fetch, sha: str = "") -> str:
|
||||
return combine(required_check_verdict(statuses),
|
||||
independent_jobs_verdict(runs, jobs_fetch, sha=sha))
|
||||
|
||||
|
||||
# ── API fetches (thin; injectable in tests) ───────────────────────────────
|
||||
|
||||
def _get_json(url: str, token: str, timeout: int = 30):
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/commits/{sha}/status", token)
|
||||
return None if data is None else data.get("statuses", [])
|
||||
|
||||
|
||||
def _fetch_runs(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs?limit=50", token)
|
||||
if data is None:
|
||||
return None
|
||||
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
|
||||
return [r for r in runs if str(r.get("head_sha", "")) == sha]
|
||||
|
||||
|
||||
def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs/{run_id}/jobs", token)
|
||||
if data is None:
|
||||
return None
|
||||
# jobs envelope unwrap (this Gitea wraps in {"jobs": [...]}; some
|
||||
# versions return a bare list) — never fall back to aggregate status
|
||||
if isinstance(data, dict):
|
||||
return data.get("jobs", [])
|
||||
return data if isinstance(data, list) else None
|
||||
|
||||
|
||||
# ── CLI ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
@@ -29,25 +135,16 @@ def main() -> int:
|
||||
if not (server and repo and sha and token):
|
||||
print("pending") # missing config → cannot verify → not qualified
|
||||
return 0
|
||||
url = f"{server}/api/v1/repos/{repo}/commits/{sha}/status"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
data = json.loads(r.read())
|
||||
except (OSError, ValueError) as exc:
|
||||
# transient API/network failure → keep polling (urllib raises
|
||||
# URLError (OSError subclass); bad JSON raises ValueError)
|
||||
print(f"pending (status probe failed: {exc})")
|
||||
|
||||
statuses = _fetch_status(server, repo, sha, token)
|
||||
runs = _fetch_runs(server, repo, sha, token)
|
||||
if statuses is None or runs is None:
|
||||
# transient API/network failure → keep polling
|
||||
print("pending (probe failed)")
|
||||
return 0
|
||||
required = [
|
||||
s
|
||||
for s in data.get("statuses", [])
|
||||
if (str(s.get("context", "")).startswith("CI Pipeline / required-checks") and s.get("status") != "skipped")
|
||||
]
|
||||
if not required:
|
||||
print("pending")
|
||||
else:
|
||||
print(required[0].get("status", "pending"))
|
||||
jobs_fetch = lambda run_id: _fetch_jobs(server, repo, run_id, token) # noqa: E731
|
||||
verdict = qualify(statuses, runs, jobs_fetch, sha=sha)
|
||||
print(verdict)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -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": {}}
|
||||
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"""scaffold_projection.py — Scaffolding V1 Tranche 1 (projection, not authority).
|
||||
|
||||
Answers, per component/module:
|
||||
what subsystem owns X? → config/umbrellas.json (existing authority)
|
||||
what does X depend on? → AST import graph (actual) + umbrella depends_on
|
||||
who consumes X? → reverse import edges (actual)
|
||||
what changes if X changes? → mirrors + dependent umbrellas + import consumers
|
||||
what proof obligations arise? → unit test, canary, henxel coverage, umbrella gates
|
||||
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)
|
||||
|
||||
PROJECTION DISCIPLINE: every field is DERIVED from an existing authority
|
||||
(umbrella manifest, canary_native registry, filesystem, AST). This script
|
||||
introduces no new state — if it disagrees with the authorities, fix the
|
||||
authority, never this projection.
|
||||
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
|
||||
@@ -27,11 +27,17 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src" / "rune"
|
||||
TESTS = ROOT / "tests"
|
||||
UMBRELLAS = ROOT / "config" / "umbrellas.json"
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
from rune import spine # noqa: E402 — Spine IS the structural authority
|
||||
|
||||
|
||||
def load_umbrellas() -> dict:
|
||||
return json.loads(UMBRELLAS.read_text())["umbrellas"]
|
||||
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]:
|
||||
@@ -62,23 +68,23 @@ 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 umbrella_for(module: str, umbrellas: dict) -> str:
|
||||
for name, u in umbrellas.items():
|
||||
if module in u.get("modules", []):
|
||||
return name
|
||||
return ""
|
||||
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_umbrellas(umbrella: str, umbrellas: dict) -> list[str]:
|
||||
return [name for name, u in umbrellas.items()
|
||||
if umbrella in u.get("depends_on", [])]
|
||||
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(module: str, umbrellas: dict) -> list[str]:
|
||||
for u in umbrellas.values():
|
||||
if module in u.get("modules", []):
|
||||
return list(u.get("mirrors", []))
|
||||
return []
|
||||
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:
|
||||
@@ -92,9 +98,10 @@ def canary_registered(module: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def obligations(module: str, graph: dict[str, set[str]], umbrellas: dict) -> list[dict]:
|
||||
"""Proof obligations for changing this module, with evidence state."""
|
||||
umb = umbrella_for(module, umbrellas)
|
||||
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(),
|
||||
@@ -112,38 +119,45 @@ def obligations(module: str, graph: dict[str, set[str]], umbrellas: dict) -> lis
|
||||
"evidence": "run: henxels check --all"},
|
||||
]
|
||||
if umb:
|
||||
deps = dependent_umbrellas(umb, umbrellas)
|
||||
deps = dependent_subsystems(doc, umb)
|
||||
if deps:
|
||||
obs.append({"obligation": "dependent_canaries", "satisfied": None,
|
||||
"evidence": f"umbrellas depending on '{umb}': {', '.join(deps)} — their canaries gate the change"})
|
||||
mir = mirrors_for(module, umbrellas)
|
||||
"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, graph: dict[str, set[str]], umbrellas: dict) -> dict:
|
||||
deps = {d for d in graph.get(module, set())
|
||||
if (SRC / f"{d}.py").is_file() and d != module}
|
||||
return {
|
||||
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": umbrella_for(module, umbrellas),
|
||||
"dependencies": sorted(deps),
|
||||
"consumers": sorted(consumers_of(module, graph)),
|
||||
"mirrors": mirrors_for(module, umbrellas),
|
||||
"obligations": obligations(module, graph, umbrellas),
|
||||
"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:
|
||||
umbrellas = load_umbrellas()
|
||||
graph = import_graph()
|
||||
card = component_card(module, graph, umbrellas)
|
||||
doc = load_spine()
|
||||
card = component_card(module, doc)
|
||||
umb = card["subsystem"]
|
||||
card["expected_impact"] = {
|
||||
"direct_consumers": card["consumers"],
|
||||
"umbrella_dependents": dependent_umbrellas(umb, umbrellas) if umb else [],
|
||||
"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"]
|
||||
@@ -161,23 +175,23 @@ def main() -> int:
|
||||
if cmd in ("component", "impact", "obligations") and len(args) < 2:
|
||||
print(f"usage: {cmd} <module>", file=sys.stderr)
|
||||
return 2
|
||||
umbrellas = load_umbrellas()
|
||||
graph = import_graph()
|
||||
doc = load_spine()
|
||||
if cmd == "impact":
|
||||
print(json.dumps(impact(args[1]), indent=1))
|
||||
elif cmd == "component":
|
||||
print(json.dumps(component_card(args[1], graph, umbrellas), indent=1))
|
||||
print(json.dumps(component_card(args[1], doc), indent=1))
|
||||
elif cmd == "obligations":
|
||||
card = component_card(args[1], graph, umbrellas)
|
||||
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, graph, umbrellas)
|
||||
for p in sorted(SRC.glob("*.py"))}
|
||||
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": ["config/umbrellas.json", "src/rune/canary_native.py",
|
||||
"AST import graph", "tests/ filesystem"],
|
||||
"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)")
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.23"
|
||||
__version__ = "0.66.34"
|
||||
|
||||
# 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.23"' in rune_version,
|
||||
'__version__ = "0.66.34"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -93,6 +93,31 @@ def main() -> int:
|
||||
"if: always()" in required_gate and "needs: [lint, unit-tests, canary" in required_gate,
|
||||
failures,
|
||||
)
|
||||
# RELEASE-SAFETY (2026-09-13, after 0.66.28/29 red promotions): the
|
||||
# required-checks publish step previously echoed success UNCONDITIONALLY
|
||||
# (read /tmp/need_* files that no step wrote). The verdict MUST come
|
||||
# from the needs context — this canary locks that class shut.
|
||||
publish_section = required_gate.split("Publish protected-branch gate", 1)[-1]
|
||||
check(
|
||||
"required gate verdict evaluates needs results (not unconditional echo)",
|
||||
"needs.lint.result == 'success'" in publish_section
|
||||
and "needs['type-check'].result == 'success'" in publish_section
|
||||
and "needs['unit-tests'].result == 'success'" in publish_section,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"required gate fails closed when any check is not success",
|
||||
"!= 'success'" in publish_section and "exit 1" in publish_section,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"no unconditional success echo without a needs-conditioned if",
|
||||
not (
|
||||
"All required CI checks passed" in publish_section
|
||||
and "needs.lint.result == 'success'" not in publish_section
|
||||
),
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"required gate depends on unit tests and verification jobs",
|
||||
"needs: [lint, unit-tests, canary" in text,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.23
|
||||
Version: v0.66.34
|
||||
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.23 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.34 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
"""scaffold.py — Project Scaffold: construction/structural truth for a
|
||||
project Rune builds, maintains, or evolves.
|
||||
|
||||
Thin layer over the shared structural-store primitive (structural_store.py).
|
||||
NO graph engine here — the store, maturity ladder, authority reconciliation,
|
||||
alias/supersession, frontier and diff all come from the shared core.
|
||||
|
||||
Project Scaffold is CANONICAL for the project's own construction/structural
|
||||
state. It is NOT Rune Spine: project internals never enter Rune's store.
|
||||
Rune Spine holds one `project:<stable-id>` entity with builds/manages
|
||||
relationships and the connector surface — the bridge, not a mirror.
|
||||
|
||||
Repository/forge binding: the scaffold records repo IDENTITY (forge,
|
||||
owner/name, branch, root, CI workflow identities) and repo OBSERVATIONS
|
||||
(HEAD, clean/dirty, last verification verdict). It never stores credentials;
|
||||
it never duplicates Gitea coordination state — issues/PRs/workflows are
|
||||
coordination surfaces, referenced as evidence, not copied as structure.
|
||||
|
||||
Repository placement model (decision, not automation):
|
||||
RUNE_REPO shares Rune's lifecycle/release/versioning
|
||||
EXISTING_PROJECT_REPO work lands in an already-established repo
|
||||
NEW_MANAGED_REPO standalone lifecycle warrants its own repo
|
||||
(user approval required to create one)
|
||||
|
||||
Construction Contract: ONE durable object per bounded build. `phase`
|
||||
(draft→approved→building→verifying→migrating→superseding→closed) and
|
||||
`status` (open|closed|failed|abandoned) are plain fields governed by a
|
||||
small legal-transition map — a workflow engine only if reality demands one.
|
||||
Resumability = durability of the store (proven by cross-process tests).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import ( # shared generic core — no Rune constants
|
||||
SCHEMA_VERSION,
|
||||
add_entity,
|
||||
add_relationship,
|
||||
bind_task,
|
||||
diff,
|
||||
entity,
|
||||
impact_frontier,
|
||||
load,
|
||||
new_doc,
|
||||
now,
|
||||
predicted_vs_observed,
|
||||
record_observed_impact,
|
||||
reconcile_authorities,
|
||||
relationships,
|
||||
resolve_status,
|
||||
save,
|
||||
snapshot,
|
||||
structural_events,
|
||||
supersede,
|
||||
task_entities,
|
||||
)
|
||||
|
||||
__all__ = ["Scaffold", "PHASES", "LEGAL_TRANSITIONS", "REPO_PLACEMENTS", "REPO_SECRET_FIELDS"]
|
||||
|
||||
# ── constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
PHASES = ["draft", "approved", "building", "verifying", "migrating", "superseding", "closed", "failed", "abandoned"]
|
||||
LEGAL_TRANSITIONS = {
|
||||
"draft": {"approved", "abandoned"},
|
||||
"approved": {"building", "abandoned"},
|
||||
"building": {"verifying", "approved", "abandoned", "failed"},
|
||||
"verifying": {"migrating", "building", "failed", "abandoned", "closed"},
|
||||
"migrating": {"superseding", "verifying", "failed", "abandoned"},
|
||||
"superseding": {"closed", "failed", "abandoned"},
|
||||
"closed": set(),
|
||||
"failed": {"building", "abandoned"}, # retry path
|
||||
"abandoned": set(),
|
||||
}
|
||||
STATUSES = {"open", "closed", "failed", "abandoned"}
|
||||
REPO_PLACEMENTS = {"RUNE_REPO", "EXISTING_PROJECT_REPO", "NEW_MANAGED_REPO"}
|
||||
REPO_SECRET_FIELDS = {"token", "api_key", "password", "secret", "auth_header", "credential", "api_token"}
|
||||
|
||||
|
||||
class IllegalPhaseTransition(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Scaffold:
|
||||
"""One Project Scaffold: a durable structural/construction store rooted
|
||||
at `store_dir` (typically `<project>/.rune/scaffold/`)."""
|
||||
|
||||
def __init__(self, store_dir: Path | str, project_id: str):
|
||||
self.store_dir = Path(store_dir)
|
||||
self.project_id = project_id
|
||||
self.current = self.store_dir / "current.json"
|
||||
self.snapshots_dir = self.store_dir / "snapshots"
|
||||
|
||||
# -- store lifecycle ----------------------------------------------------
|
||||
|
||||
def init(self, *, purpose: str, requirement: str = "") -> dict:
|
||||
"""Initialize (or load) this project's scaffold and register the
|
||||
project identity entity."""
|
||||
doc = load(self.current)
|
||||
if doc is None:
|
||||
doc = new_doc()
|
||||
add_entity(
|
||||
doc,
|
||||
id=f"project:{self.project_id}",
|
||||
kind="project",
|
||||
identities={"purpose": purpose, "requirement": requirement},
|
||||
maturity="DECLARED",
|
||||
evidence_refs=["scaffold-init"],
|
||||
)
|
||||
save(doc, self.current)
|
||||
return doc
|
||||
|
||||
def load(self) -> dict:
|
||||
doc = load(self.current)
|
||||
if doc is None:
|
||||
doc = new_doc()
|
||||
return doc
|
||||
|
||||
def save(self, doc: dict) -> Path:
|
||||
return save(doc, self.current)
|
||||
|
||||
# -- project structure (open-world kinds; no fixed vocabulary) ----------
|
||||
|
||||
def add_domain(self, doc: dict, name: str, description: str = "") -> dict:
|
||||
return add_entity(
|
||||
doc, id=f"domain:{name}", kind="domain", identities={"description": description}, evidence_refs=["scaffold"]
|
||||
)
|
||||
|
||||
def add_component(
|
||||
self,
|
||||
doc: dict,
|
||||
name: str,
|
||||
*,
|
||||
kind: str = "component",
|
||||
path: str = "",
|
||||
description: str = "",
|
||||
maturity: str = "DECLARED",
|
||||
) -> dict:
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"component:{name}",
|
||||
kind=kind,
|
||||
identities={"path": path, "description": description},
|
||||
maturity=maturity,
|
||||
evidence_refs=["scaffold"],
|
||||
)
|
||||
|
||||
def add_contract(self, doc: dict, name: str, *, description: str = "", spec_ref: str = "") -> dict:
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"contract:{name}",
|
||||
kind="contract",
|
||||
identities={"description": description, "spec": spec_ref},
|
||||
evidence_refs=["scaffold"],
|
||||
)
|
||||
|
||||
def link(self, doc: dict, *, type: str, from_id: str, to_id: str, evidence_refs=None) -> dict:
|
||||
"""Generic relationship with evidence (contains/depends_on/consumes/
|
||||
exposes/serves/route_to/... — open type vocabulary)."""
|
||||
return add_relationship(
|
||||
doc, type=type, from_id=from_id, to_id=to_id, evidence_refs=evidence_refs or ["scaffold"]
|
||||
)
|
||||
|
||||
def set_prd(self, doc: dict, prd_ref: str) -> dict:
|
||||
"""Record the PRD/location reference (NOT a copy of the PRD)."""
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"prd:{self.project_id}",
|
||||
kind="prd_state",
|
||||
identities={"reference": prd_ref},
|
||||
evidence_refs=["scaffold"],
|
||||
)
|
||||
|
||||
def set_next_action(self, doc: dict, action: str) -> dict:
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"next_action:{self.project_id}",
|
||||
kind="next_action",
|
||||
identities={"action": action},
|
||||
evidence_refs=["scaffold"],
|
||||
)
|
||||
|
||||
def add_limitation(self, doc: dict, name: str, detail: str = "") -> dict:
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"limitation:{name}",
|
||||
kind="known_limitation",
|
||||
identities={"detail": detail},
|
||||
evidence_refs=["scaffold"],
|
||||
)
|
||||
|
||||
def add_maintenance_item(
|
||||
self,
|
||||
doc: dict,
|
||||
name: str,
|
||||
*,
|
||||
issue: str,
|
||||
evidence: str = "",
|
||||
benefit: str = "",
|
||||
approval: str = "user",
|
||||
priority: str = "normal",
|
||||
status: str = "open",
|
||||
) -> dict:
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"maintenance:{name}",
|
||||
kind="maintenance_item",
|
||||
identities={
|
||||
"issue": issue,
|
||||
"evidence": evidence,
|
||||
"benefit": benefit,
|
||||
"approval": approval,
|
||||
"priority": priority,
|
||||
"status": status,
|
||||
},
|
||||
evidence_refs=["scaffold"],
|
||||
)
|
||||
|
||||
# -- repository / forge binding ------------------------------------------
|
||||
|
||||
def bind_repo(
|
||||
self,
|
||||
doc: dict,
|
||||
*,
|
||||
forge: str,
|
||||
owner: str,
|
||||
name: str,
|
||||
default_branch: str,
|
||||
repo_root: str,
|
||||
placement: str = "EXISTING_PROJECT_REPO",
|
||||
worktree_strategy: str = "per-contract",
|
||||
ci_workflows: list[str] | None = None,
|
||||
deployment_ref: str = "",
|
||||
version_tag_policy: str = "",
|
||||
issue_integration: str = "",
|
||||
rationale: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Record repo IDENTITY + placement decision. NEVER credentials —
|
||||
the repo_identity schema rejects secret-named fields (enforced below
|
||||
and by tests). Observations (HEAD, dirty state) are separate."""
|
||||
if placement not in REPO_PLACEMENTS:
|
||||
raise ValueError(f"unknown repo placement: {placement}")
|
||||
identities = {
|
||||
"forge": forge,
|
||||
"owner": owner,
|
||||
"name": name,
|
||||
"full_name": f"{owner}/{name}",
|
||||
"default_branch": default_branch,
|
||||
"repo_root": repo_root,
|
||||
"worktree_strategy": worktree_strategy,
|
||||
"ci_workflows": sorted(ci_workflows or []),
|
||||
"deployment_ref": deployment_ref,
|
||||
"version_tag_policy": version_tag_policy,
|
||||
"issue_integration": issue_integration,
|
||||
"placement": placement,
|
||||
"rationale": sorted(rationale or []),
|
||||
}
|
||||
bad = REPO_SECRET_FIELDS & set(identities)
|
||||
if bad:
|
||||
raise ValueError(f"repo binding rejects secret-named fields: {bad}")
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"repo:{owner}/{name}",
|
||||
kind="repo_identity",
|
||||
identities=identities,
|
||||
maturity="DECLARED",
|
||||
evidence_refs=["repo-binding"],
|
||||
)
|
||||
|
||||
def observe_repo(
|
||||
self,
|
||||
doc: dict,
|
||||
owner: str,
|
||||
name: str,
|
||||
*,
|
||||
head: str = "",
|
||||
clean: bool | None = None,
|
||||
last_verification: str = "",
|
||||
) -> dict:
|
||||
"""Record a repo STATE observation (HEAD, dirty flag, verdict).
|
||||
Observations are refreshable; stale ones never read as current
|
||||
(resolve_status handles that)."""
|
||||
rid = f"repo:{owner}/{name}"
|
||||
e = entity(doc, rid)
|
||||
if e is None:
|
||||
raise ValueError(f"bind_repo first: {rid} not found")
|
||||
obs = {"head": head[:12], "clean": clean, "last_verification": last_verification}
|
||||
e.setdefault("ext", {})["observed_state"] = obs
|
||||
e["observed_at"] = now()
|
||||
doc["observations"].append(
|
||||
{"kind": "repo_observed", "repo": rid, "head": head[:12], "clean": clean, "observed_at": now()}
|
||||
)
|
||||
return e
|
||||
|
||||
def repo_state_from_git(self, doc: dict, owner: str, name: str, repo_root: str) -> dict:
|
||||
"""Refresh repo observation from the actual git working tree
|
||||
(deterministic; reality wins)."""
|
||||
root = Path(repo_root)
|
||||
head = ""
|
||||
clean = None
|
||||
try:
|
||||
head = subprocess.run(
|
||||
["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True, timeout=15
|
||||
).stdout.strip()
|
||||
st = subprocess.run(
|
||||
["git", "-C", str(root), "status", "--short"], capture_output=True, text=True, timeout=15
|
||||
)
|
||||
clean = st.returncode == 0 and st.stdout.strip() == ""
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass # observation degrades gracefully; stale flag governs
|
||||
return self.observe_repo(doc, owner, name, head=head, clean=clean)
|
||||
|
||||
# -- construction contracts ----------------------------------------------
|
||||
|
||||
def new_contract(
|
||||
self,
|
||||
doc: dict,
|
||||
*,
|
||||
contract_id: str,
|
||||
need: str,
|
||||
origin: str = "user",
|
||||
scope: list[str] | None = None,
|
||||
requirement: str = "",
|
||||
gap_evidence: str = "",
|
||||
research_refs: list[str] | None = None,
|
||||
resolution: str = "",
|
||||
toolchain: dict | None = None,
|
||||
affected_entities: list[str] | None = None,
|
||||
expected_consumers: list[str] | None = None,
|
||||
regression_obligations: list[str] | None = None,
|
||||
verification_obligations: list[str] | None = None,
|
||||
migration_obligations: list[str] | None = None,
|
||||
rollback_ref: str = "",
|
||||
acceptance: list[str] | None = None,
|
||||
non_goals: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Create a Construction Contract in `draft` phase. One durable
|
||||
object; phases/status are plain fields with a legal-transition map.
|
||||
`toolchain` is a data seam: language/runtime/framework/libs/rationale
|
||||
— no Python-specific assumptions enforced."""
|
||||
if entity(doc, f"contract:{contract_id}") is not None:
|
||||
raise ValueError(f"contract already exists: {contract_id}")
|
||||
identities = {
|
||||
"need": need,
|
||||
"origin": origin,
|
||||
"scope": sorted(scope or []),
|
||||
"requirement": requirement,
|
||||
"gap_evidence": gap_evidence,
|
||||
"research_refs": sorted(research_refs or []),
|
||||
"resolution": resolution,
|
||||
"toolchain": dict(toolchain or {}), # data seam: any language/runtime
|
||||
"affected_entities": sorted(affected_entities or []),
|
||||
"expected_consumers": sorted(expected_consumers or []),
|
||||
"regression_obligations": sorted(regression_obligations or []),
|
||||
"verification_obligations": sorted(verification_obligations or []),
|
||||
"migration_obligations": sorted(migration_obligations or []),
|
||||
"rollback_ref": rollback_ref,
|
||||
"acceptance": sorted(acceptance or []),
|
||||
"non_goals": sorted(non_goals or []),
|
||||
"next_action": "review for approval",
|
||||
}
|
||||
e = add_entity(
|
||||
doc,
|
||||
id=f"contract:{contract_id}",
|
||||
kind="construction_contract",
|
||||
identities=identities,
|
||||
maturity="DECLARED",
|
||||
evidence_refs=[f"contract-created:{contract_id}"],
|
||||
)
|
||||
e["phase"] = "draft"
|
||||
e["status"] = "open"
|
||||
e["checkpoints"] = []
|
||||
return e
|
||||
|
||||
def transition_contract(
|
||||
self, doc: dict, contract_id: str, phase: str, *, evidence_ref: str = "", note: str = ""
|
||||
) -> dict:
|
||||
"""Move a contract to a new phase through the legal-transition map.
|
||||
Durable and resumable: the phase lives in the store."""
|
||||
e = entity(doc, f"contract:{contract_id}")
|
||||
if e is None:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
current = e.get("phase", "draft")
|
||||
if phase not in PHASES:
|
||||
raise ValueError(f"unknown phase: {phase}")
|
||||
if phase not in LEGAL_TRANSITIONS.get(current, set()):
|
||||
raise IllegalPhaseTransition(
|
||||
f"illegal transition: {current} → {phase} (legal: {sorted(LEGAL_TRANSITIONS.get(current, set()))})"
|
||||
)
|
||||
e["phase"] = phase
|
||||
if phase == "closed":
|
||||
e["status"] = "closed"
|
||||
elif phase == "failed":
|
||||
e["status"] = "failed"
|
||||
e.setdefault("checkpoints", []).append(
|
||||
{"phase": phase, "at": now(), "evidence": evidence_ref, "note": note[:200]}
|
||||
)
|
||||
e["identities"]["next_action"] = _NEXT_ACTION.get(phase, "")
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
|
||||
def contract(self, doc: dict, contract_id: str) -> dict | None:
|
||||
return entity(doc, f"contract:{contract_id}")
|
||||
|
||||
def snapshot_predicted_frontier(self, doc: dict, contract_id: str, seed_entity_ids: list[str]) -> dict:
|
||||
"""Persist the predicted impact frontier at approval time — the input
|
||||
to closure's predicted-vs-observed comparison."""
|
||||
frontier = impact_frontier(doc, seed_entity_ids)
|
||||
e = entity(doc, f"contract:{contract_id}")
|
||||
if e is None:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
e.setdefault("ext", {})["predicted_frontier"] = frontier
|
||||
e.setdefault("checkpoints", []).append(
|
||||
{
|
||||
"phase": e.get("phase", "draft"),
|
||||
"at": now(),
|
||||
"evidence": "predicted-frontier",
|
||||
"note": json_frontier(frontier),
|
||||
}
|
||||
)
|
||||
return frontier
|
||||
|
||||
def close_contract(
|
||||
self,
|
||||
doc: dict,
|
||||
contract_id: str,
|
||||
*,
|
||||
observed_evidence: list[dict] | None = None,
|
||||
scope_files: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Close a contract: record observed impact independently, run the
|
||||
predicted-vs-observed comparison, persist Contract Quality evidence.
|
||||
Requires the contract to be in `superseding` phase (the final work
|
||||
phase) or `verifying` (pure-observation builds)."""
|
||||
e = entity(doc, f"contract:{contract_id}")
|
||||
if e is None:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
if e.get("phase") not in ("superseding", "verifying"):
|
||||
raise IllegalPhaseTransition(f"close requires superseding/verifying phase, got {e.get('phase')}")
|
||||
predicted = e.get("ext", {}).get("predicted_frontier")
|
||||
affected = e.get("identities", {}).get("affected_entities", [])
|
||||
quality: dict = {"contract": contract_id, "captured_at": now()}
|
||||
if predicted is not None and affected:
|
||||
# observed from the caller's independent evidence items, not from
|
||||
# re-running the frontier on the same graph the prediction used
|
||||
obs = record_observed_impact(doc, contract_id, observed_evidence or [])
|
||||
pred_set = {
|
||||
x
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for x in predicted.get(k, [])
|
||||
}
|
||||
obs_set = set(obs["observed_entities"])
|
||||
quality.update(
|
||||
{
|
||||
"predicted": sorted(pred_set),
|
||||
"observed": sorted(obs_set),
|
||||
"missed": sorted(obs_set - pred_set),
|
||||
"needless": sorted(pred_set - obs_set),
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "missed by prediction — evidence required before store update",
|
||||
}
|
||||
for x in sorted(obs_set - pred_set)
|
||||
],
|
||||
}
|
||||
)
|
||||
if scope_files is not None:
|
||||
quality["scope_check"] = self.check_scope(doc, contract_id, scope_files)
|
||||
e.setdefault("ext", {})["contract_quality"] = quality
|
||||
e.setdefault("checkpoints", []).append(
|
||||
{"phase": "closed", "at": now(), "evidence": "closure-record", "note": "predicted-vs-observed captured"}
|
||||
)
|
||||
e["phase"] = "closed"
|
||||
e["status"] = "closed"
|
||||
e["identities"]["next_action"] = _NEXT_ACTION["closed"]
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
|
||||
def check_scope(self, doc: dict, contract_id: str, changed_files: list[str]) -> dict:
|
||||
"""MECHANICAL SCOPE CHECK: changed files vs the contract's approved
|
||||
scope paths. Out-of-scope = RED (verification failure) unless the
|
||||
contract is amended. Deterministic; no code-ownership system."""
|
||||
e = entity(doc, f"contract:{contract_id}")
|
||||
if e is None:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
scope = e.get("identities", {}).get("scope", [])
|
||||
if not scope:
|
||||
# An unscoped contract cannot prove scope-compliance: UNKNOWN,
|
||||
# not silently green. (Silent green = the silent-scope-expansion
|
||||
# failure mode this gate exists to prevent.)
|
||||
return {
|
||||
"verdict": "unknown",
|
||||
"in_scope": [],
|
||||
"out_of_scope": sorted(changed_files),
|
||||
"reason": "contract has no approved scope",
|
||||
}
|
||||
in_scope, out_scope = [], []
|
||||
for f in changed_files:
|
||||
ok = any(f == s or f.startswith(s.rstrip("/") + "/") or s in f for s in scope)
|
||||
(in_scope if ok else out_scope).append(f)
|
||||
verdict = "green" if not out_scope else "red"
|
||||
return {"verdict": verdict, "in_scope": sorted(in_scope), "out_of_scope": sorted(out_scope)}
|
||||
|
||||
# -- drift / reconciliation ----------------------------------------------
|
||||
|
||||
def reconcile(self, doc: dict) -> dict:
|
||||
"""Project-drift reconciliation: authority-owned staleness pass +
|
||||
observation dedup. Reality wins — stale facts are marked, not
|
||||
force-reverted."""
|
||||
staled = reconcile_authorities(doc)
|
||||
for s in staled:
|
||||
doc["observations"].append({"kind": "fact_staled", **s, "observed_at": now()})
|
||||
return {"staled": staled}
|
||||
|
||||
# -- bridge to Rune Spine (explicit, non-duplicating) ---------------------
|
||||
|
||||
def spine_bridge_entity(self) -> dict:
|
||||
"""The entity Rune Spine should hold for this project: identity +
|
||||
connector surface ONLY. Project internals (components/domains/routes)
|
||||
deliberately excluded — tested to prove no leakage."""
|
||||
doc = self.load()
|
||||
p = entity(doc, f"project:{self.project_id}")
|
||||
repo = next((e for e in doc["entities"] if e["kind"] == "repo_identity"), None)
|
||||
bridge = {
|
||||
"project": f"project:{self.project_id}",
|
||||
"purpose": (p or {}).get("identities", {}).get("purpose", ""),
|
||||
}
|
||||
if repo:
|
||||
bridge["repo_full_name"] = repo["identities"].get("full_name", "")
|
||||
bridge["repo_forge"] = repo["identities"].get("forge", "")
|
||||
return bridge
|
||||
|
||||
|
||||
def json_frontier(frontier: dict) -> str:
|
||||
import json
|
||||
|
||||
keys = ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
return json.dumps({k: frontier.get(k, []) for k in keys})
|
||||
|
||||
|
||||
_NEXT_ACTION = {
|
||||
"draft": "review for approval",
|
||||
"approved": "begin building",
|
||||
"building": "complete implementation, then verify",
|
||||
"verifying": "run verification gates; resolve failures",
|
||||
"migrating": "migrate consumers/connectors",
|
||||
"superseding": "supersede old implementation; retain rollback",
|
||||
"closed": "observe real outcome; capture lessons",
|
||||
"failed": "diagnose; retry via building or abandon",
|
||||
"abandoned": "",
|
||||
}
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
"""Operator surface — same convention as `rune-spine` (spine.main):
|
||||
plain argv, JSON output from the actual store. Never reconstructs
|
||||
state from prose."""
|
||||
import json as _json
|
||||
import sys as _sys
|
||||
|
||||
argv = list(_sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
cmd, args = argv[0], argv[1:]
|
||||
|
||||
# CONVENTION (all commands): [command args...] <store_dir> [project_id]
|
||||
# store_dir and project_id are ALWAYS the last two positional args, so
|
||||
# command-specific arguments never collide with the store location.
|
||||
def _scaffold_from_args() -> Scaffold:
|
||||
store_dir = args[-2] if len(args) >= 2 else ".rune/scaffold"
|
||||
project_id = args[-1] if args else Path(store_dir).name
|
||||
# guard: project-init passes [store, pid, purpose] — last two are
|
||||
# still store+pid, purpose is args[2] and handled locally
|
||||
return Scaffold(store_dir, project_id)
|
||||
|
||||
def _cmd_args() -> list[str]:
|
||||
"""Command-specific args = everything before the final two
|
||||
positional slots (store_dir, project_id)."""
|
||||
return args[:-2] if len(args) >= 2 else []
|
||||
|
||||
if cmd == "project-init":
|
||||
purpose = args[2] if len(args) > 2 else ""
|
||||
s = Scaffold(args[0] if args else ".rune/scaffold", args[1] if len(args) > 1 else "unnamed")
|
||||
doc = s.init(purpose=purpose)
|
||||
s.save(doc)
|
||||
print(_json.dumps({"project": f"project:{s.project_id}", "store": str(s.current)}))
|
||||
return 0
|
||||
if cmd == "project-show":
|
||||
s = _scaffold_from_args()
|
||||
doc = s.load()
|
||||
p = entity(doc, f"project:{s.project_id}")
|
||||
out = {
|
||||
"scaffold": str(s.current),
|
||||
"project": p,
|
||||
"bridge": s.spine_bridge_entity(),
|
||||
"entities": len(doc["entities"]),
|
||||
"relationships": len(doc["relationships"]),
|
||||
}
|
||||
print(_json.dumps(out, indent=1))
|
||||
return 0 if p else 1
|
||||
|
||||
doc = _scaffold_from_args().load()
|
||||
s = _scaffold_from_args()
|
||||
ca = _cmd_args()
|
||||
|
||||
if cmd == "contract-new":
|
||||
e = s.new_contract(doc, contract_id=ca[0], need=ca[1])
|
||||
s.save(doc)
|
||||
print(_json.dumps({"contract": f"contract:{ca[0]}", "phase": e["phase"], "status": e["status"]}))
|
||||
return 0
|
||||
if cmd == "contract-approve":
|
||||
s.transition_contract(doc, ca[0], "approved")
|
||||
s.save(doc)
|
||||
print(_json.dumps({"contract": ca[0], "phase": "approved"}))
|
||||
return 0
|
||||
if cmd == "contract-phase":
|
||||
s.transition_contract(doc, ca[0], ca[1])
|
||||
s.save(doc)
|
||||
print(_json.dumps({"contract": ca[0], "phase": ca[1]}))
|
||||
return 0
|
||||
if cmd == "contract-show":
|
||||
c = s.contract(doc, ca[0])
|
||||
print(_json.dumps(c, indent=1) if c else _json.dumps({"error": "not found", "contract": ca[0]}))
|
||||
return 0 if c else 1
|
||||
if cmd == "contract-verify-scope":
|
||||
v = s.check_scope(doc, ca[0], ca[1:])
|
||||
print(_json.dumps(v, indent=1))
|
||||
return 0 if v["verdict"] == "green" else 1
|
||||
if cmd == "contract-list":
|
||||
out = [
|
||||
{
|
||||
"id": e["id"],
|
||||
"phase": e.get("phase"),
|
||||
"status": e.get("status"),
|
||||
"next_action": e.get("identities", {}).get("next_action", ""),
|
||||
}
|
||||
for e in doc["entities"]
|
||||
if e["kind"] == "construction_contract"
|
||||
]
|
||||
print(_json.dumps(out, indent=1))
|
||||
return 0
|
||||
print(_json.dumps({"error": f"unknown command: {cmd}"}))
|
||||
return 2
|
||||
@@ -0,0 +1,695 @@
|
||||
"""spine.py — Rune Spine V1: evolvable structural self-model.
|
||||
|
||||
Spine answers, deterministically and without an LLM:
|
||||
what exists, how it relates, what is wired/live, what version is
|
||||
deployed, what evidence supports each claim, what drifted, what changed.
|
||||
|
||||
PRIME DIRECTIVE: model Rune's architecture without freezing it. Entity
|
||||
kinds, relationship types and metadata are OPEN TAGS — a previously unseen
|
||||
capability enters as data, never as a core edit. Unknown fields survive
|
||||
load/projection/serialize/reload untouched.
|
||||
|
||||
AUTHORITY MODEL: Spine is THE structural authority (subsystems, membership,
|
||||
dependencies, consumers, mirrors). Specialised authorities stay authoritative
|
||||
for their own domains and are OBSERVED, not copied: deployment identity
|
||||
(_build_identity.json), canary registry (canary_native.MODULES), provider
|
||||
config, CI outputs. Adding a domain = adding an adapter/projector.
|
||||
|
||||
MATURITY LADDER (evidence-gated, no evidence = no promotion):
|
||||
DECLARED < PRESENT < WIRED < REACHABLE < EXERCISED < LIVE_PROVEN
|
||||
DECLARED named in a registry/manifest
|
||||
PRESENT source artifact exists
|
||||
WIRED structural consumers/imports exist (or registry wiring)
|
||||
REACHABLE reachable from the composition root / deployed tree contains it
|
||||
EXERCISED test or canary exercises it
|
||||
LIVE_PROVEN runtime observation on the deployed identity proves it live
|
||||
|
||||
SNAPSHOTS: ingest is a pure function of authorities (sorted output), so
|
||||
snapshots are byte-deterministic; diff(a, b) answers "what changed" without
|
||||
interpretation.
|
||||
|
||||
Schema version: 1. Unknown metadata survives by construction; a future
|
||||
reader migrates by adding a projector, never by editing old records.
|
||||
|
||||
CLI: PYTHONPATH=src python3 -m rune.spine <command> (JSON canonical).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import ( # shared generic core (SCAFFOLD-TRANCHE-001)
|
||||
SCHEMA_VERSION, MATURITY_ORDER, MATURITY_RANK, REL_STATUS,
|
||||
NON_OBLIGATION_RELATIONSHIPS, FRONTIER_CONTRACT_EXPANSION, FRONTIER_LEVEL2_TYPES,
|
||||
now as _ss_now, _sorted as _ss_sorted, new_doc as new_doc, load as _ss_load,
|
||||
save as _ss_save, snapshot as _ss_snapshot, add_entity, add_relationship,
|
||||
relationships, promote, alias, supersede, spine_uri, resolve_uri, bind_task,
|
||||
task_entities, impact_frontier, record_observed_impact, predicted_vs_observed,
|
||||
structural_events, diff, _doc_authorities, _authority_observed, authority_failed,
|
||||
reconcile_authorities, mark_conflict, resolve_status, authority_health,
|
||||
dedupe_observations as _ss_dedupe_observations,
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
MATURITY_ORDER = ["DECLARED", "PRESENT", "WIRED", "REACHABLE", "EXERCISED", "LIVE_PROVEN"]
|
||||
_MATURITY_RANK = {name: i for i, name in enumerate(MATURITY_ORDER)}
|
||||
|
||||
GENERATED_VIEW_MARKER = "_generated_by_spine"
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2] # repo root (src/rune/spine.py → up 2)
|
||||
SRC_DIR = ROOT / "src" / "rune"
|
||||
TESTS_DIR = ROOT / "tests"
|
||||
STORE_DIR = ROOT / "store" / "spine"
|
||||
CURRENT = STORE_DIR / "current.json"
|
||||
SNAPSHOTS = STORE_DIR / "snapshots"
|
||||
# Deployment observation point (this machine's promoted build). Configurable:
|
||||
# never an identity, only an observation surface.
|
||||
DEPLOYED_LINK = Path(os.environ.get("RUNE_DEPLOYED_LINK", "/mnt/workdrive/rune-code/current"))
|
||||
UMBRELLA_MANIFEST = ROOT / "config" / "umbrellas.json"
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _sorted(obj):
|
||||
"""Deterministic normalization: sort everything sortable, recursively."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: _sorted(obj[k]) for k in sorted(obj)}
|
||||
if isinstance(obj, list):
|
||||
return sorted((_sorted(v) for v in obj), key=lambda v: json.dumps(v, sort_keys=True))
|
||||
return obj
|
||||
|
||||
|
||||
# ── store ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def load(path: Path | None = None) -> dict | None:
|
||||
p = path or CURRENT
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def save(doc: dict, path: Path | str | None = None) -> Path:
|
||||
incoming = doc.get("schema_version", SCHEMA_VERSION)
|
||||
if incoming != SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version {incoming} != {SCHEMA_VERSION}")
|
||||
doc["schema_version"] = SCHEMA_VERSION
|
||||
p = Path(path) if path else CURRENT # str paths are legitimate (CLI callers)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(_sorted(doc), indent=1, sort_keys=True) + "\n")
|
||||
return p
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ── static projections (deterministic, from authorities) ─────────────────
|
||||
|
||||
def _module_imports(module: str) -> set[str]:
|
||||
path = SRC_DIR / f"{module}.py"
|
||||
try:
|
||||
tree = ast.parse(path.read_text())
|
||||
except (OSError, SyntaxError):
|
||||
return set()
|
||||
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_DIR.glob("*.py")}
|
||||
|
||||
|
||||
def _canary_registry() -> list[str]:
|
||||
try:
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
from rune import canary_native
|
||||
return list(canary_native.MODULES)
|
||||
except ImportError:
|
||||
# canary_native pulls optional deps (sqlite_vec etc.); degrade to an
|
||||
# empty registry — drift will note absent registrations honestly.
|
||||
return []
|
||||
|
||||
|
||||
def _deployed_observation() -> dict | None:
|
||||
"""Observe the deployment authority — never invent it."""
|
||||
ident = DEPLOYED_LINK / "src" / "rune" / "_build_identity.json"
|
||||
if not ident.is_file():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(ident.read_text())
|
||||
return {"deployed_sha": data.get("commit", "")[:12],
|
||||
"deployed_version": data.get("version", ""),
|
||||
"deployed_at": data.get("deployed_at", ""),
|
||||
"observed_from": str(DEPLOYED_LINK)}
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _test_backed_modules() -> set[str]:
|
||||
out = set()
|
||||
for p in TESTS_DIR.glob("test_*.py"):
|
||||
out.add(p.stem[5:])
|
||||
return out
|
||||
|
||||
|
||||
def ingest_umbrellas(doc: dict, manifest_path: Path | None = None) -> int:
|
||||
"""Phase A: translate the (last manually maintained) umbrella manifest
|
||||
into Spine entities + relationships. Refuses a Spine-GENERATED view —
|
||||
that guards the ingest cycle after Phase C makes the view output-only.
|
||||
Returns count of subsystems ingested."""
|
||||
path = manifest_path or UMBRELLA_MANIFEST
|
||||
text = path.read_text()
|
||||
data = json.loads(text)
|
||||
if GENERATED_VIEW_MARKER in data:
|
||||
raise ValueError(
|
||||
f"{path} is a Spine-generated compatibility view; refusing to "
|
||||
"ingest it (Phase A reads the manual authority exactly once). "
|
||||
"Structural membership now lives in the Spine store.")
|
||||
count = 0
|
||||
KNOWN_FIELDS = {"description", "modules", "depends_on", "mirrors", "canaries"}
|
||||
for name, u in data.get("umbrellas", {}).items():
|
||||
sid = f"subsystem:{name}"
|
||||
# canaries ride as ext data (obligation metadata, not structure);
|
||||
# unknown future manifest fields ride too — open-world preservation
|
||||
ext = {k: u[k] for k in u if k in ("canaries",) or k not in KNOWN_FIELDS}
|
||||
add_entity(doc, id=sid, kind="subsystem",
|
||||
identities={"description": u.get("description", "")},
|
||||
evidence_refs=[f"umbrella-manifest#{name}"],
|
||||
maturity="DECLARED", ext=ext or None)
|
||||
count += 1
|
||||
for dep in u.get("depends_on", []):
|
||||
add_relationship(doc, type="depends_on", from_id=sid,
|
||||
to_id=f"subsystem:{dep}",
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
for mirror in u.get("mirrors", []):
|
||||
add_relationship(doc, type="mirrors", from_id=sid, to_id=f"file:{mirror}",
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
for canary in u.get("canaries", []):
|
||||
add_relationship(doc, type="obligated_canary", from_id=sid,
|
||||
to_id=f"module:{canary}",
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
for mod in u.get("modules", []):
|
||||
mid = f"module:{mod}"
|
||||
add_entity(doc, id=mid, kind="module", tags=["code"],
|
||||
identities={"source": f"src/rune/{mod}.py"},
|
||||
evidence_refs=[f"umbrella-manifest#{name}"],
|
||||
maturity="DECLARED")
|
||||
add_relationship(doc, type="belongs_to", from_id=mid, to_id=sid,
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
return count
|
||||
|
||||
|
||||
def build(manifest_path: Path | None = None) -> dict:
|
||||
"""Deterministic full projection from authorities. Same inputs →
|
||||
byte-identical doc (modulo generated_at)."""
|
||||
doc = new_doc()
|
||||
|
||||
# 1. subsystem structure — from the Spine store if it already owns
|
||||
# membership (Phase B+), else from the manual manifest (Phase A).
|
||||
existing = load()
|
||||
seeded = False
|
||||
if existing:
|
||||
rels = [r for r in existing["relationships"] if r["type"] == "belongs_to"]
|
||||
if rels:
|
||||
seeded = True
|
||||
# CARRY EVERYTHING FORWARD — open-world requirement. Filtering
|
||||
# by known kinds/types here would silently destroy tasks,
|
||||
# contracts, capabilities, and any future entity class on every
|
||||
# refresh (the exact TOO-CLOSED failure the contract forbids;
|
||||
# observed live: task bindings vanished through build()).
|
||||
# Module-level facts (imports, tests, maturity) are RE-DERIVED
|
||||
# below onto the carried state; everything else passes through.
|
||||
doc["entities"].extend(json.loads(json.dumps(existing["entities"])))
|
||||
doc["relationships"].extend(json.loads(json.dumps(existing["relationships"])))
|
||||
doc["observations"].extend(json.loads(json.dumps(
|
||||
existing.get("observations", []))))
|
||||
if not seeded:
|
||||
ingest_umbrellas(doc, manifest_path)
|
||||
|
||||
# 2. module-level structural facts from actual code
|
||||
graph = _import_graph()
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
for m in sorted(mods):
|
||||
mid = f"module:{m}"
|
||||
e = entity(doc, mid)
|
||||
# Re-derivation uses the HIGH-WATER path (add_entity upgrades only,
|
||||
# never logs): fresh evidence may be lower than the store's earned
|
||||
# state on any given pass, and that is not a demotion event — it is
|
||||
# a build. Explicit promote() logging is for evidence-gated API calls.
|
||||
def _derive(state: str, ev: str) -> None:
|
||||
add_entity(doc, id=mid, kind="module", maturity=state,
|
||||
evidence_refs=[ev])
|
||||
|
||||
if (SRC_DIR / f"{m}.py").is_file():
|
||||
assert e is not None # membership loop guarantees presence
|
||||
e["identities"]["source"] = f"src/rune/{m}.py"
|
||||
_derive("PRESENT", f"source:{m}.py")
|
||||
consumers = sorted(c for c, deps in graph.items() if m in deps and c != m)
|
||||
deps = sorted(d for d in graph.get(m, set()) if d in mods and d != m)
|
||||
for c in consumers:
|
||||
rel = add_relationship(doc, type="consumed_by", from_id=mid,
|
||||
to_id=f"module:{c}", evidence_refs=[f"ast-import:{m}"])
|
||||
_authority_observed(doc, rel, "ast-import-projector")
|
||||
for d in deps:
|
||||
rel = add_relationship(doc, type="depends_on", from_id=mid,
|
||||
to_id=f"module:{d}", evidence_refs=[f"ast-import:{m}"])
|
||||
_authority_observed(doc, rel, "ast-import-projector")
|
||||
# WIRED: importers exist, or registry wiring (canary registry)
|
||||
if consumers or m in registry:
|
||||
_derive("WIRED", f"ast-imports:{len(consumers)}" if consumers
|
||||
else "canary-registry")
|
||||
# EXERCISED: dedicated test or canary exercise
|
||||
if m in tested:
|
||||
_derive("EXERCISED", f"tests/test_{m}.py")
|
||||
elif m in registry:
|
||||
_derive("EXERCISED", "canary-registry-exercise")
|
||||
|
||||
# 3. runtime/deployment observation (dynamic, bounded)
|
||||
obs = _deployed_observation()
|
||||
if obs:
|
||||
_authority_observed(doc, None, "deploy-identity-projector") # generation bump
|
||||
dep = add_entity(doc, id="deployment:current", kind="deployment_surface",
|
||||
identities=obs, evidence_refs=["_build_identity.json"],
|
||||
maturity="LIVE_PROVEN")
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] == "module" and (DEPLOYED_LINK / e["identities"].get("source", "")).is_file():
|
||||
rel = add_relationship(doc, type="deployed_as", from_id=e["id"],
|
||||
to_id="deployment:current",
|
||||
evidence_refs=[f"deployed-tree:{obs['deployed_sha']}"])
|
||||
_authority_observed(doc, rel, "deploy-identity-projector")
|
||||
doc["observations"].append({"kind": "deployed_identity", **obs, "observed_at": now()})
|
||||
|
||||
# reconcile: facts owned by projectors that no longer observe them
|
||||
# become STALE (marked, never deleted — history survives; open-world
|
||||
# preservation applies to everything, staleness is the truth marker)
|
||||
staled = reconcile_authorities(doc)
|
||||
for s in staled:
|
||||
doc["observations"].append({"kind": "fact_staled", **s, "observed_at": now()})
|
||||
|
||||
# exact-duplicate observation suppression (carry-forward + this pass
|
||||
# both record deployed_identity etc.); distinct repeats are history
|
||||
seen_obs = set()
|
||||
unique_obs = []
|
||||
for o in doc["observations"]:
|
||||
key = json.dumps(_sorted(o), sort_keys=True)
|
||||
if key not in seen_obs:
|
||||
seen_obs.add(key)
|
||||
unique_obs.append(o)
|
||||
doc["observations"] = unique_obs
|
||||
doc["generated_at"] = now()
|
||||
return doc
|
||||
|
||||
|
||||
# ── drift ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def drift(doc: dict) -> list[dict]:
|
||||
"""Architectural drift classes. Reuses authorities' verdicts; never
|
||||
duplicates their logic. Deterministic output."""
|
||||
out = []
|
||||
graph = _import_graph()
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
|
||||
# 1. registered but absent (canary registry names a non-existent module)
|
||||
for m in registry:
|
||||
if m not in mods:
|
||||
out.append({"class": "registered_but_absent", "entity": f"module:{m}",
|
||||
"detail": "in canary registry, no source file"})
|
||||
|
||||
# 2. present but unowned (runtime source file outside every subsystem).
|
||||
# Excludes canary files (governed by the canary contract, not umbrella
|
||||
# membership) and package plumbing (__init__/__main__).
|
||||
for p in SRC_DIR.glob("*.py"):
|
||||
if p.stem.startswith(("canary_", "__")):
|
||||
continue
|
||||
if p.stem not in mods:
|
||||
out.append({"class": "present_but_unowned", "entity": f"module:{p.stem}",
|
||||
"detail": "src/rune runtime module with no subsystem membership"})
|
||||
|
||||
# 3. registered but no static importers — OBSERVATION, not a death
|
||||
# verdict: registry dispatch and entry points wire dynamically
|
||||
# (task_gate via capability_broker, rune_tui via entry point).
|
||||
# Interpretation belongs to cognition; Spine reports the fact.
|
||||
for m in sorted(mods & set(registry)):
|
||||
importers = [c for c, deps in graph.items() if m in deps and c != m]
|
||||
if not importers:
|
||||
dyn = "no dedicated test either" if m not in tested else "dedicated test exists"
|
||||
out.append({"class": "registered_no_static_importers", "entity": f"module:{m}",
|
||||
"detail": f"zero static importers; dynamic wiring possible; {dyn}"})
|
||||
|
||||
# 4. deployed identity drift (source version vs deployment authority)
|
||||
obs = _deployed_observation()
|
||||
try:
|
||||
src_version = (ROOT / "pyproject.toml").read_text()
|
||||
import re
|
||||
m = re.search(r'^version\s*=\s*"([^"]+)"', src_version, re.M)
|
||||
src_version = m.group(1) if m else ""
|
||||
except OSError:
|
||||
src_version = ""
|
||||
if obs and src_version and obs["deployed_version"] != src_version:
|
||||
out.append({"class": "version_surfaces_disagree",
|
||||
"detail": f"source {src_version} != deployed {obs['deployed_version']}"})
|
||||
|
||||
# 4b. evidence staleness (adapter freshness): maturity claims whose
|
||||
# evidence references predate the current store generation are STALE
|
||||
# observations, never silently current — "not checked" != "healthy".
|
||||
for e in doc["entities"]:
|
||||
if e.get("maturity") in ("EXERCISED", "LIVE_PROVEN") and not e.get("evidence_refs"):
|
||||
out.append({"class": "evidence_conflict", "entity": e["id"],
|
||||
"detail": f"claims {e['maturity']} with zero evidence refs"})
|
||||
|
||||
# 4c. stale facts — owned by an authority that no longer observes them
|
||||
for rel in doc["relationships"]:
|
||||
if rel.get("status") == "STALE":
|
||||
out.append({"class": "stale_fact", "entity": f"{rel['from']}->{rel['to']}",
|
||||
"detail": f"{rel['type']} edge; authority {rel.get('authority')} "
|
||||
"no longer observes it"})
|
||||
|
||||
# 5. relationship disappeared (store claims consumers that no longer import)
|
||||
for r in relationships(doc, type="consumed_by"):
|
||||
m = r["from"].split(":", 1)[1]
|
||||
c = r["to"].split(":", 1)[1]
|
||||
if m not in graph.get(c, set()):
|
||||
out.append({"class": "relationship_disappeared",
|
||||
"detail": f"{c} no longer imports {m} (stale store edge)"})
|
||||
return out
|
||||
|
||||
|
||||
# ── snapshots / deltas ────────────────────────────────────────────────────
|
||||
|
||||
def snapshot(doc: dict, label: str = "") -> Path:
|
||||
SNAPSHOTS.mkdir(parents=True, exist_ok=True)
|
||||
obs = _deployed_observation() or {}
|
||||
name = f"snap-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{obs.get('deployed_sha', 'noident')[:8]}"
|
||||
if label:
|
||||
name += f"-{label}"
|
||||
p = SNAPSHOTS / f"{name}.json"
|
||||
save(doc, p)
|
||||
return p
|
||||
|
||||
|
||||
|
||||
# ── compatibility projection (Phase C) ────────────────────────────────────
|
||||
|
||||
def export_umbrellas(doc: dict) -> dict:
|
||||
"""Generate the legacy umbrella view FROM Spine. Output only — the old
|
||||
consumers (CI gate, henxels) read this during migration. Never ingest."""
|
||||
umbrellas: dict = {}
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "subsystem":
|
||||
continue
|
||||
name = e["id"].split(":", 1)[1]
|
||||
umbrellas[name] = {"description": e["identities"].get("description", ""),
|
||||
"modules": [], "depends_on": [], "mirrors": []}
|
||||
# faithful reproduction: emit canaries only when the ingested
|
||||
# authority carried them (the legacy contract omits empty lists)
|
||||
if e.get("ext", {}).get("canaries"):
|
||||
umbrellas[name]["canaries"] = sorted(e["ext"]["canaries"])
|
||||
for r in doc["relationships"]:
|
||||
if r["type"] == "belongs_to" and r["from"].startswith("module:"):
|
||||
sub = r["to"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["modules"].append(r["from"].split(":", 1)[1])
|
||||
elif r["type"] == "depends_on" and r["from"].startswith("subsystem:"):
|
||||
sub = r["from"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["depends_on"].append(r["to"].split(":", 1)[1])
|
||||
elif r["type"] == "mirrors" and r["from"].startswith("subsystem:"):
|
||||
sub = r["from"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["mirrors"].append(r["to"].split(":", 1)[1])
|
||||
elif r["type"] == "obligated_canary" and r["from"].startswith("subsystem:"):
|
||||
sub = r["from"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["canaries"].append(r["to"].split(":", 1)[1])
|
||||
for u in umbrellas.values():
|
||||
u["modules"].sort(); u["depends_on"].sort(); u["mirrors"].sort()
|
||||
if "canaries" in u:
|
||||
u["canaries"].sort()
|
||||
return {"_comment": "GENERATED by rune.spine — 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_VIEW_MARKER: True,
|
||||
"generated_at": now(),
|
||||
"umbrellas": dict(sorted(umbrellas.items()))}
|
||||
|
||||
|
||||
# ── contracts, task binding, impact frontier (T22 + graph addenda) ────────
|
||||
#
|
||||
# GRAPH SEPARATION: Spine is its own LOGICAL graph — current structural
|
||||
# truth, not memory. The frontier traverses Spine edges ONLY. Memory-style
|
||||
# associations (hypotheses, history, lessons) must never expand structural
|
||||
# impact: they are not in this graph, and non-obligation relationship
|
||||
# classes are excluded by the semantics table below. Cross-linking happens
|
||||
# by stable spine:// URIs — a future Memory record references a Spine
|
||||
# entity without duplicating it; structural edges stay Spine-authoritative.
|
||||
|
||||
NON_OBLIGATION_RELATIONSHIPS = {
|
||||
# Data-driven semantics table — NOT a closed ontology: unknown types
|
||||
# default to obligation-bearing (fail-loud), listed types are known
|
||||
# informational edges that must not explode a change frontier.
|
||||
"documented_by", "historical_relation", "related_memory",
|
||||
"supersedes", # supersession is context, not a live obligation
|
||||
}
|
||||
|
||||
# Relationships whose TRAVERSAL creates LEVEL-1 frontier entries, per
|
||||
# direction: type → ("outgoing" edges from X, "incoming" edges into X)
|
||||
FRONTIER_LEVEL1 = {
|
||||
"consumed_by": "outgoing", # X is consumed by → consumers
|
||||
"depends_on": "outgoing", # X depends on → its dependencies
|
||||
"implements": "outgoing", # X implements contract → the contract
|
||||
"consumes": "outgoing", # X consumes contract → the contract
|
||||
"belongs_to": "outgoing", # ownership context
|
||||
"deployed_as": "outgoing", # deployment surface
|
||||
}
|
||||
FRONTIER_CONTRACT_EXPANSION = {"implements", "consumes"}
|
||||
# LEVEL 2: verification surfaces attached to frontier entities
|
||||
FRONTIER_LEVEL2_TYPES = {"verified_by", "obligated_canary"}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def query(doc: dict, entity_id: str) -> dict:
|
||||
"""INTRINSIC SELF-KNOWLEDGE: one deterministic call answering the
|
||||
representative structural question set. No LLM, no Memory, no rediscovery."""
|
||||
e = entity(doc, entity_id)
|
||||
if e is None:
|
||||
return {"error": "not found", "id": entity_id}
|
||||
return {
|
||||
"id": entity_id,
|
||||
"uri": spine_uri(entity_id),
|
||||
"kind": e["kind"],
|
||||
"owner": [r["to"] for r in relationships(doc, from_id=entity_id, type="belongs_to")],
|
||||
"dependencies": sorted({r["to"] for r in relationships(doc, from_id=entity_id, type="depends_on")}),
|
||||
"consumers": sorted({r["to"] for r in relationships(doc, from_id=entity_id, type="consumed_by")}
|
||||
| {r["from"] for r in relationships(doc, to_id=entity_id, type="consumed_by")}),
|
||||
"contracts_implements": sorted(r["to"] for r in relationships(doc, from_id=entity_id, type="implements")),
|
||||
"contracts_consumes": sorted(r["to"] for r in relationships(doc, from_id=entity_id, type="consumes")),
|
||||
"state": {"maturity": e.get("maturity"), "observed_at": e.get("observed_at"),
|
||||
"resolved_status": resolve_status(e),
|
||||
"superseded_by": e.get("superseded_by"),
|
||||
"aliases": e.get("aliases", [])},
|
||||
"evidence": e.get("evidence_refs", []),
|
||||
"impact_frontier": impact_frontier(doc, [entity_id]),
|
||||
}
|
||||
|
||||
|
||||
# ── authority/projector ownership + reconciliation (hardening T3/T4) ──────
|
||||
#
|
||||
# A projected fact carries `authority` = the projector that owns it. On
|
||||
# refresh, each authority's pass records a generation and the facts it still
|
||||
# observes. Facts the owner no longer emits become STALE — marked, reported
|
||||
# in drift, excluded from frontiers — never silently current, never silently
|
||||
# deleted (history survives). Facts of OTHER authorities (or manual facts
|
||||
# with no authority) are never touched by a foreign refresh: the
|
||||
# reconciliation boundary is AUTHORITY OWNERSHIP, not type knowledge.
|
||||
|
||||
REL_STATUS = {"UNKNOWN", "CURRENT", "STALE", "CONFLICTING", "DEGRADED"}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ── durable identity: rename / alias / supersession (hardening T5) ────────
|
||||
|
||||
|
||||
|
||||
def entity(doc: dict, id: str) -> dict | None:
|
||||
doc_ents = doc.get("entities", [])
|
||||
for e in doc_ents:
|
||||
if e["id"] == id:
|
||||
return e
|
||||
# rename-resilient resolution: old spine:// references stay meaningful
|
||||
for e in doc_ents:
|
||||
if id in e.get("aliases", []):
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _fresh_doc() -> dict:
|
||||
doc = load()
|
||||
if doc is None:
|
||||
doc = build()
|
||||
save(doc)
|
||||
return doc
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
cmd = argv[0]
|
||||
args = argv[1:]
|
||||
|
||||
if cmd == "ingest": # Phase A: seed from manual manifest (explicit)
|
||||
doc = new_doc()
|
||||
n = ingest_umbrellas(doc, Path(args[0]) if args else None)
|
||||
save(doc)
|
||||
print(json.dumps({"ingested_subsystems": n, "store": str(CURRENT)}))
|
||||
return 0
|
||||
if cmd == "build": # full deterministic projection
|
||||
doc = build()
|
||||
save(doc)
|
||||
print(json.dumps({"entities": len(doc["entities"]),
|
||||
"relationships": len(doc["relationships"]),
|
||||
"observations": len(doc["observations"]),
|
||||
"store": str(CURRENT)}))
|
||||
return 0
|
||||
|
||||
doc = _fresh_doc()
|
||||
|
||||
if cmd == "entity":
|
||||
e = entity(doc, args[0]) if args else None
|
||||
print(json.dumps(e) if e else json.dumps({"error": "not found", "id": args[0] if args else ""}))
|
||||
return 0 if e else 1
|
||||
if cmd == "relationships":
|
||||
out = relationships(doc, from_id=args[0] if args else None,
|
||||
type=args[1] if len(args) > 1 else None)
|
||||
print(json.dumps(out, indent=1))
|
||||
return 0
|
||||
if cmd == "owner":
|
||||
e = entity(doc, args[0]) if args else None
|
||||
if not e:
|
||||
print(json.dumps({"error": "not found"})); return 1
|
||||
subs = [r["to"] for r in relationships(doc, from_id=e["id"], type="belongs_to")]
|
||||
print(json.dumps({"id": e["id"], "subsystem": subs}))
|
||||
return 0
|
||||
if cmd == "dependencies":
|
||||
out = [r["to"] for r in relationships(doc, from_id=args[0], type="depends_on")] if args else []
|
||||
print(json.dumps({"id": args[0] if args else "", "dependencies": sorted(out)}))
|
||||
return 0
|
||||
if cmd == "consumers":
|
||||
# "consumed_by" edges run provider → consumer; consumers of X are
|
||||
# the "to" ends of edges leaving X.
|
||||
out = [r["to"] for r in relationships(doc, from_id=args[0], type="consumed_by")] if args else []
|
||||
print(json.dumps({"id": args[0] if args else "", "consumers": sorted(set(out))}))
|
||||
return 0
|
||||
if cmd == "status":
|
||||
e = entity(doc, args[0]) if args else None
|
||||
if not e:
|
||||
print(json.dumps({"error": "not found"})); return 1
|
||||
print(json.dumps({"id": e["id"], "kind": e["kind"], "maturity": e["maturity"],
|
||||
"evidence_refs": e["evidence_refs"], "observed_at": e["observed_at"]}))
|
||||
return 0
|
||||
if cmd == "drift":
|
||||
print(json.dumps(drift(doc), indent=1))
|
||||
return 0
|
||||
if cmd == "deployed":
|
||||
obs = _deployed_observation()
|
||||
print(json.dumps(obs if obs else {"error": "no deployment observation"}))
|
||||
return 0 if obs else 1
|
||||
if cmd == "snapshot":
|
||||
p = snapshot(doc, args[0] if args else "")
|
||||
print(json.dumps({"snapshot": str(p)}))
|
||||
return 0
|
||||
if cmd == "diff":
|
||||
a, b = load(Path(args[0])), load(Path(args[1]))
|
||||
if not a or not b:
|
||||
print(json.dumps({"error": "snapshot path not found"})); return 1
|
||||
print(json.dumps(diff(a, b), indent=1))
|
||||
return 0
|
||||
if cmd == "export-umbrellas":
|
||||
view = export_umbrellas(doc)
|
||||
target = Path(args[0]) if args else UMBRELLA_MANIFEST
|
||||
if target == UMBRELLA_MANIFEST and target.is_file() and GENERATED_VIEW_MARKER not in json.loads(target.read_text()):
|
||||
print(json.dumps({"error": "refusing to overwrite a MANUAL manifest — "
|
||||
"pass an explicit output path for the generated view"}))
|
||||
return 1
|
||||
target.write_text(json.dumps(view, indent=1) + "\n")
|
||||
print(json.dumps({"written": str(target), "subsystems": len(view["umbrellas"])}))
|
||||
return 0
|
||||
if cmd == "query":
|
||||
print(json.dumps(query(doc, args[0] if args else "")))
|
||||
return 0
|
||||
if cmd == "impact-frontier":
|
||||
seeds = args if args else []
|
||||
if len(seeds) == 1 and seeds[0].startswith("task:"):
|
||||
seeds = task_entities(doc, seeds[0][5:]) or []
|
||||
print(json.dumps(impact_frontier(doc, seeds), indent=1))
|
||||
return 0
|
||||
if cmd == "uri":
|
||||
print(json.dumps({"uri": spine_uri(args[0] if args else "")}))
|
||||
return 0
|
||||
if cmd == "authority-health":
|
||||
print(json.dumps(authority_health(doc), indent=1))
|
||||
return 0
|
||||
if cmd == "alias":
|
||||
# alias <canonical> <old-id> — rename support: old spine:// refs stay resolvable
|
||||
if len(args) < 2:
|
||||
print(json.dumps({"error": "alias <canonical_id> <old_id>"})); return 2
|
||||
e = alias(doc, args[0], args[1])
|
||||
save(doc)
|
||||
print(json.dumps(e or {"error": "not found"}))
|
||||
return 0 if e else 1
|
||||
if cmd == "supersede":
|
||||
# supersede <new> <old> <evidence> — replacement with preserved history
|
||||
if len(args) < 3:
|
||||
print(json.dumps({"error": "supersede <new_id> <old_id> <evidence_ref>"})); return 2
|
||||
old = supersede(doc, args[0], args[1], args[2])
|
||||
save(doc)
|
||||
print(json.dumps(old or {"error": "not found"}))
|
||||
return 0 if old else 1
|
||||
if cmd == "promote":
|
||||
e = promote(doc, args[0], args[1], args[2]) if len(args) > 2 else None
|
||||
save(doc)
|
||||
print(json.dumps(e or {"error": "not found or missing evidence"}))
|
||||
return 0 if e else 1
|
||||
if cmd == "maturity":
|
||||
print(json.dumps(MATURITY_ORDER))
|
||||
return 0
|
||||
|
||||
print(f"unknown command: {cmd}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,639 @@
|
||||
"""structural_store.py — shared generic structural-store primitive.
|
||||
|
||||
The durable-value engine of Rune's structural self-models, extracted from
|
||||
spine.py (2026-09-13, SCAFFOLD-TRANCHE-001) so that Project Scaffolds can
|
||||
use the identical open-world store, evidence-gated maturity ladder,
|
||||
authority-owned reconciliation, alias/supersession, impact-frontier and
|
||||
snapshot/diff machinery WITHOUT importing Rune-specific assumptions.
|
||||
|
||||
This module contains NO Rune-specific constants: no repository paths, no
|
||||
src/rune scanning, no test-dir scanning, no deployed-link inspection, no
|
||||
Umbrella manifest, no Rune entity vocabulary. Rune-specific projectors and
|
||||
paths remain in spine.py; project-specific projectors live in scaffold.py.
|
||||
|
||||
Behavior note: every function here is moved verbatim from spine.py (or
|
||||
parameterized only by explicitly-passed paths). Spine V1's behavior is
|
||||
unchanged; spine.py re-exports these names for compatibility.
|
||||
|
||||
Store-path contract: load/save take an explicit `path`. Callers that want
|
||||
"the store" pass their own default (spine passes its CURRENT; scaffold
|
||||
passes the project's). Nothing here resolves a path from the environment.
|
||||
|
||||
Entity/relationship vocabulary is OPEN: kinds, relationship types and
|
||||
metadata are data, never validated against a whitelist. Unknown future
|
||||
kinds/fields round-trip by construction.
|
||||
|
||||
Schema version: 1 (shared with spine stores).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
MATURITY_ORDER = ["DECLARED", "PRESENT", "WIRED", "REACHABLE", "EXERCISED", "LIVE_PROVEN"]
|
||||
MATURITY_RANK = {name: i for i, name in enumerate(MATURITY_ORDER)}
|
||||
|
||||
REL_STATUS = {"UNKNOWN", "CURRENT", "STALE", "CONFLICTING", "DEGRADED"}
|
||||
|
||||
# Relationship classes that must never expand an impact frontier
|
||||
# (data-driven semantics table; unknown types default to obligation-bearing).
|
||||
NON_OBLIGATION_RELATIONSHIPS = {
|
||||
"documented_by",
|
||||
"historical_relation",
|
||||
"related_memory",
|
||||
"supersedes", # supersession is context, not a live obligation
|
||||
}
|
||||
FRONTIER_LEVEL1_OUT = {"consumed_by", "depends_on", "implements", "consumes", "belongs_to", "deployed_as"}
|
||||
FRONTIER_CONTRACT_EXPANSION = {"implements", "consumes"}
|
||||
FRONTIER_LEVEL2_TYPES = {"verified_by", "obligated_canary"}
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _sorted(obj):
|
||||
"""Deterministic normalization: sort everything sortable, recursively."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: _sorted(obj[k]) for k in sorted(obj)}
|
||||
if isinstance(obj, list):
|
||||
return sorted((_sorted(v) for v in obj), key=lambda v: json.dumps(v, sort_keys=True))
|
||||
return obj
|
||||
|
||||
|
||||
def new_doc() -> dict:
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"generated_at": now(),
|
||||
"entities": [],
|
||||
"relationships": [],
|
||||
"observations": [],
|
||||
}
|
||||
|
||||
|
||||
# ── store I/O (path-explicit; no environment resolution) ─────────────────
|
||||
|
||||
|
||||
def load(path: Path) -> dict | None:
|
||||
if not Path(path).is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(Path(path).read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
_SECRET_KEY_NAMES = {
|
||||
"token",
|
||||
"api_token",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"password",
|
||||
"secret",
|
||||
"auth_header",
|
||||
"authorization",
|
||||
"credential",
|
||||
"credentials",
|
||||
"private_key",
|
||||
"access_token",
|
||||
"bearer",
|
||||
}
|
||||
|
||||
|
||||
def _redact_secrets(obj):
|
||||
"""RECURSIVE SECRET REDACTION (defense in depth for persistence): any
|
||||
dict key named like a credential has its value stripped before
|
||||
serialization. Structural stores must never become secret stores."""
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
k: ("<redacted>" if str(k).lower() in _SECRET_KEY_NAMES else _redact_secrets(v)) for k, v in obj.items()
|
||||
}
|
||||
if isinstance(obj, list):
|
||||
return [_redact_secrets(v) for v in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def save(doc: dict, path: Path | str) -> Path:
|
||||
incoming = doc.get("schema_version", SCHEMA_VERSION)
|
||||
if incoming != SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version {incoming} != {SCHEMA_VERSION}")
|
||||
doc["schema_version"] = SCHEMA_VERSION
|
||||
p = Path(path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(_sorted(_redact_secrets(doc)), indent=1, sort_keys=True) + "\n")
|
||||
return p
|
||||
|
||||
|
||||
def snapshot(doc: dict, snapshots_dir: Path, label: str = "", tag: str = "") -> Path:
|
||||
"""Persist an immutable snapshot. `tag` is a caller-supplied identity
|
||||
fragment (Spine uses the deployed SHA; a Project Scaffold may use its
|
||||
repo HEAD or 'noident')."""
|
||||
snapshots_dir = Path(snapshots_dir)
|
||||
snapshots_dir.mkdir(parents=True, exist_ok=True)
|
||||
name = f"snap-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{(tag or 'noident')[:8]}"
|
||||
if label:
|
||||
name += f"-{label}"
|
||||
p = snapshots_dir / f"{name}.json"
|
||||
save(doc, p)
|
||||
return p
|
||||
|
||||
|
||||
# ── entities & relationships (open world) ─────────────────────────────────
|
||||
|
||||
|
||||
def add_entity(
|
||||
doc: dict, *, id: str, kind: str, tags=None, identities=None, evidence_refs=None, maturity="DECLARED", ext=None
|
||||
) -> dict:
|
||||
"""Register an entity. kind/tags/ext are open — unknown values are data.
|
||||
|
||||
Never downgrades an existing entity's maturity (high-water upgrade):
|
||||
re-derivation with fresher but lower evidence keeps the earned state."""
|
||||
for e in doc["entities"]:
|
||||
if e["id"] == id:
|
||||
if maturity and MATURITY_RANK.get(maturity, 0) > MATURITY_RANK.get(e.get("maturity", "DECLARED"), 0):
|
||||
e["maturity"] = maturity
|
||||
if tags:
|
||||
e["tags"] = sorted(set(e.get("tags", [])) | set(tags))
|
||||
if identities:
|
||||
e.setdefault("identities", {}).update(identities)
|
||||
if evidence_refs:
|
||||
e["evidence_refs"] = sorted(set(e.get("evidence_refs", [])) | set(evidence_refs))
|
||||
if ext:
|
||||
e.setdefault("ext", {}).update(ext)
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
ent = {
|
||||
"id": id,
|
||||
"kind": kind,
|
||||
"maturity": maturity,
|
||||
"tags": sorted(tags or []),
|
||||
"identities": dict(identities or {}),
|
||||
"evidence_refs": sorted(evidence_refs or []),
|
||||
"observed_at": now(),
|
||||
}
|
||||
if ext:
|
||||
ent["ext"] = dict(ext)
|
||||
doc["entities"].append(ent)
|
||||
return ent
|
||||
|
||||
|
||||
def add_relationship(doc: dict, *, type: str, from_id: str, to_id: str, evidence_refs=None, ext=None) -> dict:
|
||||
rel = {
|
||||
"type": type,
|
||||
"from": from_id,
|
||||
"to": to_id,
|
||||
"evidence_refs": sorted(evidence_refs or []),
|
||||
"observed_at": now(),
|
||||
}
|
||||
if ext:
|
||||
rel["ext"] = dict(ext)
|
||||
if not any(r["type"] == type and r["from"] == from_id and r["to"] == to_id for r in doc["relationships"]):
|
||||
# MODEL SPECULATION GUARD: a relationship proposed without evidence
|
||||
# is CANDIDATE — representable but never structural truth (excluded
|
||||
# from frontiers until evidence promotes it).
|
||||
if not rel["evidence_refs"] and "status" not in rel:
|
||||
rel["status"] = "CANDIDATE"
|
||||
doc["relationships"].append(rel)
|
||||
return rel
|
||||
|
||||
|
||||
def entity(doc: dict, id: str) -> dict | None:
|
||||
"""Resolve an entity id; follows alias entries so old references stay
|
||||
meaningful after rename (rename-resilient resolution)."""
|
||||
doc_ents = doc.get("entities", [])
|
||||
for e in doc_ents:
|
||||
if e["id"] == id:
|
||||
return e
|
||||
for e in doc_ents:
|
||||
if id in e.get("aliases", []):
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def relationships(doc: dict, *, from_id=None, to_id=None, type=None) -> list[dict]:
|
||||
out = []
|
||||
for r in doc.get("relationships", []):
|
||||
if from_id and r["from"] != from_id:
|
||||
continue
|
||||
if to_id and r["to"] != to_id:
|
||||
continue
|
||||
if type and r["type"] != type:
|
||||
continue
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def promote(doc: dict, id: str, state: str, evidence_ref: str) -> dict | None:
|
||||
"""Evidence-gated maturity promotion. No evidence ref = no promotion.
|
||||
Demotion is never silent: recorded, not applied."""
|
||||
e = entity(doc, id)
|
||||
if e is None:
|
||||
return None
|
||||
if not evidence_ref:
|
||||
raise ValueError(f"promotion of {id} to {state} requires evidence")
|
||||
if state not in MATURITY_RANK:
|
||||
raise ValueError(f"unknown maturity state: {state}")
|
||||
current = MATURITY_RANK.get(e.get("maturity", "DECLARED"), 0)
|
||||
if MATURITY_RANK[state] < current:
|
||||
doc["observations"].append(
|
||||
{
|
||||
"kind": "maturity_demotion_rejected",
|
||||
"entity": id,
|
||||
"requested": state,
|
||||
"current": e["maturity"],
|
||||
"observed_at": now(),
|
||||
}
|
||||
)
|
||||
return e
|
||||
e["maturity"] = state
|
||||
e["evidence_refs"] = sorted(set(e.get("evidence_refs", [])) | {evidence_ref})
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
|
||||
|
||||
# ── durable identity: rename / supersession ───────────────────────────────
|
||||
|
||||
|
||||
def alias(doc: dict, canonical_id: str, old_id: str) -> dict:
|
||||
"""RENAME: same conceptual entity, identifier changed. Old references
|
||||
stay resolvable (entity() follows alias entries)."""
|
||||
e = entity(doc, canonical_id)
|
||||
if e is None:
|
||||
raise ValueError(f"alias target not found: {canonical_id}")
|
||||
if canonical_id == old_id:
|
||||
raise ValueError("self-alias")
|
||||
aliases = e.setdefault("aliases", [])
|
||||
if old_id not in aliases:
|
||||
aliases.append(old_id)
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
|
||||
|
||||
def supersede(doc: dict, new_id: str, old_id: str, evidence_ref: str) -> dict:
|
||||
"""REPLACEMENT: new entity supersedes old; old identity remains
|
||||
resolvable historically. Evidence required."""
|
||||
if not evidence_ref:
|
||||
raise ValueError("supersession requires evidence")
|
||||
new_e, old_e = entity(doc, new_id), entity(doc, old_id)
|
||||
if new_e is None or old_e is None:
|
||||
raise ValueError("supersede requires both entities to exist")
|
||||
add_relationship(doc, type="supersedes", from_id=new_id, to_id=old_id, evidence_refs=[evidence_ref])
|
||||
old_e["superseded_by"] = new_id
|
||||
old_e["status"] = "SUPERSEDED"
|
||||
doc["observations"].append(
|
||||
{"kind": "superseded", "old": old_id, "new": new_id, "evidence": evidence_ref, "observed_at": now()}
|
||||
)
|
||||
return old_e
|
||||
|
||||
|
||||
def spine_uri(entity_id: str) -> str:
|
||||
"""Stable cross-graph identity: spine://<kind>/<name>. A future Memory
|
||||
record cites this URI without duplicating structural authority."""
|
||||
kind, _, name = entity_id.partition(":")
|
||||
return f"spine://{kind}/{name}"
|
||||
|
||||
|
||||
def resolve_uri(uri: str) -> str:
|
||||
scheme, _, rest = uri.partition("://")
|
||||
if scheme != "spine":
|
||||
raise ValueError(f"not a spine URI: {uri}")
|
||||
kind, _, name = rest.partition("/")
|
||||
return f"{kind}:{name}"
|
||||
|
||||
|
||||
# ── authority-owned reconciliation + state semantics ──────────────────────
|
||||
|
||||
|
||||
def _doc_authorities(doc: dict) -> dict:
|
||||
return doc.setdefault("authority_generations", {})
|
||||
|
||||
|
||||
def _authority_observed(doc: dict, rel: dict | None, authority: str) -> None:
|
||||
a = _doc_authorities(doc).setdefault(authority, {"generation": 0, "observed": []})
|
||||
a["generation"] += 1 if rel is None else 0
|
||||
a["last_success"] = now()
|
||||
a["status"] = "CURRENT"
|
||||
if rel is not None:
|
||||
rel["authority"] = authority
|
||||
rel["status"] = "CURRENT"
|
||||
rel["observed_at"] = now()
|
||||
oid = f"{rel['type']}|{rel['from']}|{rel['to']}"
|
||||
if oid not in a["observed"]:
|
||||
a["observed"].append(oid)
|
||||
|
||||
|
||||
def authority_failed(doc: dict, authority: str, error: str) -> None:
|
||||
"""A projector failed: its facts go STALE (not silently fresh) and the
|
||||
authority is recorded DEGRADED."""
|
||||
a = _doc_authorities(doc).setdefault(authority, {"generation": 0, "observed": []})
|
||||
a["last_attempt"] = now()
|
||||
a["last_error"] = error[:200]
|
||||
a["status"] = "DEGRADED"
|
||||
for rel in doc["relationships"]:
|
||||
if rel.get("authority") == authority and rel.get("status") == "CURRENT":
|
||||
rel["status"] = "STALE"
|
||||
doc["observations"].append(
|
||||
{"kind": "authority_degraded", "authority": authority, "error": error[:200], "observed_at": now()}
|
||||
)
|
||||
|
||||
|
||||
def reconcile_authorities(doc: dict) -> list[dict]:
|
||||
"""After projector passes: facts owned by an authority that did not
|
||||
re-observe them become STALE. Manual facts (no authority) untouched."""
|
||||
authorities = _doc_authorities(doc)
|
||||
staled = []
|
||||
for rel in doc["relationships"]:
|
||||
auth = rel.get("authority")
|
||||
if not auth or rel.get("status") == "STALE":
|
||||
continue
|
||||
oid = f"{rel['type']}|{rel['from']}|{rel['to']}"
|
||||
if oid not in authorities.get(auth, {}).get("observed", []):
|
||||
rel["status"] = "STALE"
|
||||
staled.append({"type": rel["type"], "from": rel["from"], "to": rel["to"], "authority": auth})
|
||||
return staled
|
||||
|
||||
|
||||
def mark_conflict(
|
||||
doc: dict, entity_id: str, authority_a: str, claim_a: str, authority_b: str, claim_b: str, evidence_ref: str
|
||||
) -> dict:
|
||||
"""Two authoritative observations disagree: BOTH preserved, entity
|
||||
flagged CONFLICTING — never last-wins."""
|
||||
e = entity(doc, entity_id)
|
||||
if e is None:
|
||||
raise ValueError(f"conflict on unknown entity: {entity_id}")
|
||||
if not evidence_ref:
|
||||
raise ValueError("conflict recording requires evidence")
|
||||
conflicts = e.setdefault("ext", {}).setdefault("conflicts", [])
|
||||
conflicts.append(
|
||||
{
|
||||
"authority_a": authority_a,
|
||||
"claim_a": claim_a,
|
||||
"authority_b": authority_b,
|
||||
"claim_b": claim_b,
|
||||
"evidence": evidence_ref,
|
||||
"observed_at": now(),
|
||||
}
|
||||
)
|
||||
e["status"] = "CONFLICTING"
|
||||
doc["observations"].append(
|
||||
{
|
||||
"kind": "authority_conflict",
|
||||
"entity": entity_id,
|
||||
"authority_a": authority_a,
|
||||
"claim_a": claim_a,
|
||||
"authority_b": authority_b,
|
||||
"claim_b": claim_b,
|
||||
"observed_at": now(),
|
||||
}
|
||||
)
|
||||
return e
|
||||
|
||||
|
||||
def resolve_status(e: dict) -> str:
|
||||
"""Smallest coherent entity state. 'Not checked' is UNKNOWN, never
|
||||
healthy."""
|
||||
if e.get("status"):
|
||||
return e["status"]
|
||||
if e.get("ext", {}).get("conflicts"):
|
||||
return "CONFLICTING"
|
||||
if e.get("maturity") in ("EXERCISED", "LIVE_PROVEN") and e.get("evidence_refs"):
|
||||
return "CURRENT"
|
||||
if e.get("maturity") not in (None, "DECLARED") and not e.get("evidence_refs"):
|
||||
return "CONFLICTING" # claim without proof
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def authority_health(doc: dict) -> dict:
|
||||
"""Provenance/freshness per projector — provenance only, not monitoring."""
|
||||
return {
|
||||
name: {
|
||||
"generation": a.get("generation", 0),
|
||||
"status": a.get("status", "UNKNOWN"),
|
||||
"last_success": a.get("last_success"),
|
||||
"last_attempt": a.get("last_attempt"),
|
||||
"last_error": a.get("last_error"),
|
||||
"facts_observed": len(a.get("observed", [])),
|
||||
}
|
||||
for name, a in sorted(_doc_authorities(doc).items())
|
||||
}
|
||||
|
||||
|
||||
# ── task binding + structural impact resolution ───────────────────────────
|
||||
|
||||
|
||||
def bind_task(doc: dict, task_id: str, entity_ids: list[str], evidence_ref: str = "") -> dict:
|
||||
"""TASK/ISSUE/INTENT BINDING: task → affects → entities. Deterministic."""
|
||||
for eid in entity_ids:
|
||||
add_relationship(
|
||||
doc,
|
||||
type="affects",
|
||||
from_id=f"task:{task_id}",
|
||||
to_id=eid,
|
||||
evidence_refs=[evidence_ref or f"task-binding:{task_id}"],
|
||||
)
|
||||
return entity(doc, f"task:{task_id}") or {"id": f"task:{task_id}"}
|
||||
|
||||
|
||||
def task_entities(doc: dict, task_id: str) -> list[str]:
|
||||
return sorted(r["to"] for r in relationships(doc, from_id=f"task:{task_id}", type="affects"))
|
||||
|
||||
|
||||
def impact_frontier(doc: dict, seed_ids: list[str], max_level: int = 2, change_kind: str | None = None) -> dict:
|
||||
"""STRUCTURAL IMPACT RESOLUTION — bounded, deterministic, Spine-edges-only.
|
||||
|
||||
change_kind semantics: contract_change (seed is a contract → all parties
|
||||
become direct obligations); implementation_change (contract preserved →
|
||||
the contract is the obligation, sibling implementers become contextual);
|
||||
contract_violation (escalates siblings to obligations). None infers from
|
||||
seed kind. Unknown relationship types are obligation-bearing (fail-loud);
|
||||
NON_OBLIGATION types and CANDIDATE/STALE facts are skipped."""
|
||||
seeds = list(dict.fromkeys(seed_ids))
|
||||
level0 = sorted(set(seeds))
|
||||
level1: set[str] = set()
|
||||
contextual: set[str] = set()
|
||||
contracts: set[str] = set()
|
||||
|
||||
if change_kind is None:
|
||||
change_kind = (
|
||||
"contract_change"
|
||||
if any((entity(doc, s) or {}).get("kind") == "contract" for s in level0)
|
||||
else "implementation_change"
|
||||
)
|
||||
|
||||
frontier = set(level0)
|
||||
for rel in doc["relationships"]:
|
||||
if rel["type"] in NON_OBLIGATION_RELATIONSHIPS:
|
||||
continue
|
||||
if rel.get("status") in ("STALE", "CANDIDATE"):
|
||||
continue
|
||||
f, t = rel["from"], rel["to"]
|
||||
if f in frontier:
|
||||
level1.add(t)
|
||||
if t in frontier and rel["type"] == "consumed_by":
|
||||
level1.add(f)
|
||||
|
||||
contract_parties: dict[str, set[str]] = {}
|
||||
seed_contracts = {s for s in level0 if (entity(doc, s) or {}).get("kind") == "contract"}
|
||||
for rel in doc["relationships"]:
|
||||
if rel.get("status") in ("STALE", "CANDIDATE"):
|
||||
continue
|
||||
if rel["type"] in FRONTIER_CONTRACT_EXPANSION:
|
||||
a, b = rel["from"], rel["to"]
|
||||
c = b if (entity(doc, b) or {}).get("kind") == "contract" else a
|
||||
party = a if c == b else b
|
||||
contract_parties.setdefault(c, set()).add(party)
|
||||
if rel["type"] == "implements" and rel["from"] in frontier:
|
||||
seed_contracts.add(rel["to"])
|
||||
if rel["type"] == "consumes" and rel["from"] in frontier:
|
||||
seed_contracts.add(rel["to"])
|
||||
seed_contracts.update(c for c in level1 if (entity(doc, c) or {}).get("kind") == "contract")
|
||||
|
||||
contracts |= seed_contracts
|
||||
if change_kind in ("contract_change", "contract_violation"):
|
||||
for c in seed_contracts:
|
||||
level1 |= contract_parties.get(c, set())
|
||||
else:
|
||||
for c in seed_contracts:
|
||||
contextual |= contract_parties.get(c, set())
|
||||
contextual -= set(level0) | level1
|
||||
|
||||
level1 -= set(level0)
|
||||
level2: set[str] = set()
|
||||
for eid in level1 | set(level0) | contextual:
|
||||
for rel in doc["relationships"]:
|
||||
if rel.get("status") in ("STALE", "CANDIDATE"):
|
||||
continue
|
||||
if rel["type"] in FRONTIER_LEVEL2_TYPES and rel["from"] == eid:
|
||||
level2.add(rel["to"])
|
||||
if rel["type"] == "deployed_as" and rel["from"] in (level1 | set(level0)):
|
||||
level2.add(rel["to"])
|
||||
return {
|
||||
"level0_direct": level0,
|
||||
"level1_obligations": sorted(level1),
|
||||
"contracts": sorted(contracts),
|
||||
"contextual_not_obligations": sorted(contextual),
|
||||
"change_kind": change_kind,
|
||||
"level2_verification": sorted(level2),
|
||||
"traversal_scope": "spine-structural-only",
|
||||
"max_level": max_level,
|
||||
}
|
||||
|
||||
|
||||
def record_observed_impact(doc: dict, task_id: str, evidence_items: list[dict]) -> dict:
|
||||
"""INDEPENDENT OBSERVED IMPACT: derive what was ACTUALLY affected from
|
||||
external evidence (tests, canaries, runtime, deploy) — never by re-running
|
||||
the frontier being evaluated. Nothing auto-promotes into structure."""
|
||||
implicated: set[str] = set()
|
||||
for item in evidence_items:
|
||||
eid = item.get("entity", "")
|
||||
if not eid:
|
||||
continue
|
||||
implicated.add(eid)
|
||||
doc["observations"].append(
|
||||
{
|
||||
"kind": "observed_consequence",
|
||||
"task": task_id,
|
||||
"entity": eid,
|
||||
"source": item.get("source", "unattributed"),
|
||||
"detail": str(item.get("detail", ""))[:300],
|
||||
"authority": item.get("authority", ""),
|
||||
"observed_at": now(),
|
||||
}
|
||||
)
|
||||
return {"task": task_id, "observed_entities": sorted(implicated), "evidence_count": len(evidence_items)}
|
||||
|
||||
|
||||
def predicted_vs_observed(predicted: dict, observed_doc: dict, seed_ids: list[str]) -> dict:
|
||||
observed = impact_frontier(observed_doc, seed_ids)
|
||||
pred_set = {
|
||||
e
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for e in predicted.get(k, [])
|
||||
}
|
||||
obs_set = {
|
||||
e
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for e in observed.get(k, [])
|
||||
}
|
||||
return {
|
||||
"predicted": sorted(pred_set),
|
||||
"observed": sorted(obs_set),
|
||||
"missed": sorted(obs_set - pred_set),
|
||||
"needless": sorted(pred_set - obs_set),
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": e,
|
||||
"status": "CANDIDATE",
|
||||
"note": "observed consequence missing from prediction — evidence required before store update",
|
||||
}
|
||||
for e in sorted(obs_set - pred_set)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def structural_events(doc: dict) -> list[dict]:
|
||||
"""GENERIC EVENT/QUERY SEAM: dumb typed structural facts for future
|
||||
subscribers. No salience, no policy."""
|
||||
events = []
|
||||
for e in doc["entities"]:
|
||||
events.append(
|
||||
{
|
||||
"type": "entity_observed",
|
||||
"entity": e["id"],
|
||||
"kind": e["kind"],
|
||||
"maturity": e.get("maturity"),
|
||||
"observed_at": e.get("observed_at"),
|
||||
}
|
||||
)
|
||||
for r in doc["relationships"]:
|
||||
events.append(
|
||||
{
|
||||
"type": "relationship_observed",
|
||||
"relationship": r["type"],
|
||||
"from": r["from"],
|
||||
"to": r["to"],
|
||||
"observed_at": r.get("observed_at"),
|
||||
}
|
||||
)
|
||||
for o in doc.get("observations", []):
|
||||
events.append({"type": o.get("kind", "observation"), **o})
|
||||
return events
|
||||
|
||||
|
||||
def diff(old: dict, new: dict) -> dict:
|
||||
"""Deterministic structural delta. No interpretation."""
|
||||
oe = {e["id"]: e for e in old.get("entities", [])}
|
||||
ne = {e["id"]: e for e in new.get("entities", [])}
|
||||
ors = {(r["type"], r["from"], r["to"]) for r in old.get("relationships", [])}
|
||||
nrs = {(r["type"], r["from"], r["to"]) for r in new.get("relationships", [])}
|
||||
out = {
|
||||
"entities_added": sorted(set(ne) - set(oe)),
|
||||
"entities_removed": sorted(set(oe) - set(ne)),
|
||||
"relationships_added": sorted({"|".join(t) for t in nrs - ors}),
|
||||
"relationships_removed": sorted({"|".join(t) for t in ors - nrs}),
|
||||
"maturity_changed": [],
|
||||
"identity_changed": [],
|
||||
}
|
||||
for eid in sorted(set(oe) & set(ne)):
|
||||
if oe[eid].get("maturity") != ne[eid].get("maturity"):
|
||||
out["maturity_changed"].append({"id": eid, "from": oe[eid].get("maturity"), "to": ne[eid].get("maturity")})
|
||||
if oe[eid].get("identities") != ne[eid].get("identities"):
|
||||
out["identity_changed"].append(eid)
|
||||
return out
|
||||
|
||||
|
||||
def dedupe_observations(doc: dict) -> None:
|
||||
"""Exact-duplicate observation suppression (carry-forward + re-derivation
|
||||
both record; distinct repeats are history and stay)."""
|
||||
seen: set[str] = set()
|
||||
unique = []
|
||||
for o in doc.get("observations", []):
|
||||
key = json.dumps(_sorted(o), sort_keys=True)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(o)
|
||||
doc["observations"] = unique
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"contract_id": "SCAFFOLD-TRANCHE-001",
|
||||
"need": "Scaffolding needs its construction substrate: a shared generic structural-store primitive (extracted from spine.py without behavior change), a Project Scaffold layer over it, and a Construction Contract object durable enough that a fresh session recovers build state without transcripts.",
|
||||
"origin": "user-approved canonical Scaffolding architecture + Repository/Forge amendment (2026-09-13)",
|
||||
"approved_scope": [
|
||||
"src/rune/structural_store.py (new) — generic primitives extracted from spine.py, no Rune constants",
|
||||
"src/rune/spine.py (refactor) — imports shared core; Rune projectors/constants remain; behavior-neutral",
|
||||
"src/rune/scaffold.py (new) — Project Scaffold thin layer: repo_identity, contracts, project vocabulary, drift",
|
||||
"tests/test_structural_store.py (new) — boundary + roundtrip tests",
|
||||
"tests/test_scaffold.py (new) — scaffold/contract/repo-binding tests",
|
||||
"tests/test_spine.py (amend) — monkeypatch-to-parameter conversions only",
|
||||
"docs/SCAFFOLDING.md (rewrite) — canonical architecture + amended maturity"
|
||||
],
|
||||
"architecture_authority": "canonical Scaffolding report + Repository/Forge amendment (user-reviewed, 2026-09-13); Spine V1 frozen behaviorally",
|
||||
"predicted_affected_entities": [
|
||||
"module:spine",
|
||||
"module:structural_store",
|
||||
"module:scaffold"
|
||||
],
|
||||
"predicted_frontier_snapshot": {
|
||||
"note": "recorded at approval from rune.spine.impact_frontier(['module:spine']) — see store/spine/current.json observations",
|
||||
"seeds": ["module:spine"]
|
||||
},
|
||||
"expected_consumers": ["rune_tui (indirect, via future tranches)", "scripts/scaffold_projection.py (unchanged imports of rune.spine)", "future Memory/Scaffolding tranches"],
|
||||
"required_effects": [
|
||||
"structural_store.py contains no Rune path/projector constants (grep-gated)",
|
||||
"scaffold.py binds repo identity without secrets (schema-gated)",
|
||||
"contracts: legal-phase-map lifecycle, resumable across process restart",
|
||||
"scope check: red on out-of-scope changed file, green otherwise",
|
||||
"Spine: 68 tests pass; rebuild diff identical; 1402+ suite green"
|
||||
],
|
||||
"regression_obligations": [
|
||||
"full test suite >= 1382 baseline",
|
||||
"lint ratchet PASS",
|
||||
"pyright ratchet (CI arithmetic) PASS",
|
||||
"henxels exit 0",
|
||||
"spine rebuild produces no unexplained structural diff"
|
||||
],
|
||||
"structural_quality_obligations": [
|
||||
"no duplicate structural authority (scaffold references Spine by evidence URI, never rewrites)",
|
||||
"no orphaned code from extraction",
|
||||
"open-world kinds in scaffold (unknown entity/relation roundtrip)"
|
||||
],
|
||||
"verification_obligations": [
|
||||
"resumability proven across separate process boundary (subprocess test)",
|
||||
"toolchain neutrality: rust/typescript/go projects roundtrip in scaffold schema",
|
||||
"external internals do not leak into Rune Spine"
|
||||
],
|
||||
"migration_obligations": [],
|
||||
"rollback_path": "git revert of tranche commits; spine.py restore from pre-tranche SHA e3b459d; store/spine unchanged by refactor",
|
||||
"acceptance_criteria": [
|
||||
"all tests in §21 test plan green",
|
||||
"release path: CI green, deployed identity = tranche SHA",
|
||||
"fresh-session continuity: scaffold answers project/contract state without transcripts",
|
||||
"clean worktree at closure"
|
||||
],
|
||||
"non_goals": [
|
||||
"capability-fit discovery", "gap-decision automation", "toolchain-selection engine",
|
||||
"repo creation/bootstrap", "pattern ledger automation", "maintenance TUI",
|
||||
"migration automation", "self-upgrade", "Memory", "Substrate", "TUI changes"
|
||||
],
|
||||
"current_phase": "approved",
|
||||
"status": "open",
|
||||
"created": "2026-09-13",
|
||||
"evidence": ["chat-tranche-brief", "canonical-scaffolding-report"],
|
||||
"next_action": "Phase B: extract structural_store.py"
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"checkpoints": [
|
||||
{
|
||||
"at": "2026-09-13T02:16:19Z",
|
||||
"evidence": "store/scaffold/bootstrap_contract.json",
|
||||
"note": "bootstrap contract migrated into scaffold store",
|
||||
"phase": "migrated"
|
||||
},
|
||||
{
|
||||
"at": "2026-09-13T02:16:36Z",
|
||||
"evidence": "closure-record",
|
||||
"note": "predicted-vs-observed captured",
|
||||
"phase": "closed"
|
||||
},
|
||||
{
|
||||
"at": "2026-09-13T02:16:36Z",
|
||||
"evidence": "full suite + ruff + pyright (CI arithmetic) pending release CI",
|
||||
"note": "local gates green; ratchet skew documented (local 124/429 vs CI 117/443)",
|
||||
"phase": "verifying"
|
||||
},
|
||||
{
|
||||
"at": "2026-09-13T02:16:36Z",
|
||||
"evidence": "structural_store.py+scaffold.py implemented; 123 tranche tests green",
|
||||
"note": "Phase B/C/D complete",
|
||||
"phase": "building"
|
||||
}
|
||||
],
|
||||
"evidence_refs": [
|
||||
"contract-created:SCAFFOLD-TRANCHE-001"
|
||||
],
|
||||
"ext": {
|
||||
"contract_quality": {
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": "module:scaffold",
|
||||
"note": "missed by prediction \u2014 evidence required before store update",
|
||||
"status": "CANDIDATE"
|
||||
},
|
||||
{
|
||||
"entity": "module:structural_store",
|
||||
"note": "missed by prediction \u2014 evidence required before store update",
|
||||
"status": "CANDIDATE"
|
||||
},
|
||||
{
|
||||
"entity": "test:scaffold",
|
||||
"note": "missed by prediction \u2014 evidence required before store update",
|
||||
"status": "CANDIDATE"
|
||||
},
|
||||
{
|
||||
"entity": "test:structural_store",
|
||||
"note": "missed by prediction \u2014 evidence required before store update",
|
||||
"status": "CANDIDATE"
|
||||
}
|
||||
],
|
||||
"captured_at": "2026-09-13T02:16:36Z",
|
||||
"contract": "SCAFFOLD-TRANCHE-001",
|
||||
"missed": [
|
||||
"module:scaffold",
|
||||
"module:structural_store",
|
||||
"test:scaffold",
|
||||
"test:structural_store"
|
||||
],
|
||||
"needless": [
|
||||
"deployment:current",
|
||||
"subsystem:perception"
|
||||
],
|
||||
"observed": [
|
||||
"module:scaffold",
|
||||
"module:spine",
|
||||
"module:structural_store",
|
||||
"test:scaffold",
|
||||
"test:structural_store"
|
||||
],
|
||||
"predicted": [
|
||||
"deployment:current",
|
||||
"module:spine",
|
||||
"subsystem:perception"
|
||||
],
|
||||
"scope_amendments": [
|
||||
{
|
||||
"added": [
|
||||
"store/scaffold/",
|
||||
"store/spine/current.json"
|
||||
],
|
||||
"authorized_by": "tranche brief \u00a724 (runtime store files follow established repository policy); original RED retained in record",
|
||||
"reason": "runtime store artifacts produced by the tranche's own operation (bootstrap-contract store; spine observation refresh from real builds)"
|
||||
}
|
||||
],
|
||||
"scope_check": {
|
||||
"amendment": {
|
||||
"added": [
|
||||
"store/scaffold/",
|
||||
"store/spine/current.json"
|
||||
],
|
||||
"authorized_by": "tranche brief \u00a724 (runtime store files follow established repository policy); original RED retained in record",
|
||||
"reason": "runtime store artifacts produced by the tranche's own operation (bootstrap-contract store; spine observation refresh from real builds)"
|
||||
},
|
||||
"post_amendment": {
|
||||
"in_scope": [
|
||||
"src/rune/scaffold.py",
|
||||
"src/rune/spine.py",
|
||||
"src/rune/structural_store.py",
|
||||
"store/scaffold/",
|
||||
"store/spine/current.json",
|
||||
"tests/test_scaffold.py",
|
||||
"tests/test_structural_store.py"
|
||||
],
|
||||
"out_of_scope": [],
|
||||
"verdict": "green"
|
||||
},
|
||||
"post_release_note": "store/spine/current.json further updated post-close by construction-provenance attachment + module registration (same amendment class: runtime store artifacts)",
|
||||
"pre_amendment": {
|
||||
"in_scope": [
|
||||
"src/rune/scaffold.py",
|
||||
"src/rune/spine.py",
|
||||
"src/rune/structural_store.py",
|
||||
"tests/test_scaffold.py",
|
||||
"tests/test_structural_store.py"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"store/scaffold/",
|
||||
"store/spine/current.json"
|
||||
],
|
||||
"verdict": "red"
|
||||
}
|
||||
}
|
||||
},
|
||||
"predicted_frontier": {
|
||||
"change_kind": "implementation_change",
|
||||
"contextual_not_obligations": [],
|
||||
"contracts": [],
|
||||
"level0_direct": [
|
||||
"module:spine"
|
||||
],
|
||||
"level1_obligations": [
|
||||
"deployment:current",
|
||||
"subsystem:perception"
|
||||
],
|
||||
"level2_verification": [
|
||||
"deployment:current"
|
||||
],
|
||||
"max_level": 2,
|
||||
"traversal_scope": "spine-structural-only"
|
||||
},
|
||||
"release_evidence": {
|
||||
"all_jobs": "success",
|
||||
"auto_bump": "2731db0 (0.66.33)",
|
||||
"ci_runs": {
|
||||
"ci": 7298,
|
||||
"deploy": 7299,
|
||||
"release": 7297
|
||||
},
|
||||
"deployed_identity": "2731db03f4bd",
|
||||
"deployed_version": "0.66.33",
|
||||
"tranche_sha": "eb2f1a3"
|
||||
},
|
||||
"scope_amendments": [
|
||||
{
|
||||
"added": [
|
||||
"store/scaffold/",
|
||||
"store/spine/current.json"
|
||||
],
|
||||
"authorized_by": "tranche brief \u00a724 (runtime store files follow established repository policy); original RED retained in record",
|
||||
"reason": "runtime store artifacts produced by the tranche's own operation (bootstrap-contract store; spine observation refresh from real builds)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": "contract:SCAFFOLD-TRANCHE-001",
|
||||
"identities": {
|
||||
"acceptance": [
|
||||
"all tests in \u00a721 test plan green",
|
||||
"clean worktree at closure",
|
||||
"fresh-session continuity: scaffold answers project/contract state without transcripts",
|
||||
"release path: CI green, deployed identity = tranche SHA"
|
||||
],
|
||||
"affected_entities": [
|
||||
"module:scaffold",
|
||||
"module:spine",
|
||||
"module:structural_store"
|
||||
],
|
||||
"expected_consumers": [
|
||||
"future Memory/Scaffolding tranches",
|
||||
"rune_tui (indirect, via future tranches)",
|
||||
"scripts/scaffold_projection.py (unchanged imports of rune.spine)"
|
||||
],
|
||||
"gap_evidence": "no Project Scaffold or Construction Contract existed; spine.py was monolithic",
|
||||
"migration_obligations": [],
|
||||
"need": "Scaffolding needs its construction substrate: a shared generic structural-store primitive (extracted from spine.py without behavior change), a Project Scaffold layer over it, and a Construction Contract object durable enough that a fresh session recovers build state without transcripts.",
|
||||
"next_action": "observe real outcome; capture lessons",
|
||||
"non_goals": [
|
||||
"Memory",
|
||||
"Substrate",
|
||||
"TUI changes",
|
||||
"capability-fit discovery",
|
||||
"gap-decision automation",
|
||||
"maintenance TUI",
|
||||
"migration automation",
|
||||
"pattern ledger automation",
|
||||
"repo creation/bootstrap",
|
||||
"self-upgrade",
|
||||
"toolchain-selection engine"
|
||||
],
|
||||
"origin": "user-approved canonical Scaffolding architecture + Repository/Forge amendment (2026-09-13)",
|
||||
"regression_obligations": [
|
||||
"full test suite >= 1382 baseline",
|
||||
"henxels exit 0",
|
||||
"lint ratchet PASS",
|
||||
"pyright ratchet (CI arithmetic) PASS",
|
||||
"spine rebuild produces no unexplained structural diff"
|
||||
],
|
||||
"requirement": "construction substrate per canonical Scaffolding architecture",
|
||||
"research_refs": [
|
||||
"canonical-scaffolding-report",
|
||||
"repository-forge-amendment"
|
||||
],
|
||||
"resolution": "BUILD_NEW (shared core extracted, not copied)",
|
||||
"rollback_ref": "git revert; spine.py restore @ e3b459d",
|
||||
"scope": [
|
||||
"docs/SCAFFOLDING.md",
|
||||
"src/rune/scaffold.py",
|
||||
"src/rune/spine.py",
|
||||
"src/rune/structural_store.py",
|
||||
"store/scaffold/",
|
||||
"store/spine/current.json",
|
||||
"tests/test_scaffold.py",
|
||||
"tests/test_spine.py",
|
||||
"tests/test_structural_store.py"
|
||||
],
|
||||
"toolchain": {
|
||||
"language": "python",
|
||||
"rationale": "matches existing Rune repo toolchain"
|
||||
},
|
||||
"verification_obligations": [
|
||||
"external internals do not leak into Rune Spine",
|
||||
"resumability proven across separate process boundary (subprocess test)",
|
||||
"toolchain neutrality: rust/typescript/go projects roundtrip in scaffold schema"
|
||||
]
|
||||
},
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-09-13T02:16:36Z",
|
||||
"phase": "closed",
|
||||
"status": "closed",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"SCAFFOLD-TRANCHE-001"
|
||||
],
|
||||
"id": "module:scaffold",
|
||||
"identities": {
|
||||
"path": "src/rune/scaffold.py",
|
||||
"role": "Project Scaffold + Construction Contracts"
|
||||
},
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-09-13T02:16:19Z",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"SCAFFOLD-TRANCHE-001"
|
||||
],
|
||||
"id": "module:structural_store",
|
||||
"identities": {
|
||||
"path": "src/rune/structural_store.py",
|
||||
"role": "shared generic structural-store primitive"
|
||||
},
|
||||
"kind": "module",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-09-13T02:16:19Z",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"scaffold-init"
|
||||
],
|
||||
"id": "project:issue161-harness",
|
||||
"identities": {
|
||||
"purpose": "Rune runtime harness \u2014 the repo Rune itself is built in",
|
||||
"requirement": ""
|
||||
},
|
||||
"kind": "project",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-09-13T02:16:19Z",
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"generated_at": "2026-09-13T02:16:19Z",
|
||||
"observations": [
|
||||
{
|
||||
"authority": "",
|
||||
"detail": "17 boundary tests + 37 scaffold tests exercise the new module",
|
||||
"entity": "module:structural_store",
|
||||
"kind": "observed_consequence",
|
||||
"observed_at": "2026-09-13T02:16:36Z",
|
||||
"source": "test-run",
|
||||
"task": "SCAFFOLD-TRANCHE-001"
|
||||
},
|
||||
{
|
||||
"authority": "",
|
||||
"detail": "Project Scaffold + contracts + repo binding + CLI exercised",
|
||||
"entity": "module:scaffold",
|
||||
"kind": "observed_consequence",
|
||||
"observed_at": "2026-09-13T02:16:36Z",
|
||||
"source": "test-run",
|
||||
"task": "SCAFFOLD-TRANCHE-001"
|
||||
},
|
||||
{
|
||||
"authority": "",
|
||||
"detail": "spine.py refactored to import shared core; Rune projectors retained",
|
||||
"entity": "module:spine",
|
||||
"kind": "observed_consequence",
|
||||
"observed_at": "2026-09-13T02:16:36Z",
|
||||
"source": "git-diff",
|
||||
"task": "SCAFFOLD-TRANCHE-001"
|
||||
},
|
||||
{
|
||||
"authority": "",
|
||||
"detail": "tests/test_scaffold.py added (scaffold/contract/repo gate)",
|
||||
"entity": "test:scaffold",
|
||||
"kind": "observed_consequence",
|
||||
"observed_at": "2026-09-13T02:16:36Z",
|
||||
"source": "file-added",
|
||||
"task": "SCAFFOLD-TRANCHE-001"
|
||||
},
|
||||
{
|
||||
"authority": "",
|
||||
"detail": "tests/test_structural_store.py added (boundary gate)",
|
||||
"entity": "test:structural_store",
|
||||
"kind": "observed_consequence",
|
||||
"observed_at": "2026-09-13T02:16:36Z",
|
||||
"source": "file-added",
|
||||
"task": "SCAFFOLD-TRANCHE-001"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"evidence_refs": [
|
||||
"SCAFFOLD-TRANCHE-001"
|
||||
],
|
||||
"from": "module:scaffold",
|
||||
"observed_at": "2026-09-13T02:16:19Z",
|
||||
"to": "module:structural_store",
|
||||
"type": "imports"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"SCAFFOLD-TRANCHE-001"
|
||||
],
|
||||
"from": "module:spine",
|
||||
"observed_at": "2026-09-13T02:16:19Z",
|
||||
"to": "module:structural_store",
|
||||
"type": "imports"
|
||||
}
|
||||
],
|
||||
"schema_version": 1
|
||||
}
|
||||
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
@@ -0,0 +1,178 @@
|
||||
"""test_deploy_qualify.py — deploy qualification verdict tests.
|
||||
|
||||
Fixtures modeled on the REAL red-promotion incidents (24413e4, fd6e9b4,
|
||||
de30cd9 — 0.66.28/29 promoted with failing type-check because the
|
||||
required-checks aggregator published success unconditionally). The
|
||||
qualifier must fail closed against every shape seen live.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
|
||||
SHA = "a" * 40
|
||||
OTHER_SHA = "b" * 40
|
||||
|
||||
|
||||
def st(context, status, created, description=""):
|
||||
return {"context": context, "status": status, "created_at": created,
|
||||
"description": description}
|
||||
|
||||
|
||||
def run(path, sha, status, created, run_id, conclusions=None):
|
||||
return {"path": path, "head_sha": sha, "status": status, "created_at": created,
|
||||
"id": run_id, "_conclusions": conclusions or []}
|
||||
|
||||
|
||||
RC = "CI Pipeline / required-checks"
|
||||
|
||||
|
||||
def jobs_fetch_for(runs):
|
||||
def fetch(run_id):
|
||||
for r in runs:
|
||||
if r.get("id") == run_id:
|
||||
return [{"name": f"job{i}", "conclusion": c}
|
||||
for i, c in enumerate(r["_conclusions"])]
|
||||
return None
|
||||
return fetch
|
||||
|
||||
|
||||
# ── leg 1: required-checks context ────────────────────────────────────────
|
||||
|
||||
class TestRequiredCheckVerdict:
|
||||
def test_failed_required_check_is_failure(self):
|
||||
v = dq.required_check_verdict([st(RC + " (push)", "failure", "T2")])
|
||||
assert v == "failure"
|
||||
|
||||
def test_pending_required_check_is_pending(self):
|
||||
v = dq.required_check_verdict([st(RC + " (push)", "pending", "T2")])
|
||||
assert v == "pending"
|
||||
|
||||
def test_missing_required_check_is_pending(self):
|
||||
assert dq.required_check_verdict([]) == "pending"
|
||||
assert dq.required_check_verdict(
|
||||
[st("CI Pipeline / lint (push)", "success", "T1")]) == "pending"
|
||||
|
||||
def test_stale_success_newer_failure_is_failure(self):
|
||||
v = dq.required_check_verdict([
|
||||
st(RC + " (push)", "success", "T1"),
|
||||
st(RC + " (push)", "failure", "T2"),
|
||||
])
|
||||
assert v == "failure"
|
||||
|
||||
def test_newer_success_after_older_failure_is_success(self):
|
||||
v = dq.required_check_verdict([
|
||||
st(RC + " (push)", "failure", "T1"),
|
||||
st(RC + " (push)", "success", "T2"),
|
||||
])
|
||||
assert v == "success"
|
||||
|
||||
def test_all_skipped_is_pending_not_success(self):
|
||||
v = dq.required_check_verdict([st(RC + " (push)", "skipped", "T1")])
|
||||
assert v == "pending"
|
||||
|
||||
|
||||
# ── leg 2: independent jobs-level verification ────────────────────────────
|
||||
|
||||
class TestIndependentJobsVerdict:
|
||||
def test_all_jobs_success_is_success(self):
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T2", 1,
|
||||
["success", "success", "success"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "success"
|
||||
|
||||
def test_any_job_failure_is_failure(self):
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T2", 1,
|
||||
["success", "failure", "success"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "failure"
|
||||
|
||||
def test_in_progress_run_is_pending(self):
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "in_progress", "T2", 1,
|
||||
["success"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "pending"
|
||||
|
||||
def test_no_run_for_this_sha_is_pending(self):
|
||||
runs = [run("ci.yml@refs/heads/master", OTHER_SHA, "completed", "T2", 1,
|
||||
["success"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "pending"
|
||||
|
||||
def test_source_and_successor_sha_distinct(self):
|
||||
"""A green run on the successor SHA must not qualify the red source
|
||||
SHA (and vice versa) — the exact candidate governs."""
|
||||
runs = [run("ci.yml@refs/heads/master", OTHER_SHA, "completed", "T1", 1,
|
||||
["success", "success"]),
|
||||
run("ci.yml@refs/heads/master", SHA, "completed", "T2", 2,
|
||||
["success", "failure"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "failure"
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=OTHER_SHA) == "success"
|
||||
|
||||
def test_tag_fanout_does_not_poison_verdict(self):
|
||||
"""A failing tag-run must not fail a green master run, and a failing
|
||||
master run must not be rescued by a green tag run."""
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
|
||||
["success", "success"]),
|
||||
run("ci.yml@refs/tags/v9.9.9", SHA, "completed", "T2", 2,
|
||||
["failure"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "success"
|
||||
runs2 = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
|
||||
["failure"]),
|
||||
run("ci.yml@refs/tags/v9.9.9", SHA, "completed", "T2", 2,
|
||||
["success", "success"])]
|
||||
assert dq.independent_jobs_verdict(runs2, jobs_fetch_for(runs2), sha=SHA) == "failure"
|
||||
|
||||
def test_stale_success_newer_completed_failure_wins(self):
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
|
||||
["success", "success"]),
|
||||
run("ci.yml@refs/heads/master", SHA, "completed", "T2", 2,
|
||||
["success", "failure"])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "failure"
|
||||
|
||||
def test_jobs_probe_failure_is_pending(self):
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 99)]
|
||||
assert dq.independent_jobs_verdict(runs, lambda rid: None, sha=SHA) == "pending"
|
||||
|
||||
def test_empty_jobs_visible_is_pending(self):
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1, [])]
|
||||
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "pending"
|
||||
|
||||
|
||||
# ── combined verdict: fail-closed conjunction ─────────────────────────────
|
||||
|
||||
class TestCombinedQualify:
|
||||
def test_the_real_incident_shape_is_refused(self):
|
||||
"""THE 24413e4 shape: type-check + unit-tests failed, yet the
|
||||
aggregator published success. Even with the lying aggregator leg,
|
||||
the independent jobs leg must refuse."""
|
||||
statuses = [
|
||||
st(RC + " (push)", "success", "2026-09-12T19:13:53"), # the lie
|
||||
st("CI Pipeline / unit-tests (push)", "failure", "2026-09-12T19:13:51"),
|
||||
st("CI Pipeline / type-check (push)", "failure", "2026-09-12T19:13:34"),
|
||||
]
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
|
||||
["success", "failure", "failure", "success"])]
|
||||
v = dq.qualify(statuses, runs, jobs_fetch_for(runs), sha=SHA)
|
||||
assert v == "failure", "red candidate must never qualify"
|
||||
|
||||
def test_pending_aggregator_with_green_jobs_still_pending(self):
|
||||
statuses = [st(RC + " (push)", "pending", "T2")]
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
|
||||
["success", "success"])]
|
||||
assert dq.qualify(statuses, runs, jobs_fetch_for(runs), sha=SHA) == "pending"
|
||||
|
||||
def test_both_green_qualifies(self):
|
||||
statuses = [st(RC + " (push)", "success", "T2")]
|
||||
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
|
||||
["success", "success"])]
|
||||
assert dq.qualify(statuses, runs, jobs_fetch_for(runs), sha=SHA) == "success"
|
||||
|
||||
def test_missing_everything_is_pending(self):
|
||||
assert dq.qualify([], [], jobs_fetch_for([]), sha=SHA) == "pending"
|
||||
|
||||
def test_combine_is_fail_closed(self):
|
||||
assert dq.combine("success", "failure") == "failure"
|
||||
assert dq.combine("failure", "pending") == "failure"
|
||||
assert dq.combine("success", "pending") == "pending"
|
||||
assert dq.combine("success", "success") == "success"
|
||||
@@ -0,0 +1,565 @@
|
||||
"""test_scaffold.py — SCAFFOLD-TRANCHE-001: Project Scaffold + Construction Contract.
|
||||
|
||||
Covers (per tranche test plan):
|
||||
B. PROJECT SCAFFOLD: init/save/load, open-world kinds, domains/components,
|
||||
repo binding + no-secret serialization, drift reconciliation, PRD ref,
|
||||
next-action state.
|
||||
C. CONSTRUCTION CONTRACT: creation, approval, legal/illegal transitions,
|
||||
cross-process restart/resume, scope checker green/red, predicted
|
||||
frontier persisted, observed effects captured, closure diff.
|
||||
D. AUTHORITY BOUNDARIES: external internals stay out of Rune Spine;
|
||||
contract evidence references Spine without becoming structural truth.
|
||||
E. TOOLCHAIN DATA: multiple language/runtime combinations roundtrip.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import scaffold, spine, structural_store # noqa: E402
|
||||
|
||||
Scaffold = scaffold.Scaffold
|
||||
IllegalPhaseTransition = scaffold.IllegalPhaseTransition
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _scaffold(tmp_path, project_id="demo"):
|
||||
return Scaffold(tmp_path / ".rune" / "scaffold", project_id)
|
||||
|
||||
|
||||
def _init_doc(tmp_path, project_id="demo", purpose="test project"):
|
||||
s = _scaffold(tmp_path, project_id)
|
||||
doc = s.init(purpose=purpose)
|
||||
return s, doc
|
||||
|
||||
|
||||
def _entity(doc, eid):
|
||||
return structural_store.entity(doc, eid)
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# B. PROJECT SCAFFOLD
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class TestProjectScaffoldBasics:
|
||||
def test_initialize_records_identity(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path, project_id="garden-app", purpose="a small tool")
|
||||
p = _entity(doc, "project:garden-app")
|
||||
assert p is not None
|
||||
assert p["kind"] == "project"
|
||||
assert p["identities"]["purpose"] == "a small tool"
|
||||
|
||||
def test_save_load_roundtrip(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.set_next_action(doc, "build the parser")
|
||||
s.save(doc)
|
||||
# fresh instance = fresh runtime (cross-instance, same-process here;
|
||||
# cross-process proof is its own test below)
|
||||
s2 = _scaffold(tmp_path)
|
||||
doc2 = s2.load()
|
||||
p = _entity(doc2, "project:demo")
|
||||
na = _entity(doc2, "next_action:demo")
|
||||
assert p is not None
|
||||
assert na["identities"]["action"] == "build the parser"
|
||||
|
||||
def test_project_scaffold_never_touches_rune_store(self, tmp_path):
|
||||
"""A project scaffold is rooted at its own store dir; it must not
|
||||
read or write Rune's spine store."""
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.save(doc)
|
||||
files = sorted(p.name for p in (tmp_path / ".rune" / "scaffold").iterdir())
|
||||
assert "current.json" in files
|
||||
assert all("spine" not in f for f in files)
|
||||
|
||||
|
||||
class TestOpenWorld:
|
||||
def test_unknown_entity_kind_roundtrips(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
structural_store.add_entity(doc, id="q1", kind="quantum_fluctuation", identities={"will": "uncertain"})
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
e = _entity(doc2, "q1")
|
||||
assert e["kind"] == "quantum_fluctuation"
|
||||
assert e["identities"]["will"] == "uncertain"
|
||||
|
||||
def test_unknown_relationship_kind_roundtrips(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
structural_store.add_entity(doc, id="q1", kind="x", identities={})
|
||||
structural_store.add_entity(doc, id="q2", kind="x", identities={})
|
||||
s.link(doc, type="entangles", from_id="q1", to_id="q2")
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
rels = structural_store.relationships(doc2, from_id="q1")
|
||||
assert len(rels) == 1
|
||||
assert rels[0]["type"] == "entangles"
|
||||
|
||||
def test_domains_components_contracts_roundtrip(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.add_domain(doc, "storage", description="persistence layer")
|
||||
s.add_component(doc, "store", path="src/store.rs")
|
||||
s.add_contract(doc, "fs-api", spec_ref="docs/fs-api.md")
|
||||
s.link(doc, type="contains", from_id="domain:storage", to_id="component:store")
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
assert _entity(doc2, "domain:storage")["identities"]["description"] == "persistence layer"
|
||||
assert _entity(doc2, "component:store")["identities"]["path"] == "src/store.rs"
|
||||
assert _entity(doc2, "contract:fs-api")["identities"]["spec"] == "docs/fs-api.md"
|
||||
rels = structural_store.relationships(doc2, from_id="domain:storage")
|
||||
assert rels[0]["type"] == "contains"
|
||||
|
||||
|
||||
class TestRepoBinding:
|
||||
def test_bind_repo_roundtrip(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.bind_repo(
|
||||
doc,
|
||||
forge="gitea",
|
||||
owner="nick",
|
||||
name="garden-app",
|
||||
default_branch="main",
|
||||
repo_root="/home/nick/garden",
|
||||
placement="NEW_MANAGED_REPO",
|
||||
rationale=["independent versioning", "independent deployment"],
|
||||
)
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
r = _entity(doc2, "repo:nick/garden-app")
|
||||
assert r["kind"] == "repo_identity"
|
||||
ident = r["identities"]
|
||||
assert ident["forge"] == "gitea"
|
||||
assert ident["full_name"] == "nick/garden-app"
|
||||
assert ident["placement"] == "NEW_MANAGED_REPO"
|
||||
assert "independent versioning" in ident["rationale"]
|
||||
|
||||
def test_bind_repo_rejects_unknown_placement(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
with pytest.raises(ValueError):
|
||||
s.bind_repo(
|
||||
doc, forge="gitea", owner="o", name="r", default_branch="main", repo_root="/x", placement="WHENEVER"
|
||||
)
|
||||
|
||||
def test_no_secret_serialization(self, tmp_path):
|
||||
"""Secret-named fields are rejected at the API surface (bind_repo
|
||||
has a fixed schema — unknown/secret kwargs raise), and a secret
|
||||
value smuggled into observations is stripped before disk."""
|
||||
s, doc = _init_doc(tmp_path)
|
||||
with pytest.raises(TypeError): # no **kwargs passthrough at all
|
||||
s.bind_repo(doc, forge="gitea", owner="o", name="r", default_branch="main", repo_root="/x", token="hunter2")
|
||||
s.bind_repo(doc, forge="gitea", owner="o", name="r", default_branch="main", repo_root="/x")
|
||||
s.observe_repo(doc, "o", "r", head="abc123", clean=True, last_verification="green")
|
||||
# attempt to smuggle a secret into observations
|
||||
doc["observations"].append({"kind": "repo_observed", "api_token": "hunter2"})
|
||||
s.save(doc)
|
||||
raw = (Path(s.current)).read_text(encoding="utf-8")
|
||||
assert "hunter2" not in raw
|
||||
|
||||
def test_observe_repo_requires_binding_first(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
with pytest.raises(ValueError):
|
||||
s.observe_repo(doc, "ghost", "repo", head="abc")
|
||||
|
||||
def test_repo_state_from_git(self, tmp_path):
|
||||
"""Deterministic git observation on a real local fixture repo."""
|
||||
repo = tmp_path / "fixture"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q", "-b", "main", str(repo)], check=True)
|
||||
subprocess.run(["git", "-C", str(repo), "config", "user.email", "t@t"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo), "config", "user.name", "t"], check=True)
|
||||
(repo / "f.txt").write_text("x", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(repo), "add", "-A"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo), "commit", "-qm", "init"], check=True)
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.bind_repo(doc, forge="gitea", owner="o", name="fixture", default_branch="main", repo_root=str(repo))
|
||||
s.repo_state_from_git(doc, "o", "fixture", str(repo))
|
||||
r = _entity(doc, "repo:o/fixture")
|
||||
obs = r["ext"]["observed_state"]
|
||||
assert len(obs["head"]) == 12
|
||||
assert obs["clean"] is True
|
||||
|
||||
|
||||
class TestDrift:
|
||||
def test_reconcile_marks_stale_authority_and_keeps_reality(self, tmp_path):
|
||||
"""A projector-owned relationship whose authority fails goes STALE
|
||||
(not silently fresh, not force-reverted); reconcile keeps the
|
||||
staleness coherent across restart."""
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.add_component(doc, "store", path="src/store.rs")
|
||||
structural_store.add_entity(doc, id="capability:persistence", kind="capability", identities={})
|
||||
s.link(doc, type="serves", from_id="component:store", to_id="capability:persistence", evidence_refs=["e1"])
|
||||
# make the relationship authority-owned via the real mechanism
|
||||
rel = structural_store.relationships(doc, from_id="component:store")[0]
|
||||
structural_store._authority_observed(doc, rel, "repo-observer")
|
||||
s.save(doc)
|
||||
# external reality changed: authority fails to re-observe
|
||||
structural_store.authority_failed(doc, "repo-observer", "component path no longer exists")
|
||||
assert rel["status"] == "STALE" # failed authority → stale, not fresh
|
||||
e = _entity(doc, "component:store")
|
||||
assert e["identities"]["path"] == "src/store.rs" # reality untouched
|
||||
s.save(doc)
|
||||
rel2 = structural_store.relationships(_scaffold(tmp_path).load(), from_id="component:store")[0]
|
||||
assert rel2["status"] == "STALE" # staleness persists across restart
|
||||
obs = [o for o in doc["observations"] if o["kind"] == "authority_degraded"]
|
||||
assert len(obs) == 1
|
||||
|
||||
def test_maintenance_item_roundtrip(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.add_maintenance_item(
|
||||
doc,
|
||||
"dep-bump",
|
||||
issue="outdated dep",
|
||||
evidence="cargo audit",
|
||||
benefit="security",
|
||||
approval="auto",
|
||||
priority="low",
|
||||
)
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
m = _entity(doc2, "maintenance:dep-bump")
|
||||
assert m["kind"] == "maintenance_item"
|
||||
assert m["identities"]["status"] == "open"
|
||||
|
||||
|
||||
class TestPRDAndNextAction:
|
||||
def test_prd_reference_and_next_action_roundtrip(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.set_prd(doc, "docs/PRD.md@r7")
|
||||
s.set_next_action(doc, "implement file watcher")
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
assert _entity(doc2, "prd:demo")["identities"]["reference"] == "docs/PRD.md@r7"
|
||||
assert _entity(doc2, "next_action:demo")["identities"]["action"] == "implement file watcher"
|
||||
|
||||
|
||||
class TestToolchainDataNeutrality:
|
||||
@pytest.mark.parametrize(
|
||||
"toolchain",
|
||||
[
|
||||
{"language": "rust", "runtime": "tokio", "build_system": "cargo"},
|
||||
{"language": "typescript", "runtime": "node20", "framework": "svelte", "build_system": "vite"},
|
||||
{"language": "go", "build_system": "go-build"},
|
||||
{"language": "python", "runtime": "cpython312", "major_libraries": ["pydantic"]},
|
||||
{"language": "c++", "build_system": "cmake"},
|
||||
],
|
||||
)
|
||||
def test_toolchains_roundtrip(self, tmp_path, toolchain):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.new_contract(doc, contract_id="tc", need="demo", toolchain=toolchain)
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
assert _entity(doc2, "contract:tc")["identities"]["toolchain"] == toolchain
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# C. CONSTRUCTION CONTRACT
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
def _make_contract(tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.new_contract(
|
||||
doc,
|
||||
contract_id="C1",
|
||||
need="add export feature",
|
||||
origin="user",
|
||||
scope=["src/export", "tests/export"],
|
||||
resolution="BUILD_NEW",
|
||||
toolchain={"language": "python"},
|
||||
affected_entities=["component:exporter"],
|
||||
expected_consumers=["component:cli"],
|
||||
regression_obligations=["existing cli tests green"],
|
||||
verification_obligations=["unit + integration"],
|
||||
rollback_ref="git revert of contract commit",
|
||||
acceptance=["export produces valid csv"],
|
||||
non_goals=["no pdf export"],
|
||||
)
|
||||
return s, doc
|
||||
|
||||
|
||||
class TestContractLifecycle:
|
||||
def test_creation_defaults_to_draft_open(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
c = s.contract(doc, "C1")
|
||||
assert c["phase"] == "draft"
|
||||
assert c["status"] == "open"
|
||||
assert c["identities"]["need"] == "add export feature"
|
||||
assert c["identities"]["non_goals"] == ["no pdf export"]
|
||||
|
||||
def test_duplicate_contract_rejected(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
with pytest.raises(ValueError):
|
||||
s.new_contract(doc, contract_id="C1", need="again")
|
||||
|
||||
def test_legal_transition_path(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
s.transition_contract(doc, "C1", "approved")
|
||||
s.transition_contract(doc, "C1", "building")
|
||||
s.transition_contract(doc, "C1", "verifying")
|
||||
s.transition_contract(doc, "C1", "closed")
|
||||
c = s.contract(doc, "C1")
|
||||
assert c["phase"] == "closed"
|
||||
assert c["status"] == "closed"
|
||||
assert c["identities"]["next_action"] == "observe real outcome; capture lessons"
|
||||
|
||||
def test_illegal_transition_rejected(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
with pytest.raises(IllegalPhaseTransition):
|
||||
s.transition_contract(doc, "C1", "verifying") # draft → verifying
|
||||
# state unchanged after illegal attempt
|
||||
assert s.contract(doc, "C1")["phase"] == "draft"
|
||||
|
||||
def test_closed_contract_is_terminal(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
for p in ("approved", "building", "verifying", "closed"):
|
||||
s.transition_contract(doc, "C1", p)
|
||||
with pytest.raises(IllegalPhaseTransition):
|
||||
s.transition_contract(doc, "C1", "building")
|
||||
|
||||
def test_failed_contract_can_retry(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
s.transition_contract(doc, "C1", "approved")
|
||||
s.transition_contract(doc, "C1", "building")
|
||||
s.transition_contract(doc, "C1", "failed")
|
||||
c = s.contract(doc, "C1")
|
||||
assert c["status"] == "failed"
|
||||
s.transition_contract(doc, "C1", "building") # retry path
|
||||
assert s.contract(doc, "C1")["phase"] == "building"
|
||||
|
||||
def test_checkpoints_recorded(self, tmp_path):
|
||||
s, doc = _make_contract(tmp_path)
|
||||
s.transition_contract(
|
||||
doc, "C1", "approved", evidence_ref="user-approval-2026-09-13", note="approved in session"
|
||||
)
|
||||
c = s.contract(doc, "C1")
|
||||
assert c["checkpoints"][-1]["phase"] == "approved"
|
||||
assert c["checkpoints"][-1]["evidence"] == "user-approval-2026-09-13"
|
||||
|
||||
def test_cross_process_resume(self, tmp_path):
|
||||
"""RESUMABILITY PROOF: contract advances in one process; a DIFFERENT
|
||||
process (real subprocess, fresh interpreter) loads the store and
|
||||
observes the same phase + recoverable next action. This is not
|
||||
serialize-then-deserialize-in-the-same-function."""
|
||||
s, doc = _make_contract(tmp_path)
|
||||
s.transition_contract(doc, "C1", "approved")
|
||||
s.transition_contract(doc, "C1", "building")
|
||||
s.save(doc)
|
||||
script = (
|
||||
"import sys, json;"
|
||||
"sys.path.insert(0, %r);"
|
||||
"from rune import scaffold;"
|
||||
"s = scaffold.Scaffold(%r, 'demo');"
|
||||
"doc = s.load();"
|
||||
"c = s.contract(doc, 'C1');"
|
||||
"print(json.dumps({'phase': c['phase'],"
|
||||
"'status': c['status'],"
|
||||
"'next_action': c['identities']['next_action'],"
|
||||
"'need': c['identities']['need']}))"
|
||||
) % (os.path.join(os.path.dirname(__file__), "..", "src"), str(tmp_path / ".rune" / "scaffold"))
|
||||
out = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60)
|
||||
assert out.returncode == 0, out.stderr
|
||||
recovered = json.loads(out.stdout.strip())
|
||||
assert recovered["phase"] == "building"
|
||||
assert recovered["status"] == "open"
|
||||
assert recovered["next_action"] == "complete implementation, then verify"
|
||||
assert recovered["need"] == "add export feature"
|
||||
|
||||
|
||||
class TestScopeCheck:
|
||||
def _scoped(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.new_contract(doc, contract_id="S1", need="export feature", scope=["src/export", "tests/export"])
|
||||
s.transition_contract(doc, "S1", "approved")
|
||||
return s, doc
|
||||
|
||||
def test_scope_green(self, tmp_path):
|
||||
s, doc = self._scoped(tmp_path)
|
||||
v = s.check_scope(doc, "S1", ["src/export/writer.py", "tests/export/test_w.py"])
|
||||
assert v["verdict"] == "green"
|
||||
assert v["out_of_scope"] == []
|
||||
|
||||
def test_unscoped_contract_is_unknown_not_green(self, tmp_path):
|
||||
"""Silent-green for an unscoped contract would be the exact
|
||||
silent-scope-expansion failure mode this gate prevents."""
|
||||
s, doc = _init_doc(tmp_path)
|
||||
s.new_contract(doc, contract_id="U1", need="vague work") # no scope
|
||||
v = s.check_scope(doc, "U1", ["src/anything.py"])
|
||||
assert v["verdict"] == "unknown"
|
||||
assert v["out_of_scope"] == ["src/anything.py"]
|
||||
|
||||
def test_scope_red(self, tmp_path):
|
||||
s, doc = self._scoped(tmp_path)
|
||||
v = s.check_scope(doc, "S1", ["src/export/writer.py", "src/spine.py"])
|
||||
assert v["verdict"] == "red"
|
||||
assert v["out_of_scope"] == ["src/spine.py"]
|
||||
|
||||
def test_scope_red_through_close(self, tmp_path):
|
||||
"""An out-of-scope change must surface as RED in closure evidence,
|
||||
not be silently absorbed."""
|
||||
s, doc = self._scoped(tmp_path)
|
||||
s.transition_contract(doc, "S1", "building")
|
||||
s.transition_contract(doc, "S1", "verifying")
|
||||
s.close_contract(doc, "S1", scope_files=["src/export/writer.py", "src/unrelated.py"])
|
||||
q = s.contract(doc, "S1")["ext"]["contract_quality"]
|
||||
assert q["scope_check"]["verdict"] == "red"
|
||||
assert q["scope_check"]["out_of_scope"] == ["src/unrelated.py"]
|
||||
|
||||
|
||||
class TestPredictedVsObserved:
|
||||
def _with_frontier(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
structural_store.add_entity(doc, id="component:exporter", kind="component", identities={})
|
||||
structural_store.add_entity(doc, id="component:cli", kind="component", identities={})
|
||||
structural_store.add_entity(doc, id="contract:csv-format", kind="contract", identities={})
|
||||
s.link(doc, type="consumes", from_id="component:exporter", to_id="contract:csv-format")
|
||||
s.new_contract(
|
||||
doc, contract_id="P1", need="exporter", affected_entities=["component:exporter"], scope=["src/export"]
|
||||
)
|
||||
s.transition_contract(doc, "P1", "approved")
|
||||
s.snapshot_predicted_frontier(doc, "P1", seed_entity_ids=["component:exporter"])
|
||||
return s, doc
|
||||
|
||||
def test_predicted_frontier_persisted_at_approval(self, tmp_path):
|
||||
s, doc = self._with_frontier(tmp_path)
|
||||
pred = s.contract(doc, "P1")["ext"]["predicted_frontier"]
|
||||
assert "component:exporter" in pred["level0_direct"]
|
||||
assert "contract:csv-format" in pred["contracts"]
|
||||
|
||||
def test_closure_captures_observed_and_diff(self, tmp_path):
|
||||
s, doc = self._with_frontier(tmp_path)
|
||||
s.transition_contract(doc, "P1", "building")
|
||||
s.transition_contract(doc, "P1", "verifying")
|
||||
# independent observed evidence: predicted one + one MISSED entity
|
||||
s.close_contract(
|
||||
doc,
|
||||
"P1",
|
||||
observed_evidence=[
|
||||
{"entity": "component:exporter", "source": "test-run", "detail": "exporter exercised by new tests"},
|
||||
{
|
||||
"entity": "component:unplanned_helper",
|
||||
"source": "git-diff",
|
||||
"detail": "file added outside prediction",
|
||||
},
|
||||
],
|
||||
)
|
||||
q = s.contract(doc, "P1")["ext"]["contract_quality"]
|
||||
assert "component:exporter" in q["observed"]
|
||||
assert "component:unplanned_helper" in q["missed"]
|
||||
assert q["missed"][0] == "component:unplanned_helper"
|
||||
assert q["candidate_corrections"][0]["status"] == "CANDIDATE"
|
||||
# evidence items recorded as observations, not auto-promoted structure
|
||||
obs = [o for o in doc["observations"] if o["kind"] == "observed_consequence"]
|
||||
assert len(obs) == 2
|
||||
assert _entity(doc, "component:unplanned_helper") is None
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# D. AUTHORITY BOUNDARIES
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class TestAuthorityBoundaries:
|
||||
def test_spine_bridge_contains_no_project_internals(self, tmp_path):
|
||||
"""The bridge Rune Spine should hold carries identity + repo surface
|
||||
ONLY — no components, domains, routes, or contracts leak."""
|
||||
s, doc = _init_doc(tmp_path, project_id="garden")
|
||||
s.add_domain(doc, "internal-domain")
|
||||
s.add_component(doc, "internal-comp", path="src/secret.py")
|
||||
s.add_contract(doc, "internal-route", spec_ref="x")
|
||||
s.bind_repo(
|
||||
doc, forge="gitea", owner="nick", name="garden", default_branch="main", repo_root="/home/nick/garden"
|
||||
)
|
||||
s.save(doc)
|
||||
bridge = s.spine_bridge_entity()
|
||||
assert bridge["project"] == "project:garden"
|
||||
assert bridge["repo_full_name"] == "nick/garden"
|
||||
blob = json.dumps(bridge)
|
||||
for leaked in (
|
||||
"internal-domain",
|
||||
"internal-comp",
|
||||
"internal-route",
|
||||
"secret.py",
|
||||
"domain:",
|
||||
"component:",
|
||||
"contract:",
|
||||
):
|
||||
assert leaked not in blob, f"leaked: {leaked}"
|
||||
|
||||
def test_project_internals_do_not_enter_rune_spine_graph(self, tmp_path):
|
||||
"""Stronger form: build a real Rune Spine doc + a real project
|
||||
scaffold in tmp stores; prove the project's internals exist only in
|
||||
the scaffold store. Spine holds (at most) the bridge facts."""
|
||||
spine_doc = spine.new_doc()
|
||||
s, doc = _init_doc(tmp_path, project_id="garden")
|
||||
s.add_component(doc, "secret-widget", path="src/widget.rs")
|
||||
s.bind_repo(doc, forge="gitea", owner="nick", name="garden", default_branch="main", repo_root="/tmp/garden")
|
||||
s.save(doc)
|
||||
bridge = s.spine_bridge_entity()
|
||||
# attach ONLY bridge facts (identity + repo surface) as the runtime would
|
||||
spine.add_entity(
|
||||
spine_doc,
|
||||
id=bridge["project"],
|
||||
kind="project",
|
||||
identities={"purpose": bridge["purpose"], "repo": bridge.get("repo_full_name", "")},
|
||||
evidence_refs=["scaffold-bridge"],
|
||||
)
|
||||
spine_ids = {e["id"] for e in spine_doc["entities"]}
|
||||
assert "project:garden" in spine_ids
|
||||
assert "component:secret-widget" not in spine_ids
|
||||
assert "repo:nick/garden" not in spine_ids # repo detail is scaffold's
|
||||
|
||||
def test_contract_evidence_references_spine_without_becoming_truth(self, tmp_path):
|
||||
"""A contract may cite spine:// URIs as evidence; that citation must
|
||||
NOT create or mutate the referenced Spine entity."""
|
||||
s, doc = _init_doc(tmp_path)
|
||||
spine_doc = spine.new_doc()
|
||||
spine.add_entity(spine_doc, id="capability:test", kind="capability", identities={}, evidence_refs=["test"])
|
||||
before = json.dumps(spine_doc, sort_keys=True)
|
||||
s.new_contract(
|
||||
doc, contract_id="EV1", need="extend capability", research_refs=[spine.spine_uri("capability:test")]
|
||||
)
|
||||
s.save(doc)
|
||||
after = json.dumps(spine_doc, sort_keys=True)
|
||||
assert before == after # citation is inert on the referenced authority
|
||||
c = s.contract(doc, "EV1")
|
||||
assert c["identities"]["research_refs"] == [spine.spine_uri("capability:test")]
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# A. SHARED STRUCTURAL CORE (scaffold-side regression of key semantics)
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class TestSharedCoreSemanticsViaScaffold:
|
||||
def test_alias_supersession_works_in_scaffold_store(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
structural_store.add_entity(doc, id="old-name", kind="component", identities={})
|
||||
structural_store.add_entity(doc, id="new-name", kind="component", identities={})
|
||||
structural_store.alias(doc, "new-name", "old-name")
|
||||
structural_store.supersede(doc, "new-name", "old-name", "evidence")
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
e_old = _entity(doc2, "old-name")
|
||||
assert e_old["status"] == "SUPERSEDED"
|
||||
e_new = _entity(doc2, "new-name")
|
||||
assert structural_store.resolve_status(e_new) != "SUPERSEDED"
|
||||
assert e_new["superseded_by"] if False else True # new entity has no superseded_by
|
||||
assert "superseded_by" not in e_new
|
||||
|
||||
def test_maturity_promotion_works_in_scaffold_store(self, tmp_path):
|
||||
s, doc = _init_doc(tmp_path)
|
||||
structural_store.add_entity(doc, id="component:thing", kind="component", identities={}, maturity="DECLARED")
|
||||
structural_store.promote(doc, "component:thing", "WIRED", "tests pass")
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
assert _entity(doc2, "component:thing")["maturity"] == "WIRED"
|
||||
@@ -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,192 @@
|
||||
"""test_structural_store.py — SCAFFOLD-TRANCHE-001 boundary tests.
|
||||
|
||||
Proves the shared structural-store primitive contains no Rune-specific
|
||||
assumptions and behaves identically to the pre-extraction spine engine.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
|
||||
class TestNoRuneConstants:
|
||||
"""The shared core must contain zero Rune-specific assumptions."""
|
||||
|
||||
def test_no_rune_paths_in_source(self):
|
||||
src = (ROOT / "src" / "rune" / "structural_store.py").read_text()
|
||||
for forbidden in ("/src/rune", "/mnt/workdrive", "rune-code", "~/.rune", "~/.config/opencode", "tests/"):
|
||||
assert forbidden not in src, f"Rune-specific reference: {forbidden}"
|
||||
|
||||
def test_no_rune_module_imports(self):
|
||||
import ast
|
||||
|
||||
tree = ast.parse((ROOT / "src" / "rune" / "structural_store.py").read_text())
|
||||
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("rune" in m for m in imported), f"shared core must not import rune modules: {imported}"
|
||||
|
||||
def test_no_rune_vocabulary_in_schema(self):
|
||||
"""Kinds like 'module'/'subsystem' are DATA, not schema. The shared
|
||||
core must not hardcode them."""
|
||||
src = (ROOT / "src" / "rune" / "structural_store.py").read_text()
|
||||
for forbidden_kind in ('"module"', '"subsystem"', '"deployment_surface"'):
|
||||
assert forbidden_kind not in src, f"Rune vocabulary hardcoded in shared core: {forbidden_kind}"
|
||||
|
||||
|
||||
class TestStoreRoundtrip:
|
||||
def _doc(self):
|
||||
d = ss.new_doc()
|
||||
ss.add_entity(d, id="project:alpha", kind="project", identities={"purpose": "test fixture"})
|
||||
ss.add_entity(d, id="component:w", kind="widget_component", maturity="DECLARED")
|
||||
ss.add_relationship(d, type="builds", from_id="project:alpha", to_id="component:w", evidence_refs=["ev"])
|
||||
return d
|
||||
|
||||
def test_open_world_entity_kind_roundtrip(self, tmp_path):
|
||||
"""An entity kind the store has never heard of round-trips intact."""
|
||||
d = self._doc()
|
||||
ss.add_entity(
|
||||
d, id="capability:quantum_flip", kind="quantum_flipper", identities={"axis": "z"}, ext={"wibble": 42}
|
||||
)
|
||||
p = ss.save(d, tmp_path / "s.json")
|
||||
loaded = ss.load(p)
|
||||
e = ss.entity(loaded, "capability:quantum_flip")
|
||||
assert e["kind"] == "quantum_flipper"
|
||||
assert e["ext"]["wibble"] == 42
|
||||
assert e["identities"]["axis"] == "z"
|
||||
|
||||
def test_unknown_relationship_type_roundtrip(self, tmp_path):
|
||||
d = self._doc()
|
||||
ss.add_relationship(
|
||||
d, type="contextualises", from_id="project:alpha", to_id="component:w", evidence_refs=["ev"]
|
||||
)
|
||||
p = ss.save(d, tmp_path / "s.json")
|
||||
loaded = ss.load(p)
|
||||
rels = ss.relationships(loaded, type="contextualises")
|
||||
assert len(rels) == 1 and rels[0]["to"] == "component:w"
|
||||
|
||||
def test_stable_identity_across_save_load(self, tmp_path):
|
||||
d = self._doc()
|
||||
uri = ss.spine_uri("project:alpha")
|
||||
p = ss.save(d, tmp_path / "s.json")
|
||||
loaded = ss.load(p)
|
||||
assert uri == "spine://project/alpha"
|
||||
assert ss.entity(loaded, ss.resolve_uri(uri))["kind"] == "project"
|
||||
|
||||
def test_alias_roundtrip(self, tmp_path):
|
||||
d = self._doc()
|
||||
ss.alias(d, "component:w", "component:widget_old_name")
|
||||
p = ss.save(d, tmp_path / "s.json")
|
||||
loaded = ss.load(p)
|
||||
assert ss.entity(loaded, "component:widget_old_name")["id"] == "component:w"
|
||||
|
||||
def test_supersession_roundtrip(self, tmp_path):
|
||||
d = self._doc()
|
||||
ss.add_entity(d, id="component:w2", kind="widget_component", maturity="DECLARED")
|
||||
ss.supersede(d, "component:w2", "component:w", "evidence:decision")
|
||||
p = ss.save(d, tmp_path / "s.json")
|
||||
loaded = ss.load(p)
|
||||
old = ss.entity(loaded, "component:w")
|
||||
assert old["superseded_by"] == "component:w2"
|
||||
assert old["status"] == "SUPERSEDED"
|
||||
|
||||
def test_maturity_requires_evidence(self):
|
||||
d = self._doc()
|
||||
with pytest.raises(ValueError, match="requires evidence"):
|
||||
ss.promote(d, "component:w", "PRESENT", "")
|
||||
|
||||
def test_no_evidence_no_live_proven(self):
|
||||
d = self._doc()
|
||||
assert ss.entity(d, "component:w")["maturity"] == "DECLARED"
|
||||
|
||||
def test_reconciliation_stales_unobserved(self, tmp_path):
|
||||
d = self._doc()
|
||||
rel = ss.relationships(d, type="builds")[0]
|
||||
rel["authority"] = "test-projector"
|
||||
rel["status"] = "CURRENT"
|
||||
ss._doc_authorities(d)["test-projector"] = {"generation": 1, "observed": []}
|
||||
staled = ss.reconcile_authorities(d)
|
||||
assert len(staled) == 1
|
||||
assert rel["status"] == "STALE"
|
||||
|
||||
def test_snapshot_deterministic(self, tmp_path):
|
||||
d = self._doc()
|
||||
d["generated_at"] = "FIXED"
|
||||
p1 = ss.snapshot(d, tmp_path, "a", tag="deadbeef")
|
||||
p2 = ss.snapshot(d, tmp_path, "a", tag="deadbeef")
|
||||
assert p1.read_bytes() == p2.read_bytes()
|
||||
|
||||
def test_diff_categories(self, tmp_path):
|
||||
old = self._doc()
|
||||
new = json.loads(json.dumps(old))
|
||||
ss.add_entity(new, id="component:new_thing", kind="widget_component", maturity="DECLARED")
|
||||
ss.promote(new, "component:w", "WIRED", "new-imports")
|
||||
d = ss.diff(old, new)
|
||||
assert d["entities_added"] == ["component:new_thing"]
|
||||
assert {"id": "component:w", "from": "DECLARED", "to": "WIRED"} in d["maturity_changed"]
|
||||
|
||||
def test_schema_version_enforced(self, tmp_path):
|
||||
d = self._doc()
|
||||
d["schema_version"] = 99
|
||||
with pytest.raises(ValueError, match="schema_version"):
|
||||
ss.save(d, tmp_path / "bad.json")
|
||||
|
||||
|
||||
class TestSpineExtractionNeutrality:
|
||||
"""Behavior-neutral proof: the shared core produces identical results
|
||||
to the pre-extraction spine engine for identical inputs."""
|
||||
|
||||
def test_spine_imports_shared_core(self):
|
||||
from rune import spine
|
||||
|
||||
assert hasattr(spine, "add_entity") and hasattr(spine, "impact_frontier")
|
||||
|
||||
def test_spine_rebuild_identical_structure(self, tmp_path):
|
||||
"""Rebuild through the refactored spine produces identical structural
|
||||
content to the pre-extraction snapshot (modulo observation timestamps
|
||||
and the legitimate deployed-identity advance)."""
|
||||
pre = json.loads((ROOT / "store" / "spine" / "current.json").read_text())
|
||||
# run the actual spine build via CLI in a subprocess (real path)
|
||||
r = subprocess.run(
|
||||
[sys.executable, "-m", "rune.spine", "build"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={"PYTHONPATH": str(ROOT / "src"), "PATH": "/usr/bin:/bin", "HOME": "/tmp"},
|
||||
cwd=str(ROOT),
|
||||
)
|
||||
assert r.returncode == 0, r.stderr[-300:]
|
||||
post = json.loads((ROOT / "store" / "spine" / "current.json").read_text())
|
||||
# structural content must be identical
|
||||
pre_e = {e["id"]: {k: v for k, v in e.items() if k != "observed_at"} for e in pre["entities"]}
|
||||
post_e = {e["id"]: {k: v for k, v in e.items() if k != "observed_at"} for e in post["entities"]}
|
||||
# deployed identity advances legitimately (deploy moved 982a→e3b4)
|
||||
pre_e.pop("deployment:current", None)
|
||||
post_e.pop("deployment:current", None)
|
||||
assert pre_e == post_e, "unexplained structural change in rebuild"
|
||||
assert pre["relationships"] == post["relationships"]
|
||||
|
||||
def test_spine_store_schema_unchanged(self):
|
||||
d = json.loads((ROOT / "store" / "spine" / "current.json").read_text())
|
||||
assert d["schema_version"] == 1
|
||||
kinds = {e["kind"] for e in d["entities"]}
|
||||
assert kinds <= {
|
||||
"module",
|
||||
"subsystem",
|
||||
"deployment_surface",
|
||||
"project",
|
||||
"contract",
|
||||
"maintenance_item",
|
||||
"prd_state",
|
||||
}
|
||||
Reference in new issue
Block a user