Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec434f2a70 | ||
|
|
d9c50c66c9 | ||
|
|
a382c15562 | ||
|
|
186fdd1410 | ||
|
|
aa9750c59f | ||
|
|
ab114d260b | ||
|
|
b7865a76bb | ||
|
|
04b0f4ac96 | ||
|
|
9b328c9af1 | ||
|
|
bc0f244934 | ||
|
|
29570f404a | ||
|
|
927548e9a0 | ||
|
|
781b286f5b | ||
|
|
d1ccd7ebec | ||
|
|
3adfa1b1ed | ||
|
|
da4eece9e0 | ||
|
|
2e1f0c8cab | ||
|
|
6e2f5950bd | ||
|
|
c53c82bb35 | ||
|
|
35adcd8cb1 | ||
|
|
810d0027c3 | ||
|
|
4839a62b70 | ||
|
|
15408841ac | ||
|
|
c5c69de06f | ||
|
|
fcde0b2ce3 | ||
|
|
1f96055b69 | ||
|
|
923a21ba0b | ||
|
|
6348386869 | ||
|
|
55002ad838 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.67**
|
||||
**Current release: v0.66.76**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_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-15T16:31:19Z",
|
||||
"generated_at": "2026-09-15T22:21:34Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -12,6 +12,7 @@
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"evidence_probe",
|
||||
"forge",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
@@ -216,6 +217,7 @@
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"build_basis",
|
||||
"capability_fit",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
@@ -223,6 +225,7 @@
|
||||
"construction_commands",
|
||||
"construction_decision",
|
||||
"construction_depth",
|
||||
"construction_intent",
|
||||
"construction_interface",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
@@ -233,6 +236,8 @@
|
||||
"drift_resume",
|
||||
"event_wait",
|
||||
"failure_recovery",
|
||||
"graph_projection",
|
||||
"impact_preview",
|
||||
"layout_contract",
|
||||
"programme_continuation",
|
||||
"release_notes",
|
||||
@@ -240,6 +245,8 @@
|
||||
"scaffold",
|
||||
"spine",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"structural_store",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
|
||||
@@ -104,15 +104,15 @@
|
||||
},
|
||||
{
|
||||
"path": "src/rune/attention_capsule.py",
|
||||
"role": "AttentionCapsule — the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"role": "AttentionCapsule \u2014 the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives — the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives \u2014 the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/inference_trace.py",
|
||||
"role": "Durable per-run inference trace (tokens served/estimated, output, latency, per-block visibility)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served — builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served \u2014 builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -160,13 +160,13 @@
|
||||
"path": "src/rune/capability_broker.py",
|
||||
"role": "Authoritative capability selector (select_for_job + verified floor + find_capability escape hatch)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-01 (#24): select_for_job is the single selection authority — its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
|
||||
"evidence": "P1-01 (#24): select_for_job is the single selection authority \u2014 its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
|
||||
},
|
||||
{
|
||||
"path": "memory/index/capability_cards.json",
|
||||
"role": "Capability-card index (rebuilt on empty/stale/corrupt; native cards synthesized from TOOLS metadata)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-01 (#24): load_cards validates and rebuilds — the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
"evidence": "P1-01 (#24): load_cards validates and rebuilds \u2014 the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/local_tool_runtime.py",
|
||||
@@ -486,12 +486,6 @@
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "Defines the owner-approved canary-first Gitea sequence."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/plugins/build_mode/__init__.py",
|
||||
"role": "Checkpoint/ledger state-machine primitive used by current governed builds",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "Persists checkpoints but encodes an older phase order."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/build_mode.py",
|
||||
"role": "Separate BuildManager and worktree engine",
|
||||
@@ -504,6 +498,9 @@
|
||||
"disposition": "archive-candidate",
|
||||
"evidence": "Reports fixed success strings for phases without executing their claimed effects."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"T17 17U: plugin BuildStateMachine deleted (zero importers; second parallel orchestrator)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -540,4 +537,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# SCAFFOLDING IMPLEMENTATION AUDIT + V1 GAP MATRIX (T18 §18B)
|
||||
|
||||
_Every claim classified against evidence. Scale: LIVE_PROVEN (production
|
||||
evidence) / TEST_PROVEN (suite green, no production exercise) /
|
||||
IMPLEMENTED_UNPROVEN / PARTIAL / PLANNED / ABSENT / LEGACY / DUPLICATE._
|
||||
|
||||
## Kernel claims
|
||||
|
||||
| Claim | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Contract lifecycle + legal transitions | LIVE_PROVEN | 28 contracts driven through real flow; refusal envelopes observed |
|
||||
| classify_mode (one classifier) | LIVE_PROVEN | gates TUI /build (17G), operator hook, tests 62+ |
|
||||
| Conversational intent layer | TEST_PROVEN | 22+ held-outs incl. unseen paraphrases; 17R live exercise (one real conversational proof) |
|
||||
| Programme continuation | LIVE_PROVEN | 17E 4-stage proof on real bridge; restart/dup/failure held-outs |
|
||||
| Event-driven waits (builder) | LIVE_PROVEN | T15 15Q real model build; T17E async stage |
|
||||
| Event-driven waits (CI) | LIVE_PROVEN | T16 16D deadline path ×2 + T17 natural wake (mode=event, 112.5s) |
|
||||
| Event replay (before-registration) | TEST_PROVEN + production-realistic exercise | 17C 4.6s vs real completed CI; natural-cycle confirmation: the 17Z wake arrived mid-wait, replay path itself not yet hit naturally |
|
||||
| Cross-user bridge delivery | LIVE_PROVEN | 17C repro as runner user (2.05s) + production deploys |
|
||||
| Provider boundary | TEST_PROVEN (structural guard) + observed loud-failure | 16F/G suites; no silent fallback observed in production runs |
|
||||
| Builder cannot self-certify | LIVE_PROVEN | T14 closure refusals ×2; phase-gated close |
|
||||
| Completion truth evidence-driven | LIVE_PROVEN | contract checkpoints; verify_test_truth refusals |
|
||||
| Interruption/resume | LIVE_PROVEN | T14E SIGKILL; 17E restart held-outs |
|
||||
| Impact preview (visible, pre-build) | ABSENT → T18 | nothing renders structured impact before build today |
|
||||
| Structural project model (first-class) | PARTIAL | stores are spine-shaped (entities/rels) but projects model CONTRACTS, not architecture; no domain/subsystem/interface nesting for arbitrary projects |
|
||||
| Structural queries/impact closure for projects | PARTIAL | spine.impact_frontier exists for Rune; project stores lack query API |
|
||||
| Research→Build Basis (durable, versioned) | ABSENT → T18 | research output today stays in transient context/artifacts |
|
||||
| Research-depth decision at construction entry | PARTIAL | ADAPT routes task classes; no explicit construction-stage research gate |
|
||||
| OUTWARD prior-art-first pass | PARTIAL | stage exists in Research Core; not strengthened to precedent-first ordering |
|
||||
| Schema versioning + consent-gated migration | PARTIAL | stores carry schema_version=1; no migration machinery/consent flow |
|
||||
| Environment/runtime model | PARTIAL | deployment_surface exists in Spine; project scaffolds don't model envs |
|
||||
| Workspace lifecycle (worktree per contract) | PARTIAL | BuildManager worktrees exist (legacy adapter); not wired to contracts |
|
||||
| Forge abstraction | ABSENT (Gitea-coupled scripts) | deploy/qualify call Gitea API directly |
|
||||
| Structural drift for project scaffolds | PARTIAL | drift_resume (16P) covers checkpoint-vs-reality; no file/entity-level drift detection |
|
||||
| Graph projection for visualisation | ABSENT → T18 | Spine has snapshots; no stable read-only graph projection |
|
||||
| Memory/Substrate | ABSENT (BY DESIGN — out of scope) | rule L |
|
||||
|
||||
## V1 North Star gap matrix (this tranche's targets)
|
||||
|
||||
| North Star capability | Gap | T18 action |
|
||||
|---|---|---|
|
||||
| Research-led construction (early research, Build Basis) | ABSENT Build Basis | 18C: build_basis.py + ADAPT depth hook |
|
||||
| OUTWARD precedent-first | PARTIAL | 18D: strengthen stage contract + real research run |
|
||||
| First-class structural Scaffold | PARTIAL (contract-shaped) | 18E: structural_model.py over structural_store core |
|
||||
| Structure→scope→governance | PARTIAL | 18F: queries + impact closure feeding contracts |
|
||||
| Visible impact preview | ABSENT | 18G: impact_preview projection |
|
||||
| Structural drift | PARTIAL | 18H: extend drift_resume with entity-level checks |
|
||||
| Schema versioning + consent | PARTIAL | 18I: migration + consent flow + tests |
|
||||
| Environment/runtime model | PARTIAL | 18J: env entities + verification distinction |
|
||||
| Workspace lifecycle | PARTIAL (legacy adapter) | 18K: workspace contract on the BuildManager adapter |
|
||||
| Forge boundary | ABSENT | 18L: forge.py adapter interface (Gitea impl) |
|
||||
| Full route convergence | PARTIAL (classify shared; backends differ) | 18M: audit + smallest fix |
|
||||
| Generalisation (not phrase list) | TEST_PROVEN | 18N: adversarial paraphrase families |
|
||||
| Self-scaffold | PARTIAL (contracts only) | 18O: structural model of Scaffolding itself |
|
||||
| Upgrade-via-Scaffold proof | ABSENT | 18P: one real upgrade driven by the scaffold |
|
||||
| Graph projection | ABSENT | 18Q: versioned read-only projection |
|
||||
|
||||
## Kernel exceptions required (rule B record)
|
||||
|
||||
None yet. Every T18 plan so far EXTENDS (new modules consuming
|
||||
structural_store) rather than modifying frozen kernel behavior. Any
|
||||
exception discovered during implementation will be recorded here with
|
||||
evidence that the V1 requirement could not be met otherwise.
|
||||
@@ -0,0 +1,127 @@
|
||||
# SCAFFOLDING V1 DEFINITION (T18 §18B)
|
||||
|
||||
_Plane-English + technical definition of Scaffolding as it ACTUALLY
|
||||
exists at tranche T18 start (master 29570f4). Companion artifact:
|
||||
SCAFFOLDING_IMPLEMENTATION_AUDIT (evidence classification per claim)._
|
||||
|
||||
## What Scaffolding is (plain English)
|
||||
|
||||
Scaffolding is Rune's **construction authority**: the governed process
|
||||
by which software gets built or changed. It takes an authorized need,
|
||||
classifies what kind of construction it is, proportionally governs the
|
||||
work through a durable contract (with legal lifecycle transitions),
|
||||
derives scope from structural reality, drives builders, waits on events
|
||||
(not sleeps), refuses self-certified completion, verifies from
|
||||
canonical reality, and records evidence on durable objects so any
|
||||
future session can resume without a transcript.
|
||||
|
||||
It is NOT a scheduler, NOT a memory system, NOT a provider selector,
|
||||
NOT a research engine, NOT a release system. It is the layer that
|
||||
decides *whether, how, and with what obligations* construction happens
|
||||
— and whether it actually finished.
|
||||
|
||||
## What it owns
|
||||
|
||||
- **Construction classification** (`construction_mode.classify_mode` —
|
||||
the ONE classifier: NOT_CONSTRUCTION / LOCAL_CHANGE /
|
||||
EXISTING_PROJECT_CHANGE / PROJECT_BUILD / UNKNOWN; never silently
|
||||
chooses; unknown surfaces probe questions)
|
||||
- **Conversational interpretation** (`construction_intent.py` —
|
||||
meaning/referents/authorization; DISCUSSION→PROPOSAL→USER_APPROVED;
|
||||
feeds the ONE classifier, never deepens)
|
||||
- **Project Scaffold + Contracts** (`scaffold.py` — legal phase map
|
||||
draft→approved→building→verifying→…→closed/failed/abandoned; scope;
|
||||
obligations; checkpoints; predicted-vs-observed at close)
|
||||
- **Programme continuation** (`programme_continuation.py` —
|
||||
run_construction_programme: stages 1:1 to contracts, persist→canary→
|
||||
next; "task done ≠ programme done")
|
||||
- **Impact/scope machinery** (construction_decision, completeness
|
||||
gates, affected-surface propagation)
|
||||
- **Stage guards** (`stage_guards.py` — readiness blocks, liveness
|
||||
proves progress, exit canary proves effect; verify_test_truth refuses
|
||||
false greens)
|
||||
- **Failure taxonomy** (`failure_recovery.py` — 7 classes incl.
|
||||
PROVIDER_UNAVAILABLE; retry budgets; PERMANENT/APPROVAL never retry)
|
||||
- **Builder interface** (`construction_interface.py` — ONE builder
|
||||
boundary; JSON envelopes; builder identity recorded-but-inert;
|
||||
discover_active_contract; capabilities())
|
||||
|
||||
## What it does NOT own
|
||||
|
||||
- **Provider/model selection** (provider layer owns; ambient context
|
||||
via `provider_context.require_model`; structural guard test)
|
||||
- **Research competence** (Research Core owns; Scaffolding invokes and
|
||||
consumes — see 18C integration)
|
||||
- **Asynchronous wait/resume machinery** (W/R event bridge owns;
|
||||
`event_wait.py` is the repo-side adapter: event drives continuation,
|
||||
watchdog detects failure, durable record is truth)
|
||||
- **Release/deployment** (deploy_stage/promotion/qualify + Gitea
|
||||
workflows own it; Scaffolding records evidence)
|
||||
- **Rune's structural self-model** (Spine owns — `spine.py` over
|
||||
`structural_store.py`; Scaffolding stores use the SAME generic core)
|
||||
- **Long-horizon relevance/memory** (future Substrate — not built;
|
||||
seam exists: construction_projection + construction_commands)
|
||||
- **The TUI** (client; sends text, renders projections)
|
||||
|
||||
## How the flows work
|
||||
|
||||
**New build (PROJECT_BUILD)**: conversational/explicit intent →
|
||||
classify_mode (+ project-existence evidence) → activate() bootstraps a
|
||||
provisional Project Scaffold (identity before repository, UNKNOWN
|
||||
fields explicit) → contract draft→approved (user provenance) →
|
||||
building (builder under event wait) → verifying (exit canary;
|
||||
independent evidence) → closed with predicted-vs-observed.
|
||||
|
||||
**Existing-project change**: intent → classify (scoped/file/target
|
||||
evidence; proportional) → attach to existing authority → light or full
|
||||
contract by depth → same lifecycle, proportionate ceremony.
|
||||
|
||||
**Research-led construction (V1 target — CORRECTED 2026-09-15, Nick):**
|
||||
|
||||
> THE RESEARCH PLAN ITSELF IS CONTEXTUALLY GENERATED. There is NO
|
||||
> permanent research-topic checklist. For every request:
|
||||
> REQUEST/INTENT → understand target/current reality → identify material
|
||||
> unknowns/decisions/risks → generate a context-specific research plan →
|
||||
> perform proportional INWARD/OUTWARD/deeper research. OUTWARD asks what
|
||||
> already exists relevant to THIS specific intent (adopt/adapt/combine/
|
||||
> reject/gaps/innovation). The categories researched are selected from
|
||||
> context; deeper technical research is driven by unresolved questions
|
||||
> and consequence level, never performed mechanically.
|
||||
|
||||
`build_basis.generate_research_plan()` implements this: topics are
|
||||
synthesized from THIS intent's material unknowns/decisions/risks;
|
||||
the held-out suite proves unrelated intents produce materially
|
||||
different plans and that no canned category list exists.
|
||||
|
||||
**Future upgrades (V1 target)**: authorized need → research-depth
|
||||
decision (ADAPT) → Research Core pass → durable Build Basis →
|
||||
structural Scaffold consult/construct → impact closure + visible
|
||||
preview → contract with structurally-derived scope → isolated
|
||||
workspace → governed build/verify → merge/release → structural
|
||||
Scaffold updated. (Parts of this chain are new in T18 — see audit.)
|
||||
|
||||
**Interruption/resume**: SIGKILL-safe. Durable contract + programme
|
||||
records + wait records are the truth; fresh process resumes from
|
||||
identity alone; duplicate signals idempotent (live-proven T14E/T17E).
|
||||
|
||||
**Deployment/release participation**: Scaffolding does not run the
|
||||
pipeline; it consumes qualified CI/deploy evidence, records deployed
|
||||
identity on contracts, and refuses closure without it where contracted.
|
||||
|
||||
## How Spine relates
|
||||
|
||||
Spine = Rune's canonical structural self-model (structural_store
|
||||
generic core, maturity ladder, impact_frontier). Scaffolding's project
|
||||
stores share the SAME generic core (zero Rune constants) — one graph
|
||||
discipline, two scopes: Spine models Rune; project Scaffolds model
|
||||
their projects. No duplicate truth: a project's structural model lives
|
||||
in its own store; Rune's lives in Spine. Later retrofit will modernise
|
||||
Spine using the discipline V1 generalises (18K relationship).
|
||||
|
||||
## How Substrate will interact (without owning internals)
|
||||
|
||||
Read: versioned outbound projection (construction_projection,
|
||||
SCHEMA_VERSION=1). Write: bounded validated commands
|
||||
(construction_commands — 6 commands, lifecycle-validated, idempotent,
|
||||
no direct storage mutation). Substrate asks; Scaffolding decides.
|
||||
Memory graph explicitly out of scope (T18 rule L).
|
||||
@@ -22,7 +22,9 @@ and deploys Rune itself to `/mnt/workdrive/rune-code/current`.
|
||||
| Construction (build lifecycle) | `src/rune/scaffold.py` + `construction_*.py` | scaffold store (`<dir>/.rune/scaffold`) |
|
||||
| Builder sessions | `scripts/t15_builder_session.py` (event-driven successor; T14 is legacy) | contract + wait records |
|
||||
| Provider/model selection | `rune_operator` final decider; ambient context via `provider_context.require_model` | `state/model_routing_profiles.json` |
|
||||
| Wait/resume (any long op) | `src/rune/event_wait.py` + W/R daemon (out-of-repo snapshot at `~/.local/share/mimo-wait-resume`, data root `/mnt/workdrive/wr-bridge`) | waits/events/leases/resumes JSON |
|
||||
| Wait/resume (any long op) | `src/rune/event_wait.py` + W/R daemon (out-of-repo snapshot at `~/.local/share/mimo-wait-resume`, data root `/mnt/workdrive/wr-bridge` — PIN `RUNE_WR_ROOT`: unpinned consumers get a loud warning, never a silent split-brain) | waits/events/leases/resumes JSON |
|
||||
| Programme continuation | `src/rune/programme_continuation.py` (WIRED T17: `run_construction_programme` binds stages to Scaffolding contracts) | scaffold store `programmes/` |
|
||||
| Conversational construction | `src/rune/construction_intent.py` (meaning/referents/authorization) → `construction_mode.classify_mode` (the ONE classifier) | intents are transient; contracts durable |
|
||||
| CI qualification verdict | `scripts/deploy_qualify.py` (two legs, fail-closed) | Gitea commit status + runs |
|
||||
| Release/deploy | `scripts/deploy_stage.py` → `deploy_promotion.py` (monotonic, flock) | deploy root + `current`/`last-good` symlinks |
|
||||
| Research | `scripts/research_task_tree.py` (authority; labels are projection) | `research/issue-<N>/` |
|
||||
@@ -45,8 +47,8 @@ and deploys Rune itself to `/mnt/workdrive/rune-code/current`.
|
||||
|
||||
## Unfinished / blocked / deprecated
|
||||
|
||||
- programme_continuation.py: implemented + tested but UNWIRED (no
|
||||
production caller) — the "continue problem" machinery awaits routing.
|
||||
- programme_continuation.py: WIRED (T17 17D — run_construction_programme
|
||||
is the canonical multi-stage driver; 17E live-proven).
|
||||
- maintenance.yml canary-suite job: loops `shared/canary_*.py` — wrong
|
||||
dir (real canaries in `src/rune/`), job likely no-ops (LEGACY).
|
||||
- daemon out-of-repo: the W/R daemon is deployed from
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 354,
|
||||
"S110": 80
|
||||
"BLE001": 350,
|
||||
"S110": 78
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.67"
|
||||
version = "0.66.76"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.67"
|
||||
__version__ = "0.66.76"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
"""build_basis.py — T18 §18C: durable versioned RESEARCH BASIS for
|
||||
construction (V1 rule F: RESEARCH INFORMS STRUCTURE).
|
||||
|
||||
Research output must not disappear in transient model context. A Build
|
||||
Basis is a durable, schema-versioned document that captures WHAT
|
||||
research decided and WHY, with references — not research dumps. It is
|
||||
created after the research-depth decision and before/with the
|
||||
Construction Contract, and is attachable to:
|
||||
|
||||
- a Project Scaffold (project-level basis)
|
||||
- a Construction Contract (per-contract basis, via evidence_ref)
|
||||
- future structural entities where useful
|
||||
|
||||
Research Core ownership (rule C): this module NEVER researches. It
|
||||
records the depth decision (derived FROM adaptive_router's route — no
|
||||
second router), references the canonical pipeline's qualified output
|
||||
(stages ISSUE→…→QUALIFY, artifacts in research/issue-<N>/), and
|
||||
structures the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
# Research depth tiers — derived from adaptive_router ROUTES, never a
|
||||
# second router (rule C/A). Each tier records WHY it was chosen.
|
||||
DEPTH_NONE = "NONE" # tiny obvious local change; local evidence only
|
||||
DEPTH_INWARD = "INWARD" # repo-internal understanding suffices
|
||||
DEPTH_OUTWARD = "OUTWARD" # precedent/prior-art discovery required
|
||||
DEPTH_FULL = "FULL" # full pipeline incl. adversarial+synthesis
|
||||
|
||||
DEPTH_TIERS = (DEPTH_NONE, DEPTH_INWARD, DEPTH_OUTWARD, DEPTH_FULL)
|
||||
|
||||
# route → default depth (recorded as decision evidence; caller may
|
||||
# justify an exception, which is recorded as such — never silent)
|
||||
_ROUTE_DEFAULT_DEPTH = {
|
||||
"LOCAL_FIX": DEPTH_NONE,
|
||||
"STANDARD_CHANGE": DEPTH_INWARD,
|
||||
"RESEARCHED_CHANGE": DEPTH_FULL,
|
||||
}
|
||||
|
||||
|
||||
def research_depth(route: str, *, mode: str = "",
|
||||
justification: str = "") -> dict:
|
||||
"""The research-depth DECISION for a construction need. Derived from
|
||||
the canonical route authority (adaptive_router); `justification` is
|
||||
REQUIRED when deviating from the route default — recorded, never
|
||||
silent (rule D: record why research depth was chosen)."""
|
||||
default = _ROUTE_DEFAULT_DEPTH.get(route, DEPTH_INWARD)
|
||||
depth = default
|
||||
why = f"route {route} default"
|
||||
if justification and justification.strip():
|
||||
# an explicit justification selects a tier by name if given;
|
||||
# otherwise it annotates the default
|
||||
candidate = justification.strip().split(":", 1)[0].strip()
|
||||
if candidate in DEPTH_TIERS:
|
||||
depth = candidate
|
||||
why = justification.strip()
|
||||
else:
|
||||
why = f"route {route} default; note: {justification.strip()}"
|
||||
elif route not in _ROUTE_DEFAULT_DEPTH:
|
||||
why = f"route {route!r} unknown — INWARD floor (never silent NONE)"
|
||||
depth = DEPTH_INWARD
|
||||
return {"depth": depth, "why": why, "route": route, "mode": mode,
|
||||
"decided_at": _now()}
|
||||
|
||||
|
||||
# OUTWARD precedent-first ordering contract (rule E): the questions the
|
||||
# prior-art pass MUST answer before deeper material is consulted.
|
||||
OUTWARD_PRIOR_ART_QUESTIONS = (
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required",
|
||||
)
|
||||
|
||||
_DISPOSITIONS = ("ADOPT", "ADAPT", "COMBINE", "REJECT", "INNOVATE")
|
||||
|
||||
|
||||
def new_basis(*, question: str, depth_decision: dict,
|
||||
issue_ref: str = "") -> dict:
|
||||
"""Create an empty-but-valid Build Basis. References only — full
|
||||
research dumps live in the Research Core's artifact dirs (rule 18C:
|
||||
do not store unnecessary dumps)."""
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
"created_at": _now(),
|
||||
"question": question,
|
||||
"issue_ref": issue_ref, # e.g. research/issue-164 or Gitea issue
|
||||
"depth": depth_decision,
|
||||
"inward": {"findings": [], "refs": []},
|
||||
"outward": {"prior_art_questions_answered": [],
|
||||
"findings": [], "refs": []},
|
||||
"deeper": {"used": False, "findings": [], "refs": []},
|
||||
"dispositions": [], # [{item, disposition: ADOPT/…, why, ref}]
|
||||
"alternatives_considered": [],
|
||||
"uncertainty": [],
|
||||
"risks": [],
|
||||
"implications": {"architecture": [], "structural": [],
|
||||
"migration": [], "verification": []},
|
||||
}
|
||||
|
||||
|
||||
def set_dispositions(basis: dict, dispositions: list[dict]) -> list[str]:
|
||||
"""Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
Returns validation errors; the basis stays unqualified until valid."""
|
||||
basis["dispositions"] = [
|
||||
{"item": d.get("item", ""), "disposition": d.get("disposition", ""),
|
||||
"why": d.get("why", ""), "ref": d.get("ref", "")}
|
||||
for d in dispositions
|
||||
]
|
||||
return validate_basis(basis)
|
||||
|
||||
|
||||
def validate_basis(basis: dict) -> list[str]:
|
||||
"""Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
decision with why, at least one outward prior-art answer when depth
|
||||
is OUTWARD/FULL, and every disposition uses a legal disposition."""
|
||||
errors: list[str] = []
|
||||
if basis.get("schema_version") != SCHEMA_VERSION:
|
||||
errors.append(f"schema_version {basis.get('schema_version')!r}")
|
||||
depth = (basis.get("depth") or {}).get("depth")
|
||||
if depth not in DEPTH_TIERS:
|
||||
errors.append(f"depth {depth!r} not in {DEPTH_TIERS}")
|
||||
if not (basis.get("depth") or {}).get("why"):
|
||||
errors.append("depth decision lacks WHY (rule D)")
|
||||
if depth in (DEPTH_OUTWARD, DEPTH_FULL):
|
||||
outward = basis.get("outward") or {}
|
||||
if not (outward.get("prior_art_questions_answered")
|
||||
or outward.get("findings") or outward.get("refs")):
|
||||
errors.append("OUTWARD/FULL depth requires prior-art evidence")
|
||||
if depth == DEPTH_INWARD:
|
||||
inward = basis.get("inward") or {}
|
||||
if not (inward.get("findings") or inward.get("refs")
|
||||
or basis.get("dispositions")):
|
||||
errors.append("INWARD depth requires recorded evidence "
|
||||
"(findings/refs) — an empty basis is not "
|
||||
"qualified")
|
||||
for d in basis.get("dispositions", []):
|
||||
if d.get("disposition") not in _DISPOSITIONS:
|
||||
errors.append(f"illegal disposition {d.get('disposition')!r} "
|
||||
f"for {d.get('item','')[:40]!r}")
|
||||
return errors
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
return not validate_basis(basis)
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
"""The evidence_ref string a contract carries to point at this basis
|
||||
(durable pointer, not a copy — stores stay independent)."""
|
||||
return (f"build_basis:{basis.get('issue_ref') or 'adhoc'}:"
|
||||
f"{basis.get('created_at')}")
|
||||
|
||||
|
||||
def summary(basis: dict) -> dict:
|
||||
"""Compact projection for prompts/previews (no private reasoning —
|
||||
structured conclusions only, rule 18G)."""
|
||||
return {
|
||||
"schema_version": basis.get("schema_version"),
|
||||
"question": basis.get("question"),
|
||||
"issue_ref": basis.get("issue_ref"),
|
||||
"depth": basis.get("depth"),
|
||||
"qualified": is_qualified(basis),
|
||||
"validation": validate_basis(basis),
|
||||
"dispositions": [
|
||||
{"item": d.get("item", "")[:80],
|
||||
"disposition": d.get("disposition")}
|
||||
for d in basis.get("dispositions", [])
|
||||
],
|
||||
"uncertainty_count": len(basis.get("uncertainty", [])),
|
||||
"risks_count": len(basis.get("risks", [])),
|
||||
"implications": {k: len(v) for k, v in
|
||||
(basis.get("implications") or {}).items()},
|
||||
}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
import json
|
||||
return json.dumps(basis, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
def from_json(text: str) -> dict:
|
||||
import json
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
_ = Any # typing import kept for forward annotations
|
||||
|
||||
|
||||
# ── T18 §18D CORRECTION (Nick, 2026-09-15): THE RESEARCH PLAN ITSELF IS
|
||||
# CONTEXTUALLY GENERATED. The prior-art categories researched for the
|
||||
# structural-model task (architecture graphs, monorepo tooling, etc.)
|
||||
# were CONTEXTUAL to that task — they must never become a fixed topic
|
||||
# list. For every request the plan is derived from:
|
||||
# intent → target/current reality → material unknowns/decisions/risks
|
||||
# → context-specific research topics → proportional depth.
|
||||
# There is NO permanent research-topic checklist anywhere in Scaffolding.
|
||||
|
||||
def generate_research_plan(*, intent: str, target: str = "",
|
||||
current_reality: str = "",
|
||||
unknowns: list[str] | None = None,
|
||||
decisions: list[str] | None = None,
|
||||
risks: list[str] | None = None,
|
||||
depth: str | None = None) -> dict:
|
||||
"""Generate a CONTEXT-SPECIFIC research plan for one construction
|
||||
intent. Topics are synthesized from the material unknowns/decisions/
|
||||
risks of THIS intent — the function has no topic vocabulary and no
|
||||
permanent category list. Depth, when not supplied, is chosen by
|
||||
consequence heuristics recorded in the plan (why is always present)."""
|
||||
unknowns = [u for u in (unknowns or []) if u.strip()]
|
||||
decisions = [d for d in (decisions or []) if d.strip()]
|
||||
risks = [r for r in (risks or []) if r.strip()]
|
||||
topics: list[dict] = []
|
||||
# each material unknown becomes a research topic aimed at resolving it
|
||||
for u in unknowns:
|
||||
topics.append({
|
||||
"topic": f"resolve unknown: {u}",
|
||||
"kind": "unknown-resolution",
|
||||
"why": f"material unknown blocks confident construction: {u[:80]}",
|
||||
})
|
||||
for d in decisions:
|
||||
topics.append({
|
||||
"topic": f"inform decision: {d}",
|
||||
"kind": "decision-support",
|
||||
"why": f"consequential decision needs precedent/evidence: {d[:80]}",
|
||||
})
|
||||
for r in risks:
|
||||
topics.append({
|
||||
"topic": f"de-risk: {r}",
|
||||
"kind": "risk-investigation",
|
||||
"why": f"identified risk needs verification/mitigation evidence: {r[:80]}",
|
||||
})
|
||||
# precedent question (rule E) attaches ONLY when outward evidence is
|
||||
# plausibly relevant — an unknown/decision about external prior art.
|
||||
needs_outward = any(
|
||||
w in " ".join(unknowns + decisions).lower()
|
||||
for w in ("exist", "existing", "prior art", "precedent", "standard",
|
||||
"convention", "library", "tool", "framework", "approach"))
|
||||
if needs_outward:
|
||||
topics.append({
|
||||
"topic": ("what already exists that is relevant to THIS intent "
|
||||
"(adopt/adapt/combine/reject/innovate)"),
|
||||
"kind": "prior-art",
|
||||
"why": "material unknown references existing practice — "
|
||||
"precedent discovery required before deeper research",
|
||||
})
|
||||
if depth is None:
|
||||
# consequence heuristics: novel/architectural/unsafe → deeper
|
||||
consequential = any(
|
||||
w in (intent + " " + " ".join(risks)).lower()
|
||||
for w in ("architecture", "migration", "replace", "rewrite",
|
||||
"security", "schema", "delete", "deploy", "breaking"))
|
||||
depth = DEPTH_FULL if (consequential and topics) else (
|
||||
DEPTH_OUTWARD if needs_outward and topics else (
|
||||
DEPTH_INWARD if topics else DEPTH_NONE))
|
||||
why = ("consequence heuristics: consequential keywords + research "
|
||||
"topics present" if depth == DEPTH_FULL else
|
||||
"precedent-relevant unknowns present" if depth == DEPTH_OUTWARD
|
||||
else "local understanding suffices" if depth == DEPTH_INWARD
|
||||
else "no material unknowns — local evidence only")
|
||||
else:
|
||||
why = "depth supplied by caller (recorded, caller accountable)"
|
||||
return {
|
||||
"intent": intent,
|
||||
"target": target,
|
||||
"current_reality_note": current_reality[:200],
|
||||
"depth": {"depth": depth, "why": why},
|
||||
"topics": topics,
|
||||
"plan_generated_at": _now(),
|
||||
# the plan ITSELF is evidence that research was contextual, not
|
||||
# checklist-driven: two different intents must produce disjoint
|
||||
# topic sets unless they share genuine unknowns
|
||||
}
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.67"' in rune_version,
|
||||
'__version__ = "0.66.76"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
"""construction_intent.py — T17 §17J-§17N: the conversational
|
||||
interpretation layer of contextual construction.
|
||||
|
||||
AUTHORITY BOUNDARY (§17O):
|
||||
|
||||
THIS LAYER owns: meaning, referent resolution, target resolution,
|
||||
action-vs-discussion, conversational authorization, user constraints.
|
||||
|
||||
SCAFFOLDING (construction_mode.classify_mode) owns: construction
|
||||
classification, depth, contracts, scope, lifecycle.
|
||||
|
||||
This module NEVER deepens or reclassifies — it resolves what the user
|
||||
MEANT and whether they AUTHORIZED it, then hands a structured
|
||||
ConstructionIntent to Scaffolding, which runs the ONE canonical
|
||||
classifier (classify_mode). No second classifier lives here.
|
||||
|
||||
Not a phrase table: explicit demands are recognized by imperative
|
||||
grammar + construction verbs; questions are recognized by
|
||||
interrogative structure; referents are resolved against the live
|
||||
conversation context (pending proposals, recency, topic drift).
|
||||
Paraphrases resolve because the criteria are structural, not lexical
|
||||
incantations.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# ── conversation context (the layer's memory, NOT Scaffolding state) ────
|
||||
|
||||
@dataclass
|
||||
class Proposal:
|
||||
"""A pending construction proposal awaiting user authorization.
|
||||
Rune suggesting a change is NOT authorization to mutate (§17L)."""
|
||||
proposal_id: str
|
||||
summary: str
|
||||
created_turn: int
|
||||
superseded: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversationContext:
|
||||
"""Active conversational/project context for referent resolution.
|
||||
Recency-aware, proposal-aware, stale-context-resistant (§17M)."""
|
||||
turn: int = 0
|
||||
recent_turns: list = field(default_factory=list) # [{role, text}]
|
||||
pending_proposals: list = field(default_factory=list) # [Proposal]
|
||||
active_project: str | None = None
|
||||
active_task_id: str | None = None
|
||||
|
||||
def next_turn(self) -> int:
|
||||
self.turn += 1
|
||||
return self.turn
|
||||
|
||||
def record_turn(self, role: str, text: str) -> None:
|
||||
self.recent_turns.append({"role": role, "text": text,
|
||||
"turn": self.turn})
|
||||
self.turn += 1 # each recorded exchange advances the context
|
||||
|
||||
def add_proposal(self, summary: str) -> Proposal:
|
||||
p = Proposal(proposal_id=f"prop-{uuid.uuid4().hex[:8]}",
|
||||
summary=summary,
|
||||
created_turn=self.turn)
|
||||
self.pending_proposals.append(p)
|
||||
return p
|
||||
|
||||
def supersede_proposal(self, proposal_id: str) -> None:
|
||||
for p in self.pending_proposals:
|
||||
if p.proposal_id == proposal_id:
|
||||
p.superseded = True
|
||||
|
||||
def live_proposals(self) -> list:
|
||||
return [p for p in self.pending_proposals if not p.superseded]
|
||||
|
||||
|
||||
# ── the structured intent (§17N) ─────────────────────────────────────────
|
||||
|
||||
APPROVAL_STATES = ("DISCUSSION", "PROPOSAL", "USER_APPROVED",
|
||||
"CONSTRUCTION_STARTED")
|
||||
|
||||
# approval-state lifecycle legality (§17L): authorization may only be
|
||||
# granted from PROPOSAL or DISCUSSION (direct demand); CONSTRUCTION_STARTED
|
||||
# and mutations are Scaffolding's side of the boundary.
|
||||
LEGAL_APPROVAL_TRANSITIONS = {
|
||||
"DISCUSSION": {"PROPOSAL", "USER_APPROVED"},
|
||||
"PROPOSAL": {"USER_APPROVED"},
|
||||
"USER_APPROVED": {"CONSTRUCTION_STARTED"},
|
||||
"CONSTRUCTION_STARTED": set(),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstructionIntent:
|
||||
"""Structured meaning-resolution output (§17N). Scaffolding decides
|
||||
mode/depth from this; this layer never does."""
|
||||
intent_id: str
|
||||
objective: str
|
||||
requested_effect: str
|
||||
resolved_target: str | None
|
||||
resolved_referent: str | None
|
||||
origin_context_id: str | None
|
||||
action_intent: str # ACTION | DISCUSSION
|
||||
approval_state: str # DISCUSSION | PROPOSAL | USER_APPROVED
|
||||
confidence: float
|
||||
ambiguity: str # NONE | REFERENT | TARGET | SCOPE
|
||||
consequence_level: str # LOW | MEDIUM | HIGH
|
||||
relevant_constraints: list = field(default_factory=list)
|
||||
classification_evidence: list = field(default_factory=list)
|
||||
probe_questions: list = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"intent_id": self.intent_id,
|
||||
"objective": self.objective,
|
||||
"requested_effect": self.requested_effect,
|
||||
"resolved_target": self.resolved_target,
|
||||
"resolved_referent": self.resolved_referent,
|
||||
"origin_context_id": self.origin_context_id,
|
||||
"action_intent": self.action_intent,
|
||||
"approval_state": self.approval_state,
|
||||
"confidence": self.confidence,
|
||||
"ambiguity": self.ambiguity,
|
||||
"consequence_level": self.consequence_level,
|
||||
"relevant_constraints": self.relevant_constraints,
|
||||
"classification_evidence": self.classification_evidence,
|
||||
"probe_questions": self.probe_questions,
|
||||
}
|
||||
|
||||
|
||||
# ── structural lexical criteria (grammar classes, not incantations) ─────
|
||||
|
||||
# §17M stale-context resistance: a proposal older than this many turns
|
||||
# is stale unless the user's text explicitly references its content.
|
||||
STALE_GAP_TURNS = 2
|
||||
|
||||
_NEW_ARTICLE = re.compile(r"\b(me a|me an|a|an|another)\b")
|
||||
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
|
||||
|
||||
_CONSTRUCTION_VERBS = {
|
||||
"build", "implement", "add", "replace", "fix", "create", "make",
|
||||
"write", "port", "migrate", "refactor", "integrate", "remove",
|
||||
"extend", "wire", "ship",
|
||||
}
|
||||
_DEMONSTRATIVES = {"that", "this", "it", "those", "these"}
|
||||
_OPTION_REFERENTS = re.compile(
|
||||
r"\b(first|second|third|other|latter|former)\s+(option|approach|one)\b")
|
||||
_QUESTION_MARKERS = re.compile(
|
||||
r"^\s*(how|what|why|when|where|who|which|should|could|would|can|"
|
||||
r"is|are|did|do you|do we|do they|does it|does this|does that)\b|"
|
||||
r"\?\s*$", re.IGNORECASE)
|
||||
_ANALYSIS_MARKERS = re.compile(
|
||||
r"\b(think|opinion|view|recommend|suggest|assess|evaluate|review|"
|
||||
r"compare|consider|worth it|worth it\?|needs? improvement|any "
|
||||
r"ideas?)\b", re.IGNORECASE)
|
||||
# relative-'that' detection by PRECEDING context (T18 18N fix): 'that'
|
||||
# is a relative pronoun when the word before it is an article/noun
|
||||
# ("the proposal that you suggested") — demonstrative when
|
||||
# sentence-initial or after a verb/punctuation ("build that" /
|
||||
# "that works for me"). The old following-word heuristic misclassified
|
||||
# "that works for me" as a relative clause.
|
||||
_ARTICLES = {"the", "a", "an", "this", "these", "those", "one"}
|
||||
|
||||
|
||||
def _is_relative_that(low: str, dem_tokens: set) -> bool:
|
||||
"""True when every 'that' token reads as a relative pronoun by its
|
||||
preceding context; demonstrative otherwise."""
|
||||
words = re.findall(r"[a-z']+", low)
|
||||
seen_that = False
|
||||
for i, w in enumerate(words):
|
||||
if w != "that":
|
||||
continue
|
||||
seen_that = True
|
||||
prev = words[i - 1] if i > 0 else None
|
||||
if prev is None or prev in _CONSTRUCTION_VERBS:
|
||||
return False # sentence-initial or post-verb → demonstrative
|
||||
if prev not in _ARTICLES:
|
||||
# preceding word is neither article nor verb: treat as
|
||||
# demonstrative unless it is clearly a noun modifier —
|
||||
# conservative default is demonstrative (referent-seeking)
|
||||
return False
|
||||
return seen_that and "that" in dem_tokens
|
||||
|
||||
_GO_AHEAD = re.compile(
|
||||
r"^\s*(yes|yeah|yep|ok|okay|sure|go ahead|please do|do it|do that|"
|
||||
r"carry on|proceed|approved|lgtm|ship it)\b[\s.,!]*$", re.IGNORECASE)
|
||||
_AFFIRMATION_OPENERS = re.compile(
|
||||
r"^\s*(yes|yeah|yep|yup|ok|okay|sure|agreed|absolutely|indeed|"
|
||||
r"alright|right)\b", re.IGNORECASE)
|
||||
_APPROVAL_IDIOM = re.compile(
|
||||
r"\b(go ahead|do it|do that|carry on|build that|implement that|"
|
||||
r"make it so|make it happen|make that happen|that's the one|go with|"
|
||||
r"let's go|let's do|let's have it|proceed|good with that|"
|
||||
r"fine with that|happy with that|on board|approved|ship it)\b",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def _tokens(text: str) -> set[str]:
|
||||
return set(re.split(r"[^a-z0-9]+", text.lower()))
|
||||
|
||||
|
||||
_NEGATION = re.compile(
|
||||
r"^\s*(don't|do not|dont|never|not yet|stop|hold off|no need|"
|
||||
r"skip it|forget it)\b", re.IGNORECASE)
|
||||
_IDIOM_PREFIX = re.compile(
|
||||
r"^\s*(go ahead and|go and|please|kindly|then|now)\s+", re.IGNORECASE)
|
||||
|
||||
|
||||
def _is_question(text: str) -> bool:
|
||||
"""Interrogative STRUCTURE, not vocabulary: leading wh/auxiliary or
|
||||
terminal '?'. 'How would you build this?' is a question regardless
|
||||
of the construction verb inside it (§17K)."""
|
||||
if "?" in text and not re.match(r"^\s*(build|implement|add|fix|make|"
|
||||
r"create|write|replace|remove)\b",
|
||||
text, re.IGNORECASE):
|
||||
return True
|
||||
return bool(_QUESTION_MARKERS.match(text))
|
||||
|
||||
|
||||
def _imperative_verb(text: str) -> str | None:
|
||||
"""Leading imperative construction verb — the strong/direct demand
|
||||
signal (§17K). 'Build me a program...' → 'build'. Idiom prefixes
|
||||
('go ahead and build X', 'please implement X') are stripped so the
|
||||
real verb surfaces."""
|
||||
t = text
|
||||
for _ in range(3):
|
||||
t2 = _IDIOM_PREFIX.sub("", t)
|
||||
if t2 == t:
|
||||
break
|
||||
t = t2
|
||||
m = re.match(r"^\s*(\w+)", t, re.IGNORECASE)
|
||||
if m and m.group(1).lower() in _CONSTRUCTION_VERBS:
|
||||
return m.group(1).lower()
|
||||
return None
|
||||
|
||||
|
||||
def resolve_construction_intent(
|
||||
text: str,
|
||||
context: ConversationContext,
|
||||
*,
|
||||
origin_context_id: str | None = None,
|
||||
) -> ConstructionIntent:
|
||||
"""Resolve one user turn into a structured ConstructionIntent.
|
||||
|
||||
Criteria (§17J), in precedence order:
|
||||
1. interrogative structure → DISCUSSION (never mutation); analysis
|
||||
of an improvement question may RECORD a proposal for later.
|
||||
2. bare approval ("go ahead") or demonstrative authorization
|
||||
("build that") → resolve referent against LIVE proposals,
|
||||
recency-first; multiple equally plausible → clarification;
|
||||
none live → clarification (stale-context resistant).
|
||||
3. explicit imperative with resolved target → USER_APPROVED
|
||||
direct demand — no unnecessary re-asking (§17K).
|
||||
4. everything else → DISCUSSION; construction language without
|
||||
resolved target → ambiguity flagged, never guessed (§17M).
|
||||
"""
|
||||
context.next_turn()
|
||||
context.record_turn("user", text)
|
||||
t = text.strip()
|
||||
low = t.lower()
|
||||
ev: list[str] = []
|
||||
probes: list[str] = []
|
||||
|
||||
# 0. NEGATION (§18N): "don't build that yet" blocks authorization —
|
||||
# construction language inside a negation frame is NOT a demand.
|
||||
if _NEGATION.match(t):
|
||||
ev.append("negation frame — explicitly blocks construction")
|
||||
return _intent(t, "DISCUSSION", "DISCUSSION", 0.85, "NONE",
|
||||
ev, probes, context, origin_context_id)
|
||||
|
||||
# 1a. REQUEST MODALS: "can you fix that?" is an imperative in
|
||||
# interrogative clothing — a state-changing action request, NOT an
|
||||
# information question. Information openers (how/what/why/should we)
|
||||
# stay discussion; request modals authorize when the target resolves.
|
||||
# information openers outrank modals: "how would you build X?" asks
|
||||
# about METHOD; "can you fix that?" requests ACTION
|
||||
information_opener = bool(re.match(
|
||||
r"^\s*(how|what|why|when|where|which)\b", low))
|
||||
request_modal = (not information_opener) and bool(re.search(
|
||||
r"\b(can you|could you|would you|will you|please)\b", low))
|
||||
if request_modal:
|
||||
# modal + a construction verb somewhere in the turn
|
||||
request_modal = bool(_tokens(low) & _CONSTRUCTION_VERBS)
|
||||
if request_modal:
|
||||
ev.append("request modal (can/could/would you, please) + "
|
||||
"construction verb → action request, not discussion")
|
||||
# a modal + demonstrative + live proposal authorizes THAT
|
||||
# proposal (referent resolution outranks target resolution)
|
||||
dem_tokens_m = _tokens(low) & _DEMONSTRATIVES
|
||||
if dem_tokens_m and context.live_proposals():
|
||||
referent = _resolve_referent(low, context)
|
||||
if referent and not referent.get("ambiguous"):
|
||||
tp = referent["proposal"]
|
||||
ev.append(f"modal authorization resolves proposal "
|
||||
f"{tp.proposal_id}")
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=tp.summary,
|
||||
requested_effect=f"construct: {tp.summary}",
|
||||
resolved_target=context.active_project,
|
||||
resolved_referent=tp.proposal_id,
|
||||
origin_context_id=origin_context_id,
|
||||
action_intent="ACTION", approval_state="USER_APPROVED",
|
||||
confidence=0.85, ambiguity="NONE",
|
||||
consequence_level=_consequence(t, context),
|
||||
relevant_constraints=_constraints(context),
|
||||
classification_evidence=ev)
|
||||
target = context.active_project
|
||||
if target:
|
||||
ev.append(f"target resolved from active context: {target}")
|
||||
# demonstrative referent: "fix that" with no live proposal —
|
||||
# the antecedent is the defect described in THIS turn
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=t, requested_effect=f"fix/act: {t}",
|
||||
resolved_target=target,
|
||||
resolved_referent="this-turn-topic",
|
||||
origin_context_id=origin_context_id,
|
||||
action_intent="ACTION", approval_state="USER_APPROVED",
|
||||
confidence=0.85, ambiguity="NONE",
|
||||
consequence_level=_consequence(t, context),
|
||||
relevant_constraints=_constraints(context),
|
||||
classification_evidence=ev)
|
||||
ev.append("request modal but no resolvable target")
|
||||
probes.append("where should that change land — which project?")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
|
||||
ev, probes, context, origin_context_id)
|
||||
|
||||
# 1. questions are discussion — even with construction verbs inside
|
||||
if _is_question(t):
|
||||
ev.append("interrogative structure → discussion, not authorization")
|
||||
analysis = bool(_ANALYSIS_MARKERS.search(t))
|
||||
if analysis:
|
||||
ev.append("analysis/advice request — a proposal may be recorded "
|
||||
"for later authorization")
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=t, requested_effect="analysis/discussion",
|
||||
resolved_target=context.active_project,
|
||||
resolved_referent=None, origin_context_id=origin_context_id,
|
||||
action_intent="DISCUSSION", approval_state="DISCUSSION",
|
||||
confidence=0.85, ambiguity="NONE",
|
||||
consequence_level="LOW", classification_evidence=ev)
|
||||
|
||||
# 2. authorization of a prior proposal (bare approval or demonstrative)
|
||||
bare_approval = bool(_GO_AHEAD.match(t))
|
||||
imperative = _imperative_verb(t)
|
||||
dem_tokens = _tokens(low) & _DEMONSTRATIVES
|
||||
if "that" in dem_tokens and _is_relative_that(low, dem_tokens):
|
||||
dem_tokens.discard("that") # relative clause, not a demonstrative
|
||||
ev.append("'that' is a relative pronoun here, not a referent")
|
||||
demonstrative = bool(dem_tokens) or \
|
||||
bool(_OPTION_REFERENTS.search(low))
|
||||
option_ref = bool(_OPTION_REFERENTS.search(low))
|
||||
approval_idiom = bool(_APPROVAL_IDIOM.search(low))
|
||||
affirmation = bool(_AFFIRMATION_OPENERS.match(t))
|
||||
authorized = (bare_approval or approval_idiom or
|
||||
(affirmation and (demonstrative or option_ref)) or
|
||||
(imperative and (demonstrative or option_ref)) or
|
||||
(option_ref and imperative is not None))
|
||||
|
||||
if authorized:
|
||||
referent = _resolve_referent(low, context)
|
||||
if referent is None:
|
||||
# "go ahead and build X": idiom + explicit imperative — fall
|
||||
# through to the demand path (target resolution) rather than
|
||||
# dead-ending in referent ambiguity
|
||||
if imperative is None:
|
||||
ev.append("authorization language but NO live resolvable "
|
||||
"referent — never guessed")
|
||||
probes.append("what should that refer to? I don't have an "
|
||||
"active proposal matching it.")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
|
||||
ev, probes, context, origin_context_id)
|
||||
ev.append("no live proposal matches; treating as direct "
|
||||
"demand (explicit imperative object present)")
|
||||
if referent is None:
|
||||
# live proposals exist but are ALL STALE (topic moved on) —
|
||||
# never approve a stale referent; fall to the demand path
|
||||
ev.append("proposals stale for this turn — not selected")
|
||||
if imperative is None:
|
||||
probes.append("what should that refer to? The earlier "
|
||||
"proposal no longer seems active.")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
|
||||
ev, probes, context, origin_context_id)
|
||||
ev.append("stale proposals ignored; explicit imperative → "
|
||||
"demand path")
|
||||
referent = {"__fallthrough__": True}
|
||||
if referent.get("ambiguous"):
|
||||
ev.append("multiple equally plausible referents — asking, "
|
||||
"not guessing (§17M)")
|
||||
probes.append("which one — " + " or ".join(
|
||||
p.summary[:40] for p in referent["candidates"]) + "?")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
|
||||
ev, probes, context, origin_context_id)
|
||||
if referent.get("__fallthrough__"):
|
||||
# demand path: target from context or clarify (never stale)
|
||||
target = context.active_project
|
||||
if not target and imperative in _CREATION_VERBS and \
|
||||
_NEW_ARTICLE.search(low):
|
||||
# new standalone artifact demand — no target needed
|
||||
ev.append("creation verb + indefinite article → new "
|
||||
"standalone artifact; no existing target required")
|
||||
return _intent(t, "ACTION", "USER_APPROVED", 0.85, "NONE",
|
||||
ev, probes, context, origin_context_id,
|
||||
requested_effect=f"construct new: {t}",
|
||||
consequence_level=_consequence(t, context))
|
||||
if target:
|
||||
ev.append(f"target resolved from active context: {target}")
|
||||
return _intent(t, "ACTION", "USER_APPROVED", 0.8, "NONE",
|
||||
ev, probes, context, origin_context_id,
|
||||
requested_effect=f"construct: {t}",
|
||||
consequence_level=_consequence(t, context))
|
||||
probes.append("build where — which project/system?")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
|
||||
ev, probes, context, origin_context_id)
|
||||
target_prop = referent["proposal"]
|
||||
ev.append(f"referent resolved: {target_prop.proposal_id} "
|
||||
f"(recency-first among live proposals)")
|
||||
ev.append("prior proposal + explicit authorization → USER_APPROVED")
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=target_prop.summary,
|
||||
requested_effect=f"construct: {target_prop.summary}",
|
||||
resolved_target=context.active_project,
|
||||
resolved_referent=target_prop.proposal_id,
|
||||
origin_context_id=origin_context_id,
|
||||
action_intent="ACTION", approval_state="USER_APPROVED",
|
||||
confidence=0.9, ambiguity="NONE",
|
||||
consequence_level=_consequence(t, context),
|
||||
relevant_constraints=_constraints(context),
|
||||
classification_evidence=ev)
|
||||
|
||||
# 3. explicit imperative construction demand with resolved target
|
||||
if imperative:
|
||||
# non-constructible object check is Scaffolding's classifier's
|
||||
# job (§17O) — this layer records the demand structure only
|
||||
ev.append(f"imperative construction verb: {imperative} — strong/"
|
||||
f"direct demand signal (§17K)")
|
||||
target = context.active_project
|
||||
if target:
|
||||
ev.append(f"target resolved from active context: {target}")
|
||||
return _intent(t, "ACTION", "USER_APPROVED", 0.9, "NONE",
|
||||
ev, probes, context, origin_context_id,
|
||||
requested_effect=f"construct: {t}",
|
||||
consequence_level=_consequence(t, context))
|
||||
# no active project: a CREATION verb + indefinite article signals
|
||||
# a NEW standalone artifact — the demand is complete without an
|
||||
# existing target ("build me a program that...")
|
||||
if imperative in _CREATION_VERBS and _NEW_ARTICLE.search(low):
|
||||
ev.append("creation verb + indefinite article → new standalone "
|
||||
"artifact demand; no existing target required")
|
||||
return _intent(t, "ACTION", "USER_APPROVED", 0.85, "NONE",
|
||||
ev, probes, context, origin_context_id,
|
||||
requested_effect=f"construct new: {t}")
|
||||
ev.append("imperative demand but no resolvable target — "
|
||||
"clarification, never guessing")
|
||||
probes.append("build where — which project/system?")
|
||||
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
|
||||
ev, probes, context, origin_context_id)
|
||||
|
||||
# 4. default: discussion / context statement
|
||||
ev.append("no authorization structure, no imperative demand → "
|
||||
"discussion")
|
||||
return _intent(t, "DISCUSSION", "DISCUSSION", 0.7, "NONE",
|
||||
ev, probes, context, origin_context_id)
|
||||
|
||||
|
||||
def _resolve_referent(low: str, context: ConversationContext) -> dict | None:
|
||||
"""Recency-first referent resolution over LIVE proposals (§17M).
|
||||
Stale-context-resistant: proposals older than STALE_GAP_TURNS are
|
||||
ignored unless the user's text explicitly references their content.
|
||||
Returns {"proposal": Proposal} or {"ambiguous": True,
|
||||
"candidates": [...]} or None."""
|
||||
live = context.live_proposals()
|
||||
if not live:
|
||||
return None
|
||||
fresh = [p for p in live
|
||||
if context.turn - p.created_turn <= STALE_GAP_TURNS
|
||||
or _references(p.summary, low)]
|
||||
if not fresh:
|
||||
return None # all live proposals are stale for this turn
|
||||
live = fresh
|
||||
# option-selectors pick among live proposals by ordinal
|
||||
m = _OPTION_REFERENTS.search(low)
|
||||
if m:
|
||||
ordinals = {"first": 0, "second": 1, "third": 2, "other": 1,
|
||||
"latter": len(live) - 1, "former": 0}
|
||||
idx = ordinals.get(m.group(1).lower())
|
||||
if idx is not None and idx < len(live):
|
||||
return {"proposal": live[idx]}
|
||||
if len(live) > 1:
|
||||
return {"ambiguous": True, "candidates": live}
|
||||
# multiple live proposals + no ordinal → ambiguous unless recency
|
||||
# is decisive (only one proposal younger than the last topic shift)
|
||||
if len(live) > 1:
|
||||
# recency heuristic: if the newest proposal is at least 2 turns
|
||||
# newer than the runner-up, recency decides; otherwise ask
|
||||
newest, runner = live[-1], live[-2]
|
||||
if newest.created_turn - runner.created_turn >= 2:
|
||||
return {"proposal": newest}
|
||||
return {"ambiguous": True, "candidates": live}
|
||||
return {"proposal": live[0]}
|
||||
|
||||
|
||||
def _references(summary: str, low: str) -> bool:
|
||||
"""Does the user's text explicitly reference the proposal's content?
|
||||
Distinctive content words (len>4) appearing in the text revive a
|
||||
stale proposal (explicit > recency)."""
|
||||
words = {w for w in _tokens(summary) if len(w) > 4}
|
||||
return bool(words & set(_tokens(low)))
|
||||
|
||||
|
||||
def _consequence(text: str, context: ConversationContext) -> str:
|
||||
low = text.lower()
|
||||
if any(w in low for w in ("replace", "migrate", "rewrite", "delete",
|
||||
"remove all", "subsystem")):
|
||||
return "HIGH"
|
||||
if any(w in low for w in ("add", "extend", "port", "integrate")):
|
||||
return "MEDIUM"
|
||||
return "LOW"
|
||||
|
||||
|
||||
def _constraints(context: ConversationContext) -> list:
|
||||
"""User/project constraints recorded in recent turns (light capture;
|
||||
the constraint STORE is future Substrate's — §17O boundary)."""
|
||||
out = []
|
||||
for turn in context.recent_turns[-6:]:
|
||||
txt = turn.get("text", "").lower()
|
||||
if "must not" in txt or "don't" in txt or "never" in txt or \
|
||||
"no hardcoded" in txt:
|
||||
out.append(turn["text"][:120])
|
||||
return out
|
||||
|
||||
|
||||
def _intent(objective: str, action_intent: str, approval_state: str,
|
||||
confidence: float, ambiguity: str, ev: list, probes: list,
|
||||
context: ConversationContext, origin_context_id: str | None,
|
||||
requested_effect: str = "",
|
||||
consequence_level: str | None = None) -> ConstructionIntent:
|
||||
return ConstructionIntent(
|
||||
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
|
||||
objective=objective,
|
||||
requested_effect=requested_effect or objective,
|
||||
resolved_target=context.active_project,
|
||||
resolved_referent=None,
|
||||
origin_context_id=origin_context_id,
|
||||
action_intent=action_intent,
|
||||
approval_state=approval_state,
|
||||
confidence=confidence, ambiguity=ambiguity,
|
||||
consequence_level=consequence_level
|
||||
or ("LOW" if action_intent == "DISCUSSION" else "MEDIUM"),
|
||||
relevant_constraints=_constraints(context),
|
||||
classification_evidence=ev, probe_questions=probes)
|
||||
@@ -293,9 +293,32 @@ def classify_mode(
|
||||
file_scoped = True
|
||||
break
|
||||
|
||||
# T17 §17Q: indefinite-article new-artifact evidence — "a small CLI
|
||||
# that renames photos", "build me a program that..." — is
|
||||
# new-lifecycle language even without explicit standalone keywords.
|
||||
# Guarded: requires a CREATION verb or artifact noun, and yields to
|
||||
# local/scope/file evidence (fix a bug / add a flag never counts).
|
||||
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
|
||||
_ARTIFACT_NOUNS = {"program", "cli", "tool", "app", "script", "service",
|
||||
"library", "package", "module", "dashboard",
|
||||
"website", "bot", "generator", "tracker", "game",
|
||||
"application", "command"}
|
||||
article_standalone = False
|
||||
if re.search(r"\b(a|an|another)\s+[a-z]", obj.lower()):
|
||||
creation_verb = bool(_tokens(obj.lower()) & _CREATION_VERBS)
|
||||
artifact_noun = bool(_tokens(obj.lower()) & _ARTIFACT_NOUNS)
|
||||
# structural evidence outranks lexical newness (§5): a known
|
||||
# existing target means article language describes a CHANGE
|
||||
# ("add a flag to the deploy script"), not a new project
|
||||
if (creation_verb or artifact_noun) and not local and not scoped \
|
||||
and not file_scoped and not target_project \
|
||||
and project_exists is not True:
|
||||
article_standalone = True
|
||||
ev.append("indefinite-article new-artifact language")
|
||||
|
||||
# 3. PROJECT_BUILD: standalone/new-lifecycle language AND no existing
|
||||
# project for the target (project existence materially matters, §5)
|
||||
if standalone:
|
||||
if standalone or article_standalone:
|
||||
ev.append(f"standalone/new-lifecycle language: {standalone[:3]}")
|
||||
if project_exists is True:
|
||||
# §5: existing project preferred unless evidence says otherwise —
|
||||
|
||||
@@ -71,3 +71,48 @@ def reconcile(claims: list[dict], observer: Callable[[dict], dict]) -> dict:
|
||||
blocking = [d for d in drifts if d["class"] in BLOCKING_CLASSES]
|
||||
return {"resume": not blocking, "drifts": drifts,
|
||||
"blocking": blocking}
|
||||
|
||||
|
||||
# ── T18 §18H: structural drift detection (extends drift_resume; no
|
||||
# duplicate mechanism). Detects observable drift between a structural
|
||||
# model's claims and repository reality. REALITY WINS unless user-locked.
|
||||
|
||||
def detect_structural_drift(model, repo_root) -> list[dict]:
|
||||
"""Compare structural-model component claims against source reality.
|
||||
Observable cases: source file moved/missing (component identities
|
||||
carry 'source'), entities for files that no longer exist. Returns
|
||||
drift records in the classify_drift vocabulary; reality wins unless
|
||||
the claim is user-locked (authority == 'user' on the entity's
|
||||
evidence or identities.user_locked)."""
|
||||
import os
|
||||
from pathlib import Path as _P
|
||||
root = _P(repo_root)
|
||||
drifts = []
|
||||
for e in model.doc["entities"]:
|
||||
if e.get("kind") != "component":
|
||||
continue
|
||||
src = ((e.get("ext") or {}).get("source")
|
||||
or (e.get("identities") or {}).get("source"))
|
||||
if not src:
|
||||
continue
|
||||
locked = (e.get("identities") or {}).get("user_locked") is True
|
||||
expected = root / src
|
||||
if not expected.exists():
|
||||
# reality: file gone/moved — search one level for a moved twin
|
||||
moved = None
|
||||
if expected.name:
|
||||
for cand in root.rglob(expected.name):
|
||||
if cand.is_file():
|
||||
moved = str(cand.relative_to(root))
|
||||
break
|
||||
drifts.append({
|
||||
"claim": f"{e['id']} source at {src}",
|
||||
"class": "CONFLICTING" if locked else "MATERIAL",
|
||||
"evidence": (f"source missing; moved to {moved}"
|
||||
if moved else "source file absent from repo"),
|
||||
"reality_wins": not locked,
|
||||
"reconciliation": (f"update source to {moved}" if moved
|
||||
else "entity requires re-derivation or "
|
||||
"user-locked preservation"),
|
||||
})
|
||||
return drifts
|
||||
@@ -95,9 +95,24 @@ for attempt in range(3):
|
||||
|
||||
|
||||
def wr_root() -> Path:
|
||||
"""Resolve the bridge DATA root. T17 17T finding: two live roots
|
||||
existed (the shared /mnt/workdrive/wr-bridge and the legacy
|
||||
~/.local/share/mimo-wait-resume) and an unpinned consumer silently
|
||||
read the stale one. Policy now: explicit env → shared root if it
|
||||
exists (the canonical production root) → legacy home (warn loudly —
|
||||
a stale-store split-brain must be visible, never silent)."""
|
||||
import sys as _sys
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
shared = Path("/mnt/workdrive/wr-bridge")
|
||||
if shared.exists():
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
_sys.stderr.write(
|
||||
"event_wait: RUNE_WR_ROOT unset — using shared bridge "
|
||||
f"root {shared} while legacy root {DEFAULT_WR_ROOT} also "
|
||||
"exists; pin RUNE_WR_ROOT to silence\n")
|
||||
return shared
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
return DEFAULT_WR_ROOT
|
||||
return FALLBACK_WR_ROOT
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""forge.py — T18 §18L: minimum stable forge abstraction boundary.
|
||||
|
||||
Rune today is Gitea-centric; Scaffolding must not make Gitea itself part
|
||||
of canonical construction semantics. This module defines the boundary:
|
||||
Git remains the source-history authority; a Forge adapter handles the
|
||||
remote-services surface (push/CI status/PR/protected checks/release
|
||||
metadata). GiteaForge is the real working adapter, delegating to the
|
||||
existing gitea_auth + scripts (rule A: no duplicate implementation).
|
||||
|
||||
A future Forgejo/GitHub/GitLab adapter implements the same interface
|
||||
without changing Scaffolding's core model.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class ForgeAdapter(Protocol):
|
||||
"""The minimum stable forge boundary. All methods may raise
|
||||
ForgeUnavailable — callers treat forge failures as blocking-for-that-
|
||||
operation, never as construction-truth changes."""
|
||||
|
||||
def push(self, repo: str, branch: str) -> dict: ...
|
||||
|
||||
def commit_status(self, repo: str, sha: str) -> str: ...
|
||||
|
||||
def ci_runs_for_sha(self, repo: str, sha: str) -> list[dict]: ...
|
||||
|
||||
def create_or_patch_release(self, repo: str, tag: str,
|
||||
title: str, body: str) -> dict: ...
|
||||
|
||||
|
||||
class ForgeUnavailable(RuntimeError):
|
||||
"""The forge could not be reached or refused the operation."""
|
||||
|
||||
|
||||
class GiteaForge:
|
||||
"""Real adapter. Delegates to the existing canonical implementations
|
||||
(gitea_auth credential ladder; deploy_qualify verdict logic) — no
|
||||
duplicated API clients (rule A)."""
|
||||
|
||||
def __init__(self, server: str | None = None, repo: str = "conductor/rune"):
|
||||
self._server = server # None → scripts' env/credential resolution
|
||||
self._repo = repo
|
||||
|
||||
def _env(self) -> dict:
|
||||
import os
|
||||
env = dict(os.environ)
|
||||
if self._server:
|
||||
env["GITEA_SERVER"] = self._server
|
||||
env.setdefault("GITEA_REPO", self._repo)
|
||||
return env
|
||||
|
||||
def push(self, repo: str, branch: str) -> dict:
|
||||
import subprocess
|
||||
proc = subprocess.run(["git", "push", "gitea", f"master:{branch}"],
|
||||
capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise ForgeUnavailable(f"push failed: {proc.stderr[:200]}")
|
||||
return {"pushed": True, "branch": branch}
|
||||
|
||||
@staticmethod
|
||||
def _gitea_auth():
|
||||
"""Canonical scripts/gitea_auth credential ladder (rule A: no
|
||||
duplicate auth). scripts/ is not a package — path shim, the
|
||||
established spec-loaded-scripts pattern."""
|
||||
import sys
|
||||
from pathlib import Path as _P
|
||||
scripts = _P(__file__).resolve().parents[2] / "scripts"
|
||||
if str(scripts) not in sys.path:
|
||||
sys.path.insert(0, str(scripts))
|
||||
import gitea_auth
|
||||
return gitea_auth
|
||||
|
||||
def commit_status(self, repo: str, sha: str) -> str:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
gitea_auth = self._gitea_auth()
|
||||
server = self._server or os.environ.get(
|
||||
"GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
if not gitea_auth.has_token():
|
||||
raise ForgeUnavailable("no forge credentials (gitea token)")
|
||||
req = urllib.request.Request(
|
||||
f"{server}/api/v1/repos/{repo}/commits/{sha}/status",
|
||||
headers={"Authorization": f"token {gitea_auth.get_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
data = json.loads(r.read())
|
||||
except OSError as exc:
|
||||
raise ForgeUnavailable(f"status unreachable: {exc}") from exc
|
||||
return data.get("state", "unknown")
|
||||
|
||||
def ci_runs_for_sha(self, repo: str, sha: str) -> list[dict]:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
gitea_auth = self._gitea_auth()
|
||||
server = self._server or os.environ.get(
|
||||
"GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
if not gitea_auth.has_token():
|
||||
raise ForgeUnavailable("no forge credentials (gitea token)")
|
||||
req = urllib.request.Request(
|
||||
f"{server}/api/v1/repos/{repo}/actions/runs?limit=50",
|
||||
headers={"Authorization": f"token {gitea_auth.get_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
data = json.loads(r.read())
|
||||
except OSError as exc:
|
||||
raise ForgeUnavailable(f"runs unreachable: {exc}") from exc
|
||||
return [run for run in data.get("workflow_runs", [])
|
||||
if str(run.get("head_sha", "")) == sha]
|
||||
|
||||
def create_or_patch_release(self, repo: str, tag: str,
|
||||
title: str, body: str) -> dict:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
gitea_auth = self._gitea_auth()
|
||||
server = self._server or os.environ.get(
|
||||
"GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
if not gitea_auth.has_token():
|
||||
raise ForgeUnavailable("no forge credentials (gitea token)")
|
||||
# delegate to the canonical idempotent publisher
|
||||
import subprocess
|
||||
import sys
|
||||
script = None
|
||||
for cand in ("scripts/create_release.py",):
|
||||
if (sys.path and __import__("pathlib").Path(cand).is_file()):
|
||||
script = cand
|
||||
break
|
||||
if script:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, script, "--tag", tag, "--title", title,
|
||||
"--body", body], capture_output=True, text=True)
|
||||
return {"rc": proc.returncode, "stdout": proc.stdout[-400:]}
|
||||
raise ForgeUnavailable("canonical release publisher not found")
|
||||
|
||||
|
||||
def default_forge() -> ForgeAdapter:
|
||||
"""The working adapter (rule 18L: prove Gitea still works)."""
|
||||
return GiteaForge()
|
||||
@@ -0,0 +1,108 @@
|
||||
"""graph_projection.py — T18 §18Q: stable versioned READ-ONLY graph
|
||||
projection from a structural Scaffold, suitable for later local-web
|
||||
visualisation, GUI, TUI tree, or programmatic queries.
|
||||
|
||||
This is a PROJECTION — never another authority (rule 18Q). Structural
|
||||
truth remains the Scaffold (structural.json) / Spine. Built THROUGH the
|
||||
self-scaffolded upgrade (contract 18q-graph-projection): the upgrade
|
||||
started from the maintained Scaffold, per the 18P proof.
|
||||
|
||||
Formats: versioned JSON (programmatic), DOT and Mermaid (visual —
|
||||
dependency-cruiser-proven human-usable formats per the 18D research).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
PROJECTION_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def project_json(model) -> dict:
|
||||
"""Stable versioned JSON projection: nodes + edges, no internal
|
||||
storage layout leaked (Substrate-safe seam, rule 18E)."""
|
||||
nodes = []
|
||||
for e in model.doc["entities"]:
|
||||
nodes.append({
|
||||
"id": e["id"],
|
||||
"kind": e.get("kind"),
|
||||
"maturity": e.get("maturity"),
|
||||
"name": (e.get("identities") or {}).get("name"),
|
||||
"ext": {k: v for k, v in (e.get("ext") or {}).items()
|
||||
if k in ("source", "bounded_adapter")},
|
||||
})
|
||||
edges = []
|
||||
for r in model.doc["relationships"]:
|
||||
edges.append({
|
||||
"type": r.get("type"),
|
||||
"from": r.get("from"),
|
||||
"to": r.get("to"),
|
||||
"authority": r.get("authority"),
|
||||
"status": r.get("status"),
|
||||
})
|
||||
return {
|
||||
"schema_version": PROJECTION_SCHEMA_VERSION,
|
||||
"project": getattr(model, "project_id", ""),
|
||||
"generated_at": _now(),
|
||||
"node_count": len(nodes),
|
||||
"edge_count": len(edges),
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
}
|
||||
|
||||
|
||||
def project_dot(model) -> str:
|
||||
"""Graphviz DOT — clusters per domain, edges labeled by type."""
|
||||
lines = ["digraph structural {", " rankdir=LR;",
|
||||
' node [shape=box, style="rounded,filled", fillcolor="#eef"];']
|
||||
for e in model.doc["entities"]:
|
||||
eid = e["id"].replace('"', "'")
|
||||
label = (e.get("identities") or {}).get("name") or \
|
||||
e["id"].split(":", 1)[-1]
|
||||
lines.append(f' "{eid}" [label="{label} ({e["kind"]})"];')
|
||||
for r in model.doc["relationships"]:
|
||||
if r["type"] == "belongs_to" and r.get("status") == "root":
|
||||
continue
|
||||
a = str(r["from"]).replace('"', "'")
|
||||
b = str(r["to"]).replace('"', "'")
|
||||
lines.append(f' "{a}" -> "{b}" [label="{r["type"]}"];')
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def project_mermaid(model) -> str:
|
||||
"""Mermaid flowchart — renders on GitHub/GitLab/many viewers."""
|
||||
lines = ["graph LR"]
|
||||
ids = {e["id"] for e in model.doc["entities"]}
|
||||
safe = {}
|
||||
for i, eid in enumerate(sorted(ids)):
|
||||
safe[eid] = f"n{i}"
|
||||
for e in model.doc["entities"]:
|
||||
label = ((e.get("identities") or {}).get("name")
|
||||
or e["id"].split(":", 1)[-1]).replace('"', "'")
|
||||
lines.append(f' {safe[e["id"]]}["{label} ({e["kind"]})"]')
|
||||
for r in model.doc["relationships"]:
|
||||
if r["type"] == "belongs_to" and r.get("status") == "root":
|
||||
continue
|
||||
if r["from"] in safe and r["to"] in safe:
|
||||
lines.append(f' {safe[r["from"]]} -->|{r["type"]}| '
|
||||
f'{safe[r["to"]]}')
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_all(model, out_dir) -> dict:
|
||||
"""Write all three formats; returns paths. Read-only over truth —
|
||||
writing projections never mutates the model."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
out = Path(out_dir)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
j = out / "structural_graph.json"
|
||||
d = out / "structural_graph.dot"
|
||||
mm = out / "structural_graph.mmd"
|
||||
j.write_text(json.dumps(project_json(model), indent=1, sort_keys=True))
|
||||
d.write_text(project_dot(model))
|
||||
mm.write_text(project_mermaid(model))
|
||||
return {"json": str(j), "dot": str(d), "mermaid": str(mm)}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
import time
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
@@ -0,0 +1,69 @@
|
||||
"""impact_preview.py — T18 §18G: visible change-impact preview.
|
||||
|
||||
For consequential changes, a user-visible STRUCTURED projection of what
|
||||
a construction will touch — BEFORE build. Derived from the structural
|
||||
Scaffold's impact closure + the Build Basis summary. Contains
|
||||
structured conclusions/evidence only — NEVER private chain-of-thought
|
||||
(rule 18G). This is a projection, not an authority.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
PREVIEW_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def build_preview(model, closure: dict, *, basis: dict | None = None,
|
||||
contract_scope: list[str] | None = None) -> dict:
|
||||
"""Assemble the preview from an impact_closure (structural_model)
|
||||
and optionally the Build Basis summary. Pure derivation — no hidden
|
||||
reasoning, no mutation."""
|
||||
if closure.get("error"):
|
||||
return {"schema_version": PREVIEW_SCHEMA_VERSION,
|
||||
"error": closure["error"]}
|
||||
scope = set(closure.get("mutation_scope", []))
|
||||
observation = closure.get("observation_scope") or {}
|
||||
deps = observation.get("direct_dependencies", [])
|
||||
# candidate files: identities' source paths if modeled
|
||||
candidate_files = []
|
||||
for eid in sorted(scope):
|
||||
e = model.get(eid) or {}
|
||||
src = (e.get("identities") or {}).get("source")
|
||||
if src:
|
||||
candidate_files.append(src)
|
||||
return {
|
||||
"schema_version": PREVIEW_SCHEMA_VERSION,
|
||||
"target": closure.get("target"),
|
||||
"affected_domains": ([closure.get("owning_domain")]
|
||||
if closure.get("owning_domain") else []),
|
||||
"affected_subsystems": sorted(
|
||||
e for e in scope if str(e).startswith("subsystem:")),
|
||||
"consumers_connectors": sorted(
|
||||
e for e in scope if e != closure.get("target")),
|
||||
"candidate_files": sorted(set(candidate_files))[:50],
|
||||
"contract_scope_additions": sorted(
|
||||
set(contract_scope or []) - set(candidate_files)),
|
||||
"state_implications": closure.get("state_surfaces", []),
|
||||
"environment_runtime_implications": sorted(
|
||||
model.deployment_surfaces(closure["target"])
|
||||
if model.get(closure["target"]) else []),
|
||||
"migration_implications": (basis or {}).get("implications",
|
||||
{}).get("migration", []),
|
||||
"risks_uncertainties": [
|
||||
*(basis or {}).get("risks", []),
|
||||
*closure.get("uncertainty", []),
|
||||
],
|
||||
"verification_plan": closure.get("verification_scope", []),
|
||||
"rollback_implications": [
|
||||
"revert contract scope commits; deployment rollbacks via "
|
||||
"last-good symlink (canonical deploy machinery)"],
|
||||
"basis_summary": (basis or {}).get("depth"),
|
||||
}
|
||||
|
||||
|
||||
def validate_preview(preview: dict) -> list[str]:
|
||||
errs = []
|
||||
if preview.get("schema_version") != PREVIEW_SCHEMA_VERSION:
|
||||
errs.append("schema_version mismatch")
|
||||
for f in ("target", "verification_plan", "risks_uncertainties"):
|
||||
if f not in preview:
|
||||
errs.append(f"missing {f}")
|
||||
return errs
|
||||
@@ -241,6 +241,13 @@ class TaskStateRepository:
|
||||
with self._lock:
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
def list_all(self) -> list[TaskStateRecord]:
|
||||
"""Every tracked task record (T17 17F: user-facing counters and
|
||||
listings derive from THIS authority, not the legacy operator
|
||||
cache)."""
|
||||
with self._lock:
|
||||
return list(self._tasks.values())
|
||||
|
||||
def list_open(self, channel_id: str) -> list[TaskStateRecord]:
|
||||
with self._lock:
|
||||
return sorted(self._open_in_channel(channel_id), key=lambda t: t.updated_at)
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Mode Plugin — State machine orchestrator.
|
||||
|
||||
Each phase is an independent module. The state machine coordinates:
|
||||
1. Read current state from checkpoint file
|
||||
2. Execute the current phase's module
|
||||
3. Write output to checkpoint file
|
||||
4. Advance to next phase
|
||||
5. Repeat
|
||||
|
||||
Key insight from Gauntlet: "Checkpoint files on disk. Reports generated
|
||||
from checkpoints, not memory." If the conversation dies, the next session
|
||||
resumes from the last checkpoint. No drift because state is on disk.
|
||||
|
||||
Architecture:
|
||||
- Each phase is a separate module in phases/
|
||||
- State machine reads checkpoint.json to know current phase
|
||||
- Each phase writes its output to checkpoint.json
|
||||
- Cron tasks fire each phase independently
|
||||
- Ledger tracks progress for resumability
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CONFIGURATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PHASES_DIR = os.path.join(PLUGIN_DIR, "phases")
|
||||
BUILDS_DIR = os.path.expanduser("~/.rune/work/builds")
|
||||
WORKTREE_DIR = os.path.expanduser("~/.rune/worktrees")
|
||||
REPO_DIR = "/mnt/workdrive/rune"
|
||||
|
||||
# Phase execution order
|
||||
PHASE_ORDER = [
|
||||
"assess",
|
||||
"research",
|
||||
"feasible",
|
||||
"consult",
|
||||
"consent",
|
||||
"plan",
|
||||
"build",
|
||||
"verify",
|
||||
"correct",
|
||||
"integrate",
|
||||
"record",
|
||||
]
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# STATE MACHINE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class BuildStateMachine:
|
||||
"""State machine for build mode. Reads/writes checkpoint files."""
|
||||
|
||||
def __init__(self, build_id: str):
|
||||
self.build_id = build_id
|
||||
self.build_dir = os.path.join(BUILDS_DIR, build_id)
|
||||
self.checkpoint_path = os.path.join(self.build_dir, "checkpoint.json")
|
||||
self.ledger_path = os.path.join(self.build_dir, "ledger.json")
|
||||
self.trajectory_path = os.path.join(self.build_dir, "trajectory.json")
|
||||
|
||||
os.makedirs(self.build_dir, exist_ok=True)
|
||||
|
||||
def load_checkpoint(self) -> Dict[str, Any]:
|
||||
"""Load current state from checkpoint file."""
|
||||
if os.path.exists(self.checkpoint_path):
|
||||
with open(self.checkpoint_path) as f:
|
||||
return json.load(f)
|
||||
return self._initial_checkpoint()
|
||||
|
||||
def save_checkpoint(self, checkpoint: Dict[str, Any]):
|
||||
"""Save state to checkpoint file."""
|
||||
checkpoint["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
with open(self.checkpoint_path, "w") as f:
|
||||
json.dump(checkpoint, f, indent=2)
|
||||
|
||||
def _initial_checkpoint(self) -> Dict[str, Any]:
|
||||
"""Create initial checkpoint."""
|
||||
return {
|
||||
"build_id": self.build_id,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"current_phase": "assess",
|
||||
"phase_status": "pending",
|
||||
"phases": {},
|
||||
"description": "",
|
||||
"worktree_path": None,
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"batches": [],
|
||||
"corrections": 0,
|
||||
"max_corrections": 3,
|
||||
"files_changed": [],
|
||||
"contracts": [],
|
||||
"failure_patterns": [],
|
||||
}
|
||||
|
||||
def get_current_phase(self) -> str:
|
||||
"""Get current phase from checkpoint."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
return checkpoint.get("current_phase", "assess")
|
||||
|
||||
def get_phase_status(self) -> str:
|
||||
"""Get current phase status from checkpoint."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
return checkpoint.get("phase_status", "pending")
|
||||
|
||||
def advance_phase(self):
|
||||
"""Advance to next phase."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
current = checkpoint["current_phase"]
|
||||
|
||||
# Mark current phase as complete
|
||||
checkpoint["phases"][current] = "complete"
|
||||
|
||||
# Find next phase
|
||||
try:
|
||||
idx = PHASE_ORDER.index(current)
|
||||
if idx + 1 < len(PHASE_ORDER):
|
||||
next_phase = PHASE_ORDER[idx + 1]
|
||||
checkpoint["current_phase"] = next_phase
|
||||
checkpoint["phase_status"] = "pending"
|
||||
else:
|
||||
checkpoint["phase_status"] = "complete"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.save_checkpoint(checkpoint)
|
||||
|
||||
def set_phase_status(self, status: str, details: Dict[str, Any] = None):
|
||||
"""Set current phase status."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
checkpoint["phase_status"] = status
|
||||
if details:
|
||||
checkpoint.update(details)
|
||||
self.save_checkpoint(checkpoint)
|
||||
|
||||
def log_to_ledger(self, entry: Dict[str, Any]):
|
||||
"""Append entry to ledger."""
|
||||
ledger = []
|
||||
if os.path.exists(self.ledger_path):
|
||||
with open(self.ledger_path) as f:
|
||||
ledger = json.load(f)
|
||||
|
||||
entry["timestamp"] = datetime.now(timezone.utc).isoformat()
|
||||
ledger.append(entry)
|
||||
|
||||
with open(self.ledger_path, "w") as f:
|
||||
json.dump(ledger, f, indent=2)
|
||||
|
||||
def log_trajectory(self, action: str, details: Dict[str, Any] = None):
|
||||
"""Append to trajectory log (like SWE-agent .traj files)."""
|
||||
trajectory = []
|
||||
if os.path.exists(self.trajectory_path):
|
||||
with open(self.trajectory_path) as f:
|
||||
trajectory = json.load(f)
|
||||
|
||||
entry = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"action": action,
|
||||
"details": details or {},
|
||||
}
|
||||
trajectory.append(entry)
|
||||
|
||||
with open(self.trajectory_path, "w") as f:
|
||||
json.dump(trajectory, f, indent=2)
|
||||
|
||||
def get_resume_point(self) -> Optional[str]:
|
||||
"""Get phase to resume from."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
current = checkpoint.get("current_phase")
|
||||
status = checkpoint.get("phase_status")
|
||||
|
||||
if status == "complete":
|
||||
# All phases complete
|
||||
return None
|
||||
|
||||
return current
|
||||
|
||||
def can_resume(self) -> bool:
|
||||
"""Check if build can be resumed."""
|
||||
return self.get_resume_point() is not None
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get build status."""
|
||||
checkpoint = self.load_checkpoint()
|
||||
return {
|
||||
"build_id": self.build_id,
|
||||
"description": checkpoint.get("description", ""),
|
||||
"current_phase": checkpoint.get("current_phase", "assess"),
|
||||
"phase_status": checkpoint.get("phase_status", "pending"),
|
||||
"phases": checkpoint.get("phases", {}),
|
||||
"batches": len(checkpoint.get("batches", [])),
|
||||
"corrections": checkpoint.get("corrections", 0),
|
||||
"worktree": checkpoint.get("worktree_path"),
|
||||
"complexity_score": checkpoint.get("complexity_score", 0),
|
||||
"estimated_steps": checkpoint.get("estimated_steps", 0),
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE EXECUTION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def execute_phase(build_id: str, phase: str = None) -> Dict[str, Any]:
|
||||
"""Execute a phase. If phase is None, execute current phase."""
|
||||
sm = BuildStateMachine(build_id)
|
||||
|
||||
if phase is None:
|
||||
phase = sm.get_current_phase()
|
||||
|
||||
# Check if phase module exists
|
||||
phase_module_path = os.path.join(PHASES_DIR, f"{phase}.py")
|
||||
if not os.path.exists(phase_module_path):
|
||||
return {"success": False, "error": f"Phase module not found: {phase}"}
|
||||
|
||||
# Import and execute phase module
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location(f"phase_{phase}", phase_module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Execute phase
|
||||
result = module.execute(sm)
|
||||
|
||||
# Log to trajectory
|
||||
sm.log_trajectory(f"phase_{phase}", {"result": result})
|
||||
|
||||
# Advance to next phase if successful
|
||||
if result.get("success"):
|
||||
sm.advance_phase()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
sm.log_trajectory(f"phase_{phase}_error", {"error": str(e)})
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: build_mode_plugin.py [start|status|resume|execute|abort]")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "start":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py start <description>")
|
||||
sys.exit(1)
|
||||
description = " ".join(sys.argv[2:])
|
||||
build_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
sm = BuildStateMachine(build_id)
|
||||
checkpoint = sm.load_checkpoint()
|
||||
checkpoint["description"] = description
|
||||
sm.save_checkpoint(checkpoint)
|
||||
sm.log_to_ledger({"action": "build_started", "description": description})
|
||||
print(f"✅ Build {build_id} started")
|
||||
print(f" Description: {description}")
|
||||
print(f" Checkpoint: {sm.checkpoint_path}")
|
||||
|
||||
elif cmd == "status":
|
||||
if len(sys.argv) < 3:
|
||||
# List all builds
|
||||
if os.path.exists(BUILDS_DIR):
|
||||
builds = os.listdir(BUILDS_DIR)
|
||||
if builds:
|
||||
print(f"Builds: {len(builds)}")
|
||||
for build_id in sorted(builds):
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
status = sm.get_status()
|
||||
print(
|
||||
f" {build_id}: {status['description']} ({status['current_phase']}: {status['phase_status']})"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print("No builds found.")
|
||||
else:
|
||||
print("No builds found.")
|
||||
else:
|
||||
build_id = sys.argv[2]
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
status = sm.get_status()
|
||||
print(f"Build: {status['build_id']}")
|
||||
print(f" Description: {status['description']}")
|
||||
print(f" Phase: {status['current_phase']} ({status['phase_status']})")
|
||||
print(f" Batches: {status['batches']}")
|
||||
print(f" Corrections: {status['corrections']}")
|
||||
print(f" Worktree: {status['worktree']}")
|
||||
print(f" Complexity: {status['complexity_score']}")
|
||||
print(f" Estimated steps: {status['estimated_steps']}")
|
||||
except FileNotFoundError:
|
||||
print(f"Build {build_id} not found")
|
||||
|
||||
elif cmd == "resume":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py resume <build_id>")
|
||||
sys.exit(1)
|
||||
build_id = sys.argv[2]
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
if sm.can_resume():
|
||||
resume_point = sm.get_resume_point()
|
||||
print(f"Resuming build {build_id} from phase: {resume_point}")
|
||||
# Execute the current phase
|
||||
result = execute_phase(build_id)
|
||||
print(f"Result: {result}")
|
||||
else:
|
||||
print(f"Build {build_id} cannot be resumed (all phases complete)")
|
||||
except FileNotFoundError:
|
||||
print(f"Build {build_id} not found")
|
||||
|
||||
elif cmd == "execute":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py execute <build_id> [phase]")
|
||||
sys.exit(1)
|
||||
build_id = sys.argv[2]
|
||||
phase = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
result = execute_phase(build_id, phase)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
elif cmd == "abort":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: build_mode_plugin.py abort <build_id>")
|
||||
sys.exit(1)
|
||||
build_id = sys.argv[2]
|
||||
try:
|
||||
sm = BuildStateMachine(build_id)
|
||||
checkpoint = sm.load_checkpoint()
|
||||
worktree = checkpoint.get("worktree_path")
|
||||
if worktree:
|
||||
subprocess.run(
|
||||
["git", "worktree", "remove", worktree, "--force"],
|
||||
cwd=REPO_DIR,
|
||||
capture_output=True,
|
||||
)
|
||||
sm.log_to_ledger({"action": "build_aborted"})
|
||||
print(f"✅ Build {build_id} aborted")
|
||||
except FileNotFoundError:
|
||||
print(f"Build {build_id} not found")
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assess phase — Complexity scoring and codebase impact estimation.
|
||||
|
||||
This is the first phase. It scores the user's request to determine:
|
||||
- If build mode is needed (score 8+)
|
||||
- How many steps are estimated
|
||||
- What files will be affected
|
||||
|
||||
Output: Updates checkpoint with complexity_score, estimated_steps, description.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
# Add parent to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the assess phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
if not description:
|
||||
return {"success": False, "error": "No description provided"}
|
||||
|
||||
# Import complexity scorer
|
||||
try:
|
||||
from rune.complexity_scorer import score_request
|
||||
|
||||
breakdown = score_request(description)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Complexity scorer failed: {e}"}
|
||||
|
||||
# Update checkpoint
|
||||
checkpoint["complexity_score"] = breakdown.total
|
||||
checkpoint["estimated_steps"] = _estimate_steps(description)
|
||||
checkpoint["assess_signals"] = breakdown.signals
|
||||
checkpoint["assess_mode"] = breakdown.mode
|
||||
checkpoint["assess_confidence"] = breakdown.confidence
|
||||
sm.save_checkpoint(checkpoint)
|
||||
|
||||
# Log to ledger
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "assess_complete",
|
||||
"complexity_score": breakdown.total,
|
||||
"estimated_steps": checkpoint["estimated_steps"],
|
||||
"mode": breakdown.mode,
|
||||
"confidence": breakdown.confidence,
|
||||
"signals": breakdown.signals,
|
||||
}
|
||||
)
|
||||
|
||||
# Check if build mode is needed
|
||||
if breakdown.total < 8:
|
||||
return {
|
||||
"success": True,
|
||||
"build_mode_needed": False,
|
||||
"complexity_score": breakdown.total,
|
||||
"estimated_steps": checkpoint["estimated_steps"],
|
||||
"mode": breakdown.mode,
|
||||
"reason": f"Score {breakdown.total} < 8, task mode recommended",
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"build_mode_needed": True,
|
||||
"complexity_score": breakdown.total,
|
||||
"estimated_steps": checkpoint["estimated_steps"],
|
||||
"mode": breakdown.mode,
|
||||
"confidence": breakdown.confidence,
|
||||
"signals": breakdown.signals,
|
||||
}
|
||||
|
||||
|
||||
def _estimate_steps(description: str) -> int:
|
||||
"""Estimate number of steps from description."""
|
||||
steps = 0
|
||||
desc_lower = description.lower()
|
||||
|
||||
# Each distinct verb ≈ 1-2 steps
|
||||
verbs = [
|
||||
"add",
|
||||
"create",
|
||||
"build",
|
||||
"implement",
|
||||
"update",
|
||||
"fix",
|
||||
"refactor",
|
||||
"migrate",
|
||||
"test",
|
||||
"deploy",
|
||||
"configure",
|
||||
"integrate",
|
||||
"design",
|
||||
]
|
||||
verb_count = sum(1 for v in verbs if v in desc_lower)
|
||||
steps += verb_count * 2
|
||||
|
||||
# Each "and" or "then" ≈ 1 step
|
||||
conjunctions = ["and", "then", "also", "plus"]
|
||||
conjunction_count = sum(1 for c in conjunctions if c in desc_lower)
|
||||
steps += conjunction_count
|
||||
|
||||
# Each file reference ≈ 1 step
|
||||
import re
|
||||
|
||||
file_refs = re.findall(r"\b\w+\.\w+\b", description)
|
||||
steps += len(file_refs)
|
||||
|
||||
# Minimum 3 steps for build mode
|
||||
return max(steps, 3)
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build phase — Execute batches in 3 phases.
|
||||
|
||||
Three-phase execution:
|
||||
1. Foundation: Types, interfaces, shared utilities
|
||||
2. Implementation: Feature code
|
||||
3. Integration: Tests, wiring, documentation
|
||||
|
||||
Output: Updates checkpoint with build results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the build phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
batches = checkpoint.get("batches", [])
|
||||
|
||||
if not batches:
|
||||
return {"success": False, "error": "No batches to build"}
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "build_started",
|
||||
"batches": len(batches),
|
||||
}
|
||||
)
|
||||
|
||||
# Build is done by the TUI's model
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"build_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"build_needed": True,
|
||||
"batches": batches,
|
||||
"instruction": "Execute batches in 3 phases: Foundation → Implementation → Integration",
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consent phase — User approval.
|
||||
|
||||
Presents the plan to the user and waits for approval.
|
||||
Output: Updates checkpoint with approved.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the consent phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
complexity_score = checkpoint.get("complexity_score", 0)
|
||||
estimated_steps = checkpoint.get("estimated_steps", 0)
|
||||
patterns = checkpoint.get("consult_patterns", [])
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "consent_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Consent is handled by the TUI
|
||||
# This module just prepares the approval request
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"consent_needed": True,
|
||||
"description": description,
|
||||
"complexity_score": complexity_score,
|
||||
"estimated_steps": estimated_steps,
|
||||
"patterns": patterns,
|
||||
"instruction": "Present plan to user and wait for approval",
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consult phase — Knowledge base check + failure pattern lookup.
|
||||
|
||||
Checks llm-craft and textual-craft knowledge bases for best practices.
|
||||
Queries failure pattern database for similar past failures.
|
||||
Output: Updates checkpoint with patterns, best_practices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the consult phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "consult_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Query knowledge bases
|
||||
patterns = []
|
||||
best_practices = []
|
||||
|
||||
try:
|
||||
# Check llm-craft knowledge base
|
||||
kb_path = os.path.join(os.path.dirname(__file__), "..", "..", "skills", "llm-craft", "knowledge.md")
|
||||
if os.path.exists(kb_path):
|
||||
with open(kb_path) as f:
|
||||
content = f.read()
|
||||
# Extract relevant patterns
|
||||
if "test" in description.lower():
|
||||
patterns.append("Use contract testing for interface verification")
|
||||
if "refactor" in description.lower():
|
||||
patterns.append("Refactor in small steps, verify after each change")
|
||||
if "new feature" in description.lower():
|
||||
patterns.append("Start with interface definition, then implementation")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Query failure patterns
|
||||
failure_patterns = checkpoint.get("failure_patterns", [])
|
||||
if failure_patterns:
|
||||
best_practices.append(f"Learn from {len(failure_patterns)} past failures")
|
||||
|
||||
# Update checkpoint
|
||||
checkpoint["consult_patterns"] = patterns
|
||||
checkpoint["consult_best_practices"] = best_practices
|
||||
sm.save_checkpoint(checkpoint)
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "consult_complete",
|
||||
"patterns": patterns,
|
||||
"best_practices": best_practices,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"patterns": patterns,
|
||||
"best_practices": best_practices,
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Correct phase — Fix failed batches.
|
||||
|
||||
3 attempts max per batch. After 2 failures, re-plan instead of blindly retrying.
|
||||
Output: Updates checkpoint with corrections.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the correct phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
corrections = checkpoint.get("corrections", 0)
|
||||
max_corrections = checkpoint.get("max_corrections", 3)
|
||||
|
||||
if corrections >= max_corrections:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Max corrections ({max_corrections}) reached",
|
||||
"instruction": "Escalate to user with diagnostic summary",
|
||||
}
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "correct_started",
|
||||
"corrections": corrections,
|
||||
}
|
||||
)
|
||||
|
||||
# Correct is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
checkpoint["corrections"] = corrections + 1
|
||||
sm.save_checkpoint(checkpoint)
|
||||
|
||||
sm.set_phase_status("in_progress", {"correct_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"correct_needed": True,
|
||||
"corrections": corrections + 1,
|
||||
"instruction": "Fix failed batches, re-verify. After 2 failures, re-plan.",
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Feasible phase — Feasibility gate + complexity report.
|
||||
|
||||
Checks if the task is feasible based on research findings.
|
||||
Output: Updates checkpoint with feasible, risk, complexity_report.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the feasible phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
research = checkpoint.get("research_findings", {})
|
||||
|
||||
# Feasibility is determined by the TUI's model
|
||||
# This module just tracks the state
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "feasible_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
sm.set_phase_status("in_progress", {"feasibility_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"feasibility_needed": True,
|
||||
"description": description,
|
||||
"research": research,
|
||||
"instruction": "Assess feasibility based on research findings",
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Integrate phase — Scope guardian (full test suite).
|
||||
|
||||
Runs full verification after all batches pass.
|
||||
Output: Updates checkpoint with integration results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the integrate phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "integrate_started",
|
||||
}
|
||||
)
|
||||
|
||||
# Integrate is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"integrate_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"integrate_needed": True,
|
||||
"instruction": "Run full test suite: canary + ruff + pyright + contracts + TUI validator",
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plan phase — Work manager creates batches with dependency DAG.
|
||||
|
||||
Creates batches of 1-3 files each, routes to local or cloud models.
|
||||
Output: Updates checkpoint with batches.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the plan phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
complexity_score = checkpoint.get("complexity_score", 0)
|
||||
estimated_steps = checkpoint.get("estimated_steps", 0)
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "plan_started",
|
||||
"description": description,
|
||||
"complexity_score": complexity_score,
|
||||
"estimated_steps": estimated_steps,
|
||||
}
|
||||
)
|
||||
|
||||
# Plan is done by the TUI's model
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"plan_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"plan_needed": True,
|
||||
"description": description,
|
||||
"complexity_score": complexity_score,
|
||||
"estimated_steps": estimated_steps,
|
||||
"instruction": "Create batches with dependency DAG, route to models",
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record phase — Update PRD, GitHub, knowledge base, trajectory log.
|
||||
|
||||
Final phase. Updates all documentation and records what was learned.
|
||||
Output: Updates checkpoint with record results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the record phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
build_id = sm.build_id
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "record_started",
|
||||
"build_id": build_id,
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Record is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"record_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"record_needed": True,
|
||||
"build_id": build_id,
|
||||
"description": description,
|
||||
"instruction": "Update PRD, commit to GitHub, update knowledge base with learnings",
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Research phase — Deep research on the task.
|
||||
|
||||
Always runs for build mode. Uses deep-research skill or parallel sub-agents.
|
||||
Output: Updates checkpoint with research_findings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the research phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
description = checkpoint.get("description", "")
|
||||
|
||||
if not description:
|
||||
return {"success": False, "error": "No description provided"}
|
||||
|
||||
# Log to ledger
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "research_started",
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
|
||||
# Research is done by the TUI's model (not this module)
|
||||
# This module just tracks the state
|
||||
# The actual research happens in the TUI's _send_to_model flow
|
||||
|
||||
# For now, mark as needing external research
|
||||
sm.set_phase_status("in_progress", {"research_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"research_needed": True,
|
||||
"description": description,
|
||||
"instruction": "Use deep-research skill or parallel sub-agents to research this task",
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify phase — Tiered verification.
|
||||
|
||||
Tiered verification strategy:
|
||||
1. Syntactic: Per-edit lint, type-check, format (<1s)
|
||||
2. Semantic: Per-batch unit tests, import validation (<30s)
|
||||
3. Integration: Per-wave contract tests, build/compile (<5min)
|
||||
4. System: Full test suite, property-based (<15min)
|
||||
|
||||
Output: Updates checkpoint with verification results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def execute(sm) -> Dict[str, Any]:
|
||||
"""Execute the verify phase."""
|
||||
checkpoint = sm.load_checkpoint()
|
||||
batches = checkpoint.get("batches", [])
|
||||
|
||||
sm.log_to_ledger(
|
||||
{
|
||||
"action": "verify_started",
|
||||
"batches": len(batches),
|
||||
}
|
||||
)
|
||||
|
||||
# Verify is done by the TUI
|
||||
# This module just tracks the state
|
||||
|
||||
sm.set_phase_status("in_progress", {"verify_requested": True})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"verify_needed": True,
|
||||
"batches": batches,
|
||||
"instruction": "Run tiered verification: syntactic → semantic → integration → system",
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
|
||||
# stage states
|
||||
@@ -101,6 +102,15 @@ class Programme:
|
||||
"next_action": None,
|
||||
"reason": (f"{len(approval)} stage(s) need user "
|
||||
f"authorization")}
|
||||
# T17 §17E: all stages terminal but some FAILED — the programme
|
||||
# may still close (programme-level acceptance decides), never
|
||||
# hang in UNKNOWN_INTERRUPTION.
|
||||
if all(s.state in TERMINAL for s in self.stages):
|
||||
failed = [s.stage_id for s in self.stages if s.state == FAILED]
|
||||
return {"verdict": "PROGRAMME_COMPLETE_WITH_FAILURES",
|
||||
"next_action": None,
|
||||
"reason": (f"all stages terminal; failed: {failed} — "
|
||||
f"programme-level acceptance decides")}
|
||||
return {"verdict": "UNKNOWN_INTERRUPTION", "next_action": None,
|
||||
"reason": "no stage runnable and none terminal"}
|
||||
|
||||
@@ -163,3 +173,177 @@ def run_programme(prog: Programme, stage_runner, *,
|
||||
stage.state = stage_runner(stage) # runner decides the new state
|
||||
log.append({"stage": target, "new_state": stage.state})
|
||||
return log
|
||||
|
||||
|
||||
# ── T17 §17D: production wiring — Scaffolding-integrated driver ─────────
|
||||
#
|
||||
# The 15M canary proved the RULE; this adapter makes it CANONICAL. One
|
||||
# programme = a list of stages; each stage maps 1:1 to a Scaffolding
|
||||
# contract (stage_id == contract_id). Stages run through the Construction
|
||||
# Interface lifecycle (approved→building→verifying→closed); asynchronous
|
||||
# stage work runs under the event bridge (WAITING_EVENT is a state, not
|
||||
# a stop). Persisted to the scaffold store's programme area — reloadable
|
||||
# after restart with no transcript.
|
||||
|
||||
DEFAULT_PROGRAMME_AREA = "programmes"
|
||||
|
||||
|
||||
def _programme_base(scaffold_store: Path) -> Path:
|
||||
return Path(scaffold_store) / DEFAULT_PROGRAMME_AREA
|
||||
|
||||
|
||||
def scaffolding_stage_runner(interface, *, scaffold_store: Path,
|
||||
daemon_url: str = "",
|
||||
watchdog_secs: int = 900):
|
||||
"""Return a stage_runner for run_programme bound to the Construction
|
||||
Interface. The stage's work is read from stage.result["work"] — a
|
||||
callable (interface, stage) → dict with {"ok": bool, "evidence": str,
|
||||
"async_cmd": [argv] optional}. When async_cmd is present the work
|
||||
runs under the event bridge and the driver resumes on the EVENT;
|
||||
canonical verification still decides completion truth. Stages without
|
||||
work complete as governance-only transitions."""
|
||||
from rune import event_wait as ew # local import: optional dependency
|
||||
|
||||
def _phase(contract_id: str) -> str:
|
||||
# ALWAYS re-read: transition() saves through its own doc copy, so
|
||||
# a locally-held contract dict goes stale after any transition
|
||||
# (T17 17E live finding — the draft→approved→building chain
|
||||
# silently stopped after the first step on a stale reference).
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
return (c or {}).get("phase", "")
|
||||
|
||||
def run(stage: ProgrammeStage) -> str:
|
||||
contract_id = stage.stage_id
|
||||
phase = _phase(contract_id)
|
||||
if phase == "":
|
||||
stage.result["error"] = f"no contract {contract_id}"
|
||||
return FAILED
|
||||
if phase == "closed":
|
||||
return COMPLETE # idempotent re-entry (duplicate signal case)
|
||||
if phase == "draft":
|
||||
interface.transition(contract_id, "approved",
|
||||
evidence_ref="programme-stage-start")
|
||||
if _phase(contract_id) == "approved":
|
||||
interface.transition(contract_id, "building",
|
||||
evidence_ref="programme-stage-start")
|
||||
work = cast("Any", stage.result.get("work"))
|
||||
if work is None:
|
||||
interface.transition(contract_id, "verifying",
|
||||
evidence_ref="governance-only-stage")
|
||||
interface.close(
|
||||
contract_id,
|
||||
observed_evidence=[{"source": "programme-driver",
|
||||
"detail": "governance-only stage: "
|
||||
"no work, closure by "
|
||||
"programme acceptance"}],
|
||||
scope_files=[])
|
||||
return COMPLETE
|
||||
# work is either a callable (interface, stage) → outcome dict, or
|
||||
# a literal outcome spec dict ({"ok", "evidence", "async_cmd"?})
|
||||
outcome: dict = (dict(cast("dict", work(interface, stage)))
|
||||
if callable(work) else dict(cast("dict", work)))
|
||||
async_cmd = outcome.get("async_cmd")
|
||||
if async_cmd:
|
||||
op = f"stage-{contract_id}-{int(time.time())}"
|
||||
ew.register_process_wait(
|
||||
op, contract_id=contract_id,
|
||||
project=getattr(interface.scaffold, "project_id", ""),
|
||||
objective=stage.objective,
|
||||
watched_for="stage work completion event",
|
||||
watchdog_secs=watchdog_secs)
|
||||
notifier = ew.run_builder_under_events(
|
||||
async_cmd, op, daemon_url=daemon_url or ew.DEFAULT_DAEMON_URL)
|
||||
stage.result["operation_id"] = op
|
||||
stage.result["notifier_pgid"] = notifier.pid
|
||||
result = ew.await_resume_event(op, watchdog_secs=watchdog_secs,
|
||||
pgid=notifier.pid)
|
||||
if result["kind"] != "event":
|
||||
stage.result["error"] = f"stage event wait: {result['kind']}"
|
||||
interface.transition(contract_id, "failed",
|
||||
evidence_ref="stage-event-lost")
|
||||
return FAILED
|
||||
rec: dict = result.get("wait") or {}
|
||||
outcome["evidence"] = (f"event resume; wait {rec.get('wait_id')} "
|
||||
f"verdict={rec.get('verdict')}")
|
||||
if not outcome.get("ok"):
|
||||
stage.result["error"] = outcome.get("evidence", "work failed")
|
||||
# truthful failure: record the evidence ON the contract and
|
||||
# drive it to the failed phase via the legal transition map
|
||||
# (building→failed is legal; no exception swallowing)
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
if c is not None:
|
||||
c.setdefault("checkpoints", []).append({
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()),
|
||||
"phase": c.get("phase", ""),
|
||||
"evidence": "programme-stage-failed",
|
||||
"note": (outcome.get("evidence") or "work failed")[:200],
|
||||
})
|
||||
interface.scaffold.save(doc)
|
||||
phase = c.get("phase", "")
|
||||
if phase == "building":
|
||||
interface.transition(contract_id, "failed",
|
||||
evidence_ref="programme-stage-failed")
|
||||
return FAILED
|
||||
# completion truth: work evidence recorded ON the contract, then
|
||||
# closed (evidence lives on the durable object, not the log)
|
||||
if _phase(contract_id) == "building":
|
||||
interface.transition(contract_id, "verifying",
|
||||
evidence_ref="programme-stage-verify")
|
||||
doc = interface.scaffold.load()
|
||||
c = interface.scaffold.contract(doc, contract_id)
|
||||
if c is not None:
|
||||
c.setdefault("checkpoints", []).append({
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"phase": "verifying",
|
||||
"evidence": "programme-stage-work",
|
||||
"note": (outcome.get("evidence") or "work completed")[:200],
|
||||
})
|
||||
interface.scaffold.save(doc)
|
||||
interface.close(
|
||||
contract_id,
|
||||
observed_evidence=[{"source": "programme-driver",
|
||||
"detail": outcome.get("evidence", "ok")}],
|
||||
scope_files=[])
|
||||
return COMPLETE
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def run_construction_programme(prog: Programme, interface, *,
|
||||
scaffold_store: Path,
|
||||
daemon_url: str = "",
|
||||
watchdog_secs: int = 900,
|
||||
max_transitions: int = 100) -> list[dict]:
|
||||
"""Canonical multi-stage construction continuation (T17 §17D):
|
||||
run_programme with the Scaffolding-bound runner; persists the
|
||||
programme around every transition so a restart resumes from the
|
||||
durable record, never a transcript. Returns the transition log."""
|
||||
runner = scaffolding_stage_runner(
|
||||
interface, scaffold_store=scaffold_store,
|
||||
daemon_url=daemon_url, watchdog_secs=watchdog_secs)
|
||||
log: list[dict] = []
|
||||
base = _programme_base(scaffold_store)
|
||||
for _ in range(max_transitions):
|
||||
prog.save(base)
|
||||
canary = prog.continuation_canary()
|
||||
log.append({"canary": canary})
|
||||
verdict, action = canary["verdict"], canary["next_action"]
|
||||
if action is None:
|
||||
break
|
||||
kind, target = action
|
||||
if kind == "fallback":
|
||||
log.append({"fallback_started": target})
|
||||
break
|
||||
stage = next(s for s in prog.stages if s.stage_id == target)
|
||||
if stage.state == COMPLETE:
|
||||
continue # duplicate continuation signal: idempotent
|
||||
stage.state = ELIGIBLE
|
||||
new_state = runner(stage)
|
||||
stage.state = new_state
|
||||
log.append({"stage": target, "new_state": new_state})
|
||||
prog.save(base)
|
||||
prog.save(base)
|
||||
return log
|
||||
@@ -740,12 +740,39 @@ class RuneOperator:
|
||||
json.dump(self.state.to_dict(), f, indent=2)
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get Rune's current status."""
|
||||
"""Get Rune's current status. T17 17F: task counters derive from
|
||||
the CANONICAL TaskStateRepository — operator.json's task list is
|
||||
a legacy cache, never a second authority."""
|
||||
counters = self._task_counters()
|
||||
return {
|
||||
"identity": self.state.identity,
|
||||
"role": self.state.role,
|
||||
"current_task": self.state.current_task,
|
||||
"active_model": self.state.active_model,
|
||||
**counters,
|
||||
}
|
||||
|
||||
def _task_counters(self) -> Dict[str, int]:
|
||||
"""Counters from the canonical repo; repo vocabulary mapped to
|
||||
the legacy counter buckets (created→pending)."""
|
||||
buckets = {"pending": 0, "running": 0, "completed": 0}
|
||||
try:
|
||||
records = self._task_state.list_all()
|
||||
except Exception: # noqa: BLE001 — repo unavailable: in-memory fallback
|
||||
records = []
|
||||
if records:
|
||||
for r in records:
|
||||
if r.status in ("created", "pending"):
|
||||
buckets["pending"] += 1
|
||||
elif r.status == "running":
|
||||
buckets["running"] += 1
|
||||
elif r.status == "completed":
|
||||
buckets["completed"] += 1
|
||||
return {"tasks_pending": buckets["pending"],
|
||||
"tasks_running": buckets["running"],
|
||||
"tasks_completed": buckets["completed"]}
|
||||
# no repo records (fresh store): fall back to the in-memory set
|
||||
return {
|
||||
"tasks_pending": len([t for t in self.state.tasks if t.status == "pending"]),
|
||||
"tasks_running": len([t for t in self.state.tasks if t.status == "running"]),
|
||||
"tasks_completed": len([t for t in self.state.tasks if t.status == "completed"]),
|
||||
|
||||
+61
-2
@@ -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.67
|
||||
Version: v0.66.76
|
||||
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.67 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.76 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -3141,6 +3141,65 @@ class RuneApp(App):
|
||||
|
||||
elif arg.startswith("start "):
|
||||
description = arg[6:]
|
||||
# T17 17G: /build start goes through the SAME canonical
|
||||
# Scaffolding classification as every other construction
|
||||
# route — no independent construction state machine.
|
||||
# NOT_CONSTRUCTION refuses; UNKNOWN surfaces the probe
|
||||
# questions; PROJECT_BUILD/EXISTING_PROJECT_CHANGE
|
||||
# activate the contract, then delegate to BuildManager
|
||||
# as a bounded adapter (worktree isolation + resume
|
||||
# ledger remain its unique capabilities).
|
||||
try:
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
iface = ConstructionInterface(builder="tui-build-cmd")
|
||||
envelope = iface.classify(description)
|
||||
state = envelope.get("state") or {}
|
||||
mode_name = state.get("mode", "UNKNOWN")
|
||||
probes = state.get("probe_questions", [])
|
||||
except Exception as exc: # noqa: BLE001 — classification
|
||||
mode_name = "UNKNOWN" # unavailable: refuse, fail closed
|
||||
probes = []
|
||||
self._ch().add_message("system",
|
||||
f"[red]Construction classification unavailable ({exc}) — refusing to start a build[/red]")
|
||||
return
|
||||
if mode_name in ("NOT_CONSTRUCTION", "UNKNOWN"):
|
||||
msg = (f"[yellow]'/build start' refused: classified "
|
||||
f"{mode_name}.[/yellow]")
|
||||
for q in probes[:3]:
|
||||
msg += f"\n - {q}"
|
||||
self._ch().add_message("system", msg)
|
||||
return
|
||||
# T18 18M FULL BACKEND CONVERGENCE: admission mints a
|
||||
# canonical Construction Contract (same as every other
|
||||
# route) — /build start no longer runs an independent
|
||||
# construction state machine. BuildManager remains the
|
||||
# bounded worktree/ledger adapter driven BY the contract.
|
||||
import time as _t
|
||||
_cid = f"build-{int(_t.time())}"
|
||||
try:
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
_iface = ConstructionInterface(builder="tui-build-cmd")
|
||||
_doc = _iface.scaffold.load()
|
||||
except Exception as exc: # noqa: BLE001 — deliberate degradation: the refusal gate already reported; construction proceeds legacy-governed rather than blocked
|
||||
self._ch().add_message(
|
||||
"system", f"[yellow]contract mint unavailable ({exc})[/yellow]")
|
||||
_iface = None
|
||||
if _iface is not None:
|
||||
try:
|
||||
_iface.scaffold.new_contract(
|
||||
_doc, contract_id=_cid, need=description,
|
||||
origin="user", scope=[],
|
||||
requirement="/build start (classified: "
|
||||
f"{mode_name})")
|
||||
_iface.scaffold.save(_doc)
|
||||
_iface.transition(_cid, "approved",
|
||||
evidence_ref="user:/build-start")
|
||||
_iface.transition(_cid, "building",
|
||||
evidence_ref="build-manager-adapter")
|
||||
except Exception as exc: # noqa: BLE001 — contract mint
|
||||
self._ch().add_message("system",
|
||||
f"[yellow]contract mint failed ({exc}) — build "
|
||||
f"proceeds unclassified-governed (legacy)[/yellow]")
|
||||
manager = BuildManager()
|
||||
build_id = manager.start_build(description)
|
||||
self._ch().add_message("system", f"[bold]✅ Build {build_id} started[/bold]")
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""structural_migration.py — T18 §18I: schema versioning + consent for
|
||||
structural Scaffolds.
|
||||
|
||||
USER REQUIREMENT (T18 mandate): when Scaffolding itself later receives a
|
||||
SIGNIFICANT structural/version uplift, known projects previously
|
||||
scaffolded must NOT be silently upgraded. Material migrations require
|
||||
user consent; minor backwards-compatible migrations stay automatic.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class ConsentRequired(Exception):
|
||||
"""A material schema migration was requested without user approval."""
|
||||
|
||||
|
||||
def classify_migration(*, from_v: int, to_v: int,
|
||||
changes: list[str]) -> str:
|
||||
"""MINOR (automatic) vs MATERIAL (consent required). Deterministic:
|
||||
renames/removals/id-format changes/semantic shifts are MATERIAL;
|
||||
additive optional kinds/fields are MINOR."""
|
||||
material_markers = ("rename", "drop ", "remove", "delete",
|
||||
"id format", "semantic", "restructure",
|
||||
"change format", "merge", "split")
|
||||
for c in changes:
|
||||
cl = c.lower()
|
||||
if any(m in cl for m in material_markers):
|
||||
return "MATERIAL"
|
||||
return "MINOR"
|
||||
|
||||
|
||||
def affected_projects(stores: dict[str, dict], proposed_v: int) -> list[str]:
|
||||
"""Which known projects (schema_version < proposed_v) would a
|
||||
proposed uplift touch. Scaffolding presents these for consent —
|
||||
never silently upgrades."""
|
||||
return sorted(name for name, doc in stores.items()
|
||||
if doc.get("schema_version", 0) < proposed_v)
|
||||
|
||||
|
||||
def apply_migration(*, model_store, from_v: int, to_v: int,
|
||||
changes: list[str], user_approved: bool,
|
||||
approved_by: str = "") -> dict:
|
||||
"""Apply (or refuse) a schema migration. MATERIAL without explicit
|
||||
user approval raises ConsentRequired — the silent upgrade is
|
||||
structurally impossible. Returns a durable migration record."""
|
||||
kind = classify_migration(from_v=from_v, to_v=to_v, changes=changes)
|
||||
if kind == "MATERIAL" and not user_approved:
|
||||
raise ConsentRequired(
|
||||
f"material schema migration {from_v}→{to_v} requires user "
|
||||
f"consent: {changes[:3]}")
|
||||
return {
|
||||
"from_v": from_v, "to_v": to_v, "kind": kind,
|
||||
"changes": changes, "user_approved": user_approved,
|
||||
"approved_by": approved_by if user_approved else None,
|
||||
"applied_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def consent_brief(*, from_v: int, to_v: int, changes: list[str],
|
||||
benefits: list[str]) -> dict:
|
||||
"""The consent presentation (rule 18I): current version, proposed
|
||||
version, benefits, semantic changes, risks, expected effect."""
|
||||
return {
|
||||
"current_version": from_v,
|
||||
"proposed_version": to_v,
|
||||
"benefits": benefits,
|
||||
"semantic_changes": changes,
|
||||
"migration_risks": [
|
||||
"stored models require one-time reconciliation after upgrade",
|
||||
"consumers of the old schema need re-reading via projection",
|
||||
],
|
||||
"expected_effect": ("models keep their data; identity and "
|
||||
"relationship semantics change per the listed "
|
||||
"changes"),
|
||||
"requires": "explicit user approval before any project is upgraded",
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
"""structural_model.py — T18 §18E/§18F: the first-class STRUCTURAL
|
||||
SCAFFOLD for scaffolded projects.
|
||||
|
||||
RESEARCH-LED (Build Basis: research/t18-structural-model-prior-art,
|
||||
QUALIFIED 2026-09-15):
|
||||
|
||||
ADOPTED: Backstage relation vocabulary (partOf/belongs_to,
|
||||
depends_on, provides/consumes, ownedBy) + entity-ref discipline;
|
||||
Kythe edge kinds (calls, reads, writes, satisfies, overrides) +
|
||||
derived-reverse-edge rule; Nx affected semantics for impact closure;
|
||||
Bazel rdeps(kind(test)) for verification scoping.
|
||||
|
||||
ADAPTED: identity is DETERMINISTIC and STABLE across versions
|
||||
(Kythe explicitly declines this — a persistent model cannot); C4
|
||||
deployment shapes become machine-derived deployed_by edges; Glean's
|
||||
batch layering → externally-derived relationships carry a distinct
|
||||
authority.
|
||||
|
||||
REJECTED: heavyweight graph stores (no evidence/maturity/consent
|
||||
semantics); LSIF; Stack Graphs (archived).
|
||||
|
||||
INNOVATION (absent from prior art): evidence_refs + authority +
|
||||
status on every relationship; verified_by/deployed_by to concrete
|
||||
entities; consent-gated schema migration.
|
||||
|
||||
Storage: the EXISTING structural_store generic core (rule A — no
|
||||
second graph store, no competing Spine; Spine uses the same core for
|
||||
Rune, project models use it for their projects).
|
||||
|
||||
ENTITY KINDS: project, domain, subsystem, component (module), interface,
|
||||
state_store, test_suite, deployment_surface, environment.
|
||||
RELATIONSHIPS: belongs_to, depends_on, calls, provides, consumes,
|
||||
reads, writes, connects_to, verified_by, deployed_by, owns, supersedes,
|
||||
legacy_of — every edge carries evidence_refs + authority + status.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import (
|
||||
add_entity, add_relationship, entity, load, new_doc, save,
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
ENTITY_KINDS = (
|
||||
"project", "domain", "subsystem", "component", "interface",
|
||||
"state_store", "test_suite", "deployment_surface", "environment",
|
||||
)
|
||||
|
||||
RELATION_TYPES = (
|
||||
"belongs_to", "owns", "depends_on", "calls", "provides", "consumes",
|
||||
"reads", "writes", "connects_to", "verified_by", "deployed_by",
|
||||
"supersedes", "legacy_of",
|
||||
)
|
||||
|
||||
|
||||
class StructuralModel:
|
||||
"""A project's persistent interconnected structural model. One file
|
||||
per project: <store_dir>/structural.json (spine-shaped — same
|
||||
generic core, open-world: unknown future kinds/relationships survive
|
||||
round-trips by construction)."""
|
||||
|
||||
def __init__(self, store_dir: Path | str, project_id: str):
|
||||
self.store_dir = Path(store_dir)
|
||||
self.project_id = project_id
|
||||
self.path = self.store_dir / "structural.json"
|
||||
doc = load(self.path)
|
||||
if doc is None:
|
||||
doc = new_doc()
|
||||
self._init_project(doc)
|
||||
self.doc = doc
|
||||
|
||||
# -- persistence ----------------------------------------------------
|
||||
|
||||
def _init_project(self, doc: dict) -> None:
|
||||
add_entity(doc, id=f"project:{self.project_id}", kind="project",
|
||||
maturity="DECLARED",
|
||||
evidence_refs=["structural-model-init"])
|
||||
root = add_relationship(doc, type="belongs_to",
|
||||
from_id=f"project:{self.project_id}",
|
||||
to_id=f"project:{self.project_id}",
|
||||
evidence_refs=["structural-model-root"])
|
||||
root["authority"] = "model-author"
|
||||
root["status"] = "root"
|
||||
save(doc, self.path)
|
||||
|
||||
def save(self) -> None:
|
||||
self.doc["schema_version"] = SCHEMA_VERSION
|
||||
save(self.doc, self.path)
|
||||
|
||||
def reload(self) -> None:
|
||||
doc = load(self.path)
|
||||
if doc is not None:
|
||||
self.doc = doc
|
||||
|
||||
# -- entity/relationship construction -------------------------------
|
||||
|
||||
def add(self, kind: str, name: str, *, parent: str | None = None,
|
||||
ext: dict | None = None,
|
||||
evidence_ref: str = "structural-model") -> str:
|
||||
"""Add an entity with a STABLE deterministic id (kind:name —
|
||||
persistent across model versions; rule 18E). parent (an id) adds
|
||||
the belongs_to edge (Backstage partOf semantics)."""
|
||||
if kind not in ENTITY_KINDS:
|
||||
raise ValueError(f"unknown entity kind {kind!r} — known: "
|
||||
f"{ENTITY_KINDS}")
|
||||
eid = f"{kind}:{name}"
|
||||
e = add_entity(self.doc, id=eid, kind=kind, maturity="DECLARED",
|
||||
evidence_refs=[evidence_ref])
|
||||
e["identities"]["name"] = name
|
||||
if ext:
|
||||
e.setdefault("ext", {}).update(ext)
|
||||
if parent:
|
||||
self.relate(eid, "belongs_to", parent,
|
||||
evidence_ref=evidence_ref)
|
||||
return eid
|
||||
|
||||
def relate(self, from_id: str, rel: str, to_id: str, *,
|
||||
evidence_ref: str = "structural-model",
|
||||
authority: str = "model-author",
|
||||
status: str = "active", ext: dict | None = None) -> dict:
|
||||
"""Add a typed relationship. Every edge carries evidence_refs +
|
||||
authority + status (the INNOVATION per the Build Basis —
|
||||
authority-graded claims). Authority values: model-author
|
||||
(agent-observed/derived), importer (external tool derived —
|
||||
Glean batch-layering adaptation), user (user-locked)."""
|
||||
if rel not in RELATION_TYPES:
|
||||
raise ValueError(f"unknown relationship {rel!r} — known: "
|
||||
f"{RELATION_TYPES}")
|
||||
r = add_relationship(self.doc, type=rel, from_id=from_id,
|
||||
to_id=to_id, evidence_refs=[evidence_ref])
|
||||
r["authority"] = authority
|
||||
r["status"] = status
|
||||
if ext:
|
||||
r["ext"] = ext
|
||||
return r
|
||||
|
||||
# -- queries (§18F) ---------------------------------------------------
|
||||
|
||||
def get(self, eid: str) -> dict | None:
|
||||
return entity(self.doc, eid)
|
||||
|
||||
def children(self, eid: str) -> list[str]:
|
||||
return [r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["to"] == eid
|
||||
and r["from"] != eid]
|
||||
|
||||
def beneath(self, eid: str) -> list[str]:
|
||||
"""Transitive descendants (bounded by the graph itself)."""
|
||||
seen, out, frontier = {eid}, [], [eid]
|
||||
while frontier:
|
||||
cur = frontier.pop()
|
||||
for child in self.children(cur):
|
||||
if child not in seen:
|
||||
seen.add(child)
|
||||
out.append(child)
|
||||
frontier.append(child)
|
||||
return sorted(out)
|
||||
|
||||
def owning_domain(self, eid: str) -> str | None:
|
||||
cur, hops = eid, 0
|
||||
while hops < 16:
|
||||
if cur.startswith("domain:"):
|
||||
return cur
|
||||
parents = [r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["from"] == cur]
|
||||
if not parents:
|
||||
return None
|
||||
cur = parents[0]
|
||||
hops += 1
|
||||
return None
|
||||
|
||||
def dependencies(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "depends_on" and r["from"] == eid
|
||||
and r.get("status") != "stale")
|
||||
|
||||
def consumers(self, eid: str) -> list[str]:
|
||||
"""Who depends on OR consumes this entity (interfaces are
|
||||
consumed; components are depended on — Backstage relation pairs)."""
|
||||
return sorted(r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] in ("depends_on", "consumes")
|
||||
and r["to"] == eid and r.get("status") != "stale")
|
||||
|
||||
def interfaces_for(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "provides" and r["from"] == eid)
|
||||
|
||||
def state_owned(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "owns" and r["from"] == eid)
|
||||
|
||||
def verification_surfaces(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "verified_by" and r["from"] == eid)
|
||||
|
||||
def deployment_surfaces(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "deployed_by" and r["from"] == eid)
|
||||
|
||||
def canonical_state_owner(self, eid: str) -> str | None:
|
||||
"""The canonical-state owner: the state_store this entity owns,
|
||||
or (for a state_store) itself."""
|
||||
e = self.get(eid)
|
||||
if e and e.get("kind") == "state_store":
|
||||
return eid
|
||||
owned = self.state_owned(eid)
|
||||
return owned[0] if owned else None
|
||||
|
||||
def supersession(self, eid: str) -> dict:
|
||||
return {
|
||||
"supersedes": sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "supersedes"
|
||||
and r["from"] == eid),
|
||||
"superseded_by": sorted(
|
||||
r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "supersedes" and r["to"] == eid),
|
||||
"legacy_of": sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "legacy_of"
|
||||
and r["from"] == eid),
|
||||
}
|
||||
|
||||
# -- impact closure (§18F) -------------------------------------------
|
||||
|
||||
def impact_closure(self, eid: str, *, max_depth: int = 8) -> dict:
|
||||
"""STRUCTURALLY BOUNDED SCOPE (rule J): the necessary structural
|
||||
impact closure for a change to eid. Nx-affected semantics:
|
||||
reverse-dependency closure + verification + deployment + state
|
||||
surfaces. Unknown/dynamic relationships stay explicit uncertainty
|
||||
— never silent (rule 18F)."""
|
||||
if self.get(eid) is None:
|
||||
return {"error": f"unknown entity {eid!r}"}
|
||||
# reverse-dependency closure (consumers, transitively, bounded)
|
||||
seen, frontier, depth = {eid}, [eid], 0
|
||||
consumers: set[str] = set()
|
||||
while frontier and depth < max_depth:
|
||||
nxt = []
|
||||
for cur in frontier:
|
||||
for c in self.consumers(cur):
|
||||
if c not in seen:
|
||||
seen.add(c)
|
||||
consumers.add(c)
|
||||
nxt.append(c)
|
||||
frontier, depth = nxt, depth + 1
|
||||
direct_deps = self.dependencies(eid)
|
||||
# ripple through PROVIDED interfaces (Kythe exports semantics):
|
||||
# a change that touches an entity's provided interface reaches
|
||||
# every consumer of that interface
|
||||
for iface in self.interfaces_for(eid):
|
||||
for c in self.consumers(iface):
|
||||
if c not in seen:
|
||||
seen.add(c)
|
||||
consumers.add(c)
|
||||
uncertainty = [
|
||||
{"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed "
|
||||
"lookup/late-binding consumers — reconcilers must "
|
||||
"verify at runtime"}]
|
||||
# lockfile/deps edge (Nx documented case): deps outside closure
|
||||
# are OBSERVATION scope, not mutation scope
|
||||
return {
|
||||
"target": eid,
|
||||
"owning_domain": self.owning_domain(eid),
|
||||
"mutation_scope": sorted({eid} | consumers),
|
||||
"observation_scope": {
|
||||
"direct_dependencies": direct_deps,
|
||||
# interfaces the target provides AND consumes — both
|
||||
# directions are observation-relevant for a change
|
||||
"interfaces_provided": self.interfaces_for(eid),
|
||||
"interfaces_consumed": sorted(
|
||||
r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "consumes" and r["from"] == eid),
|
||||
},
|
||||
"state_surfaces": sorted(
|
||||
{s for e in ({eid} | consumers)
|
||||
for s in self.state_owned(e)}),
|
||||
"verification_scope": sorted(
|
||||
{v for e in ({eid} | consumers)
|
||||
for v in self.verification_surfaces(e)}),
|
||||
"deployment_scope": sorted(
|
||||
{d for e in ({eid} | consumers)
|
||||
for d in self.deployment_surfaces(e)}),
|
||||
"invariants": [
|
||||
f"belongs_to hierarchy of {eid} remains stable",
|
||||
"canonical-state ownership unchanged"],
|
||||
"non_goals": [],
|
||||
"uncertainty": uncertainty,
|
||||
"closure_depth": depth,
|
||||
}
|
||||
|
||||
|
||||
# ── T18 §18K: workspace lifecycle foundation ────────────────────────────
|
||||
#
|
||||
# Canonical lifecycle (rule 18K) — BuildManager remains the bounded
|
||||
# adapter for the worktree mechanics (17G decision), the LIFECYCLE and
|
||||
# its record live here so contracts can carry workspace provenance:
|
||||
#
|
||||
# verified source/base SHA → per-contract isolated worktree →
|
||||
# construction → verification → commit → push → CI/merge/release →
|
||||
# deployment confirmation → evidence archival → cleanup
|
||||
#
|
||||
# Records never hard-code live-runtime mutation (rule P): the deployed
|
||||
# generation changes only through the canonical promote machinery.
|
||||
|
||||
WORKSPACE_LIFECYCLE_STAGES = (
|
||||
"base_verified", "worktree_created", "construction",
|
||||
"verified", "committed", "pushed", "ci_qualified",
|
||||
"released", "deployment_confirmed", "evidence_archived", "cleaned",
|
||||
)
|
||||
|
||||
|
||||
def workspace_record(*, contract_id: str, construction_base: str,
|
||||
runtime_generation: str = "",
|
||||
candidate_generation: str = "",
|
||||
promotion_target: str = "gitea/master",
|
||||
rollback_generation: str = "") -> dict:
|
||||
"""The workspace provenance record a contract carries. Generations
|
||||
are SHAs/dirs — never live-tree mutations."""
|
||||
return {
|
||||
"contract_id": contract_id,
|
||||
"construction_base": construction_base,
|
||||
"runtime_generation": runtime_generation,
|
||||
"candidate_generation": candidate_generation,
|
||||
"promotion_target": promotion_target,
|
||||
"rollback_generation": rollback_generation,
|
||||
"stages_completed": [],
|
||||
"lifecycle": list(WORKSPACE_LIFECYCLE_STAGES),
|
||||
}
|
||||
|
||||
|
||||
def workspace_advance(record: dict, stage: str, *,
|
||||
evidence: str = "") -> dict:
|
||||
"""Advance the workspace lifecycle one stage (order-enforced)."""
|
||||
if stage not in WORKSPACE_LIFECYCLE_STAGES:
|
||||
raise ValueError(f"unknown workspace stage {stage!r}")
|
||||
done = record["stages_completed"]
|
||||
if done:
|
||||
last = WORKSPACE_LIFECYCLE_STAGES.index(done[-1])
|
||||
cur = WORKSPACE_LIFECYCLE_STAGES.index(stage)
|
||||
if cur != last + 1:
|
||||
raise ValueError(
|
||||
f"lifecycle order violation: {done[-1]} → {stage}")
|
||||
elif stage != WORKSPACE_LIFECYCLE_STAGES[0]:
|
||||
raise ValueError(f"lifecycle must start at "
|
||||
f"{WORKSPACE_LIFECYCLE_STAGES[0]}, got {stage!r}")
|
||||
done.append(stage)
|
||||
if evidence:
|
||||
record.setdefault("evidence", []).append(
|
||||
{"stage": stage, "evidence": evidence[:200]})
|
||||
return record
|
||||
@@ -109,8 +109,14 @@ def _redact_secrets(obj):
|
||||
dict key named like a credential has its value stripped before
|
||||
serialization. Structural stores must never become secret stores."""
|
||||
if isinstance(obj, dict):
|
||||
def _is_secret_key(key) -> bool:
|
||||
kl = str(key).lower()
|
||||
return (kl in _SECRET_KEY_NAMES
|
||||
or any(w in kl for w in ("token", "api_key", "apikey",
|
||||
"password", "secret",
|
||||
"credential", "private_key")))
|
||||
return {
|
||||
k: ("<redacted>" if str(k).lower() in _SECRET_KEY_NAMES else _redact_secrets(v)) for k, v in obj.items()
|
||||
k: ("<redacted>" if _is_secret_key(k) else _redact_secrets(v)) for k, v in obj.items()
|
||||
}
|
||||
if isinstance(obj, list):
|
||||
return [_redact_secrets(v) for v in obj]
|
||||
|
||||
@@ -52,6 +52,14 @@ def _save(data):
|
||||
os.replace(tmp_path, LEDGER_PATH)
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
"""Public read of the persisted task ledger (T17 17R: the TUI
|
||||
startup loader called task_persist.load() which did not exist —
|
||||
the AttributeError was silently swallowed and startup loading was
|
||||
dead code). Returns the raw ledger doc: {"tasks": [...]}."""
|
||||
return _load()
|
||||
|
||||
|
||||
def export(tasks_json):
|
||||
"""Save a list of tasks (from task tool output) to the ledger.
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# T17 §17F/§17G/§17H audits (2026-09-15, from parallel read-only sweeps)
|
||||
|
||||
## 17F — task-state surfaces
|
||||
1. TaskStateRepository (orchestration/conversation_state.py:111) — CANONICAL
|
||||
(durable JSONL + hot checkpoint, legal transitions, wired through composition)
|
||||
2. RuneState.tasks in operator.json (rune_operator.py:377) — DUPLICATE
|
||||
(double-written on every send_task; the one true duplicate authority pair)
|
||||
3. task_persist.py persisted.json — LEGACY/QUEUE (TUI startup load is dead code:
|
||||
calls load() which does not exist, silently swallowed)
|
||||
4. memory_store working table — CACHE (single consumer: its own CLI)
|
||||
5. auto_tasks.db — QUEUE (self-contained, no bridge)
|
||||
6. work_manager batch meta — QUEUE (task-adjacent, TUI /work only)
|
||||
CONSOLIDATION: #1 authority; migrate #2 writers (send_task → repo only,
|
||||
derive get_status/list_tasks), fix #3 dead load, keep #4-6 as bounded caches/queues.
|
||||
|
||||
## 17G — build_mode
|
||||
Unique: VCS worktree isolation (create/merge/delete) — NOT in Scaffolding;
|
||||
phase ledger/resume. NOT unique: DAG batching (work_manager), correction
|
||||
loop (counter only), complexity assess (placeholder). Plugin BuildStateMachine
|
||||
= second parallel orchestrator (DUPLICATE, only assess.py referenced).
|
||||
T15 classification lives in operator hook path; TUI /build start bypasses
|
||||
it and mints unclassified state.
|
||||
SMALLEST MIGRATION: _cmd_build start → classify first (same classify_mode),
|
||||
refuse NOT_CONSTRUCTION/UNKNOWN with probes, activate contract, then delegate
|
||||
to BuildManager as bounded adapter (worktree + resume ledger). Delete plugin
|
||||
duplicate.
|
||||
|
||||
## 17H — W/R daemon ownership (verdict)
|
||||
INTENTIONAL SHARED INFRASTRUCTURE with packaging debt. The bridge serves
|
||||
multiple consumers (deploy waiter as runner user, daemon as nick, MiMo
|
||||
sessions). Smallest clean migration: keep out-of-repo deployment BUT (a)
|
||||
versioned snapshot (git tag per release), (b) RUNE_WR_CODE discovery
|
||||
already implemented in event_wait, (c) health endpoint exists. Full
|
||||
in-repo packaging DEFERRED — the shared-root contract (0644/0666) is the
|
||||
load-bearing fix and it is now guarded by tests.
|
||||
@@ -0,0 +1,42 @@
|
||||
# T17 §17T — real-Rune re-entry benchmark (2026-09-15)
|
||||
|
||||
Target: the event/wait-resume subsystem. Two fresh-identity agents, no
|
||||
transcript. A: cold repo exploration. B: docs/STRUCTURAL_MAP.md first,
|
||||
then targeted reconciliation.
|
||||
|
||||
## Measured
|
||||
|
||||
| Metric | A cold | B map-assisted |
|
||||
|---|---|---|
|
||||
| Files opened | 6 | 4 (+ grep over ~10) |
|
||||
| Commands run | 14 (11 bash + 3 reads) | 13 (10 bash + 3 grep) |
|
||||
| Test runs | 1 (19 tests) | 1 (8 tests) |
|
||||
| Authority identified | correct (durable waitstore record) | correct |
|
||||
| Ownership identified | correct (out-of-repo bridge) | correct |
|
||||
| Unfinished/duplication found | CI adapter no production call site; bridge unversioned | CI coverage gap; TWO data roots (split-brain risk); t14 legacy; build_mode wired |
|
||||
| Wrong assumptions | **1 material**: claimed daemon DEAD with stale pidfile (false — PID 287505 live, /health ok) | 0 material; flagged env-pinning dependency |
|
||||
| Map reconciliation | n/a | 5 CONFIRMED, 1 UNVERIFIABLE, 0 DRIFTED |
|
||||
| Confidence | 80-95% | high/medium, calibrated |
|
||||
|
||||
## Honest verdict
|
||||
|
||||
1. **Raw cost ≈ equal** (6 vs 4 files, 14 vs 13 commands) — consistent
|
||||
with 16O: no step-count win on a well-structured repo.
|
||||
2. **Correctness gap is real**: the cold path asserted a FALSE runtime
|
||||
fact (daemon dead) from a stale pidfile and reported it at 95%
|
||||
confidence; the map-assisted path, knowing the daemon's deployment
|
||||
shape from the map, verified liveness and reported the env-pinning
|
||||
dependency instead. One false belief about a LIVE system is exactly
|
||||
the failure mode re-entry exists to prevent.
|
||||
3. **Coverage gap**: the map-assisted path found the two-data-roots
|
||||
split-brain risk (legacy ~/.local/share/mimo-wait-resume/waits still
|
||||
holds 46 records; wr_root() defaults there when RUNE_WR_ROOT unset);
|
||||
the cold path missed it.
|
||||
4. The map itself survived reconciliation with ZERO drifted claims —
|
||||
maintained structural projections are earning their update cost.
|
||||
|
||||
## Actionable finding (fed to 17U/W)
|
||||
The two-data-roots finding is real debt: `wr_root()` (event_wait.py)
|
||||
falls back to the legacy home when RUNE_WR_ROOT is unset. Production
|
||||
pins it (deploy.yml), but any unpinned consumer silently reads a stale
|
||||
store. Candidate: drop the legacy fallback or make it warn loudly.
|
||||
@@ -0,0 +1,109 @@
|
||||
# T17 §17V — Scaffolding freeze assessment + §17X adversarial matrix
|
||||
(2026-09-15)
|
||||
|
||||
## §17X — adversarial system matrix (36 cases → evidence)
|
||||
|
||||
| # | Case | Evidence |
|
||||
|---|------|----------|
|
||||
| 1 | explicit build request | 17Q-1 (PROJECT_BUILD, bootstrap) |
|
||||
| 2 | indirect authorization | 17Q-4 ("that's the one, make it happen") |
|
||||
| 3 | discussion only | 17Q-2 + 17L test (3 phrasings) |
|
||||
| 4 | ambiguous referent | 17Q-6 (asks, never guesses) |
|
||||
| 5 | stale referent | 17Q-8 (topic-shift resistance) |
|
||||
| 6 | superseded proposal | 17Q-20 |
|
||||
| 7 | option-selection referent | 17Q-5 (ordinal) |
|
||||
| 8 | new project | 17Q-14 |
|
||||
| 9 | existing project | 17Q-11 |
|
||||
| 10 | local change | 17Q-12 |
|
||||
| 11 | non-construction request | 17Q-10 ("build a list") |
|
||||
| 12 | research request | 17Q-9 |
|
||||
| 13 | provider unavailable | 16G-C (verbatim codes) |
|
||||
| 14 | provider manually changed | 16G-G (scaffold byte-identical) |
|
||||
| 15 | duplicate completion event | 15E double-resume tests |
|
||||
| 16 | completion event before waiter | 17C replay (4.6s prod-realistic) |
|
||||
| 17 | completion event after waiter | 17C cross-user delivery (2.05s) |
|
||||
| 18 | lost event | T16 16D live cycles (deadline recovery, promoted) |
|
||||
| 19 | controller restart | 15E restart tests + resume_or_recover |
|
||||
| 20 | stage restart | 17E wiring: already-closed skip test |
|
||||
| 21 | programme restart | 17E wiring: restart-from-durable-record test |
|
||||
| 22 | subtask≠programme | 15M canary + 17E failure isolation |
|
||||
| 23 | programme complete | 17E: PROGRAMME_COMPLETE / _WITH_FAILURES honest verdicts |
|
||||
| 24 | blocked stage | canary BLOCKED→fallback (15M tests) |
|
||||
| 25 | approval-required stage | canary APPROVAL_REQUIRED honest stop (15M) |
|
||||
| 26 | duplicate task state migration | 17F held-outs (counters from authority) |
|
||||
| 27 | legacy build route | 17G gate (/build start refused unless classified) |
|
||||
| 28 | command/NL equivalence | 17Q-15/16 (same classifier, same semantics) |
|
||||
| 29 | ContextIntent replay/idempotency | intents stateless; contracts idempotent (17E dup-signal test) |
|
||||
| 30 | invalid Substrate command | 16K refusal tests |
|
||||
| 31 | long-gap re-entry | 16O + 17T benchmarks |
|
||||
| 32 | repository drift | 16P VAT-regression blocks resume |
|
||||
| 33 | false-green test | stage_guards verify_test_truth (15F-I) |
|
||||
| 34 | Henxels structural failure | rc=0 all session; manifest gate caught every new module |
|
||||
| 35 | release stale publisher | T14B authority re-read (prior tranche, tests stand) |
|
||||
| 36 | deployment convergence | T16/T17: 0.66.63→0.66.66 auto-converged, verified |
|
||||
|
||||
Coverage honestly noted: cases 13-15, 19, 33, 35 rely on T14-T16 suites
|
||||
(green in this session's full runs); cases 16-18 carry THIS-tranche live
|
||||
production evidence.
|
||||
|
||||
## §17V — FREEZE ASSESSMENT
|
||||
|
||||
Checklist (mandate's 17 items):
|
||||
1. one canonical construction kernel — YES (scaffold.py + ONE
|
||||
classify_mode; T17 17G closed the /build bypass gate; 17U deleted the
|
||||
dead second orchestrator)
|
||||
2. native conversational/default activation — YES (operator hook path,
|
||||
T15; T17 added the conversational interpretation layer)
|
||||
3. explicit construction commands — YES (17K/17Q-1/7)
|
||||
4. contextual multi-turn activation — YES (17J-17N, 17Q matrix 22 cases)
|
||||
5. one canonical programme continuation mechanism — YES (17D/17E wired +
|
||||
live-proven)
|
||||
6. event-driven waiting canonical where events exist — YES (16C/17C;
|
||||
CI + builder adapters, replay, cross-user delivery)
|
||||
7. watchdog only as recovery/failure detection — YES (proven in
|
||||
production twice; wake path proven production-realistically)
|
||||
8. provider ownership outside Scaffolding — YES (16F structural guard)
|
||||
9. builder cannot self-certify — YES (phase-gated close; closure
|
||||
refusals observed live in T14)
|
||||
10. completion truth evidence-driven — YES (checkpoints on contracts;
|
||||
verify_test_truth)
|
||||
11. interruption/resume proven — YES (T14E live; 17E restart tests)
|
||||
12. long-gap re-entry supported — YES (drift_resume + 16O/17T honest
|
||||
benchmarks)
|
||||
13. Rune itself uses Scaffolding — YES (T16 16R, T17 17R/Y contracts
|
||||
closed through the real flow)
|
||||
14. future Substrate seam stable — YES (16I/J/K; SCHEMA_VERSION=1;
|
||||
zero direct-store coupling tests)
|
||||
15. major duplicate construction authorities removed or consciously
|
||||
deprecated — YES with residuals (plugin deleted; /build gated;
|
||||
build_mode kept as bounded adapter BY DECISION; task-state cache
|
||||
demoted by design, full writer retirement deferred)
|
||||
16. real-world generalisation begun — PARTIAL (fixtures + Rune itself;
|
||||
no external project yet)
|
||||
17. remaining work mostly bugs/edge/docs/adoption — YES (16W/17U debt
|
||||
lists are small and bounded)
|
||||
|
||||
## VERDICT: FREEZE_READY (with named residuals)
|
||||
|
||||
Rationale: every architecture-level capability the freeze checklist
|
||||
names is implemented, canonical, and verified — most of them with live
|
||||
production evidence from T16/T17. The residuals are deliberately
|
||||
bounded and none is architectural:
|
||||
- build_mode worktree adapter retirement (needs a migration plan, not
|
||||
new architecture)
|
||||
- task-state cache writer retirement (demoted, consumers derived)
|
||||
- replay-path first NATURAL cycle confirmation (mechanism proven
|
||||
production-realistically; only the natural-timing capture pending)
|
||||
- external-project generalisation (adoption, not construction)
|
||||
|
||||
Recommended posture: stop adding Scaffolding architecture. Future work:
|
||||
USE it (Rune retrofit completion, real projects), fix observed defects,
|
||||
simplify, stabilise — exactly the direction the mandate anticipated.
|
||||
|
||||
## §17Z POSTSCRIPT — natural event-path proof (19:15Z, final release cycle)
|
||||
The 17C residual closed ITSELF on the final release: deploy waiter for
|
||||
927548e registered 19:13:28, CI completion wake received 19:15:16,
|
||||
controller resumed ON THE EVENT (mode="event", no rearm, no deadline),
|
||||
authority re-read verdict success at 19:15:20, wall 112.5s, PROMOTED.
|
||||
First NATURAL production event-driven CI wake. v0.66.72 released,
|
||||
deployed current = 927548e = gitea/master = release. Full convergence.
|
||||
@@ -0,0 +1,39 @@
|
||||
# T18 §18A — exact current-state audit (2026-09-15, fresh verification)
|
||||
|
||||
All facts independently verified this session; nothing trusted from the
|
||||
T17 report without re-verification.
|
||||
|
||||
## Identity / release chain
|
||||
- HEAD = gitea/master = **29570f4** (T17 17Z postscript)
|
||||
- Deployed current → 29570f4 (symlink 20:22); last-good → 927548e (0.66.72's
|
||||
bump parent)
|
||||
- Release **v0.66.72** (20:06:36); the post-report doc commit DRAINED and
|
||||
DEPLOYED — T17's "draining" state is fully converged.
|
||||
|
||||
## Gates (lock-exact environments, 29570f4)
|
||||
- Full suite: **1920 passed + 84 subtests** (127s)
|
||||
- Lint ratchet: PASS (BLE001 350/350, S110 78/78 — post-17U shrink)
|
||||
- Type ratchet: PASS — errors **111** (baseline 117 — IMPROVED, honest
|
||||
high-water keeps the lower count), warnings 435 (≤443)
|
||||
- Henxels: rc=0
|
||||
|
||||
## Infrastructure
|
||||
- W/R daemon: pid 287505, /health ok, data root /mnt/workdrive/wr-bridge
|
||||
(shared root; wr_root() anti-split-brain policy active)
|
||||
- Spine store: 148 entities (139 module, 8 subsystem, 1 deployment_surface),
|
||||
636 relationships
|
||||
- Scaffolding store: 28 contracts — now ALL terminal after 18A
|
||||
reconciliation; 3 durable programme records (17E proofs)
|
||||
|
||||
## 18A drift reconciliation performed
|
||||
- 8 orphan contracts from superseded 17E proof attempts (17e-stage-*,
|
||||
17e2-stage-*) were draft/approved in the main store — abandoned
|
||||
truthfully with reconciliation checkpoints. Open contracts: ZERO.
|
||||
|
||||
## Current routes (verified by presence, detailed audits follow)
|
||||
- Task-state authority: TaskStateRepository (17F)
|
||||
- build_mode: bounded adapter behind the /build classification gate (17G)
|
||||
- Contextual routes: construction_intent → classify_mode (17J-Q),
|
||||
request-modals + information-opener precedence live
|
||||
- programme continuation: run_construction_programme wired + live-proven
|
||||
- structural map: docs/STRUCTURAL_MAP.md (updated 17T)
|
||||
@@ -0,0 +1,22 @@
|
||||
# T18 §18D CORRECTION — contextual research scope (2026-09-15, Nick)
|
||||
|
||||
CORRECTION: the 18D research topics (architecture graphs, dependency
|
||||
graphs, service topology, monorepo tooling, ADRs, graph formats) were
|
||||
EXAMPLES relevant to the structural-model task — not a universal
|
||||
research checklist. Scaffolding must never hard-code research topics.
|
||||
|
||||
IMPLEMENTED:
|
||||
- build_basis.generate_research_plan(): context-specific plan synthesis
|
||||
(intent → target/reality → unknowns/decisions/risks → topics). No
|
||||
topic vocabulary in code. Depth by consequence heuristics, always
|
||||
with WHY.
|
||||
- Held-outs (tests/test_contextual_research_plan.py): unrelated intents
|
||||
produce materially different plans; the 18D architecture topics do
|
||||
NOT leak into unrelated research; no-unknowns → depth NONE with empty
|
||||
topics; prior-art machinery summons only when unknowns reference
|
||||
existing practice, phrased for THIS intent.
|
||||
- Principle persisted in docs/SCAFFOLDING_V1_DEFINITION.md.
|
||||
|
||||
The 18D Build Basis (research/t18-structural-model-prior-art) remains
|
||||
VALID: those topics were contextually correct for designing the
|
||||
structural model itself.
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"alternatives_considered": [],
|
||||
"created_at": "2026-09-15T21:13:20Z",
|
||||
"deeper": {
|
||||
"findings": [],
|
||||
"refs": [],
|
||||
"used": false
|
||||
},
|
||||
"depth": {
|
||||
"decided_at": "2026-09-15T21:13:20Z",
|
||||
"depth": "FULL",
|
||||
"mode": "",
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"why": "route RESEARCHED_CHANGE default"
|
||||
},
|
||||
"dispositions": [
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Backstage relation vocabulary + entity-ref discipline + catalog-info.yaml emission",
|
||||
"ref": "backstage.io",
|
||||
"why": "Domain/System/Component/Resource/API maps 1:1 to our nesting; ecosystem interop free"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Kythe edge-kind vocabulary (ref/call, property/reads-writes, satisfies, overrides) + derived-reverse-edge rule",
|
||||
"ref": "kythe.io/docs/schema",
|
||||
"why": "disciplined, versioned, explicit"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "SCIP monikers for cross-index identity bridging",
|
||||
"ref": "scip",
|
||||
"why": "decouples stable identity from any one indexer"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Nx affected semantics as impact-closure spec (diff\u2192owning entities\u2192reverse-dep closure)",
|
||||
"ref": "nx.dev/concepts/affected",
|
||||
"why": "productionized impact_frontier"
|
||||
},
|
||||
{
|
||||
"disposition": "ADOPT",
|
||||
"item": "Bazel rdeps(kind(.*_test)) as verified_by-driven verification scoping",
|
||||
"ref": "bazel.build",
|
||||
"why": "semantic template for test-surface queries"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "Kythe VName stability contract",
|
||||
"ref": "kythe.io",
|
||||
"why": "Kythe explicitly declines cross-version stability \u2014 our persistent model REQUIRES it; keep deterministic regeneration but promise stability"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "C4 deployment shapes (DeploymentNode/ContainerInstance)",
|
||||
"ref": "c4model.com",
|
||||
"why": "become machine-derived deployed_by/connects_to with evidence, not hand-authored"
|
||||
},
|
||||
{
|
||||
"disposition": "ADAPT",
|
||||
"item": "Glean immutable-batch layering",
|
||||
"ref": "glean.software",
|
||||
"why": "our snapshots are philosophically identical; mark externally-derived edges with distinct authority"
|
||||
},
|
||||
{
|
||||
"disposition": "COMBINE",
|
||||
"item": "Backstage org model + Kythe code model + Nx traversal COMBINED on our evidence-gated store",
|
||||
"ref": "combined",
|
||||
"why": "prior art occupies one layer each; the cross-layer join is the actual design"
|
||||
},
|
||||
{
|
||||
"disposition": "REJECT",
|
||||
"item": "Adopting CodeQL/Kythe/Glean as our store core; generic graph DB (Neo4j); LSIF; Stack Graphs (archived 2025-09)",
|
||||
"ref": "various",
|
||||
"why": "none carry evidence_refs/authority/status/maturity as first-class; heavyweight or dead"
|
||||
},
|
||||
{
|
||||
"disposition": "INNOVATE",
|
||||
"item": "Consent-gated schema migration; verified_by edges to verification entities; deployed_by with code linkage; authority-graded evidence on every edge",
|
||||
"ref": "18V assessment",
|
||||
"why": "genuinely absent from all surveyed systems"
|
||||
}
|
||||
],
|
||||
"implications": {
|
||||
"architecture": [],
|
||||
"migration": [],
|
||||
"structural": [],
|
||||
"verification": []
|
||||
},
|
||||
"inward": {
|
||||
"findings": [],
|
||||
"refs": []
|
||||
},
|
||||
"issue_ref": "research/t18-structural-model-prior-art",
|
||||
"kind": "build_basis",
|
||||
"outward": {
|
||||
"findings": [],
|
||||
"prior_art_questions_answered": [
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required"
|
||||
],
|
||||
"refs": [
|
||||
"https://backstage.io/docs/features/software-catalog/system-model/",
|
||||
"https://kythe.io/docs/schema/",
|
||||
"https://github.com/scip-code/scip",
|
||||
"https://nx.dev/concepts/affected",
|
||||
"https://c4model.com/",
|
||||
"https://bazel.build/query/language",
|
||||
"https://github.com/dependency-cruiser/dependency-cruiser"
|
||||
]
|
||||
},
|
||||
"question": "What existing systems/modeling approaches should Scaffolding's structural Scaffold adopt, adapt, combine, or reject?",
|
||||
"risks": [],
|
||||
"schema_version": 1,
|
||||
"uncertainty": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"target": "component:structural_model",
|
||||
"affected_domains": [
|
||||
"domain:structural-awareness"
|
||||
],
|
||||
"affected_subsystems": [],
|
||||
"consumers_connectors": [],
|
||||
"candidate_files": [],
|
||||
"contract_scope_additions": [],
|
||||
"state_implications": [
|
||||
"state_store:structural-store"
|
||||
],
|
||||
"environment_runtime_implications": [],
|
||||
"migration_implications": [],
|
||||
"risks_uncertainties": [
|
||||
{
|
||||
"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed lookup/late-binding consumers \u2014 reconcilers must verify at runtime"
|
||||
}
|
||||
],
|
||||
"verification_plan": [
|
||||
"test_suite:scaffolding-tests"
|
||||
],
|
||||
"rollback_implications": [
|
||||
"revert contract scope commits; deployment rollbacks via last-good symlink (canonical deploy machinery)"
|
||||
],
|
||||
"basis_summary": {
|
||||
"depth": "INWARD",
|
||||
"why": "depth supplied by caller (recorded, caller accountable)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"contract_id": "18q-graph-projection",
|
||||
"construction_base": "b7865a76bbdb",
|
||||
"runtime_generation": "dep-29570f4",
|
||||
"candidate_generation": "",
|
||||
"promotion_target": "gitea/master",
|
||||
"rollback_generation": "",
|
||||
"stages_completed": [
|
||||
"base_verified",
|
||||
"worktree_created",
|
||||
"construction",
|
||||
"verified",
|
||||
"committed"
|
||||
],
|
||||
"lifecycle": [
|
||||
"base_verified",
|
||||
"worktree_created",
|
||||
"construction",
|
||||
"verified",
|
||||
"committed",
|
||||
"pushed",
|
||||
"ci_qualified",
|
||||
"released",
|
||||
"deployment_confirmed",
|
||||
"evidence_archived",
|
||||
"cleaned"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"stage": "worktree_created",
|
||||
"evidence": "harness worktree (existing dev worktree, rule P)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# T18 §18R/§18S — adversarial matrix + V1 gap reassessment (2026-09-15)
|
||||
|
||||
## §18R — 20-case matrix → evidence
|
||||
|
||||
| # | Case | Evidence |
|
||||
|---|------|----------|
|
||||
| 1 | stale Scaffold | test_18r_stale_scaffold_reconciles_via_drift (MATERIAL + reconciliation) |
|
||||
| 2 | changed file location | 18H moved-file detection (src/widgets/widget.py) |
|
||||
| 3 | unknown dynamic consumer | test_18r_dynamic_consumers_stay_uncertain (explicit uncertainty) |
|
||||
| 4 | broken connector | structural_model: consumers/depends_on status field; stale edges excluded from queries |
|
||||
| 5 | research unavailable | build_basis: unqualified basis → is_qualified False; contracts cannot attach qualified=false silently |
|
||||
| 6 | research contradicts architecture assumption | dispositions carry REJECT with why (18D basis: REJECT graph-DB adoption) |
|
||||
| 7 | target expands outside scope | rule J: reconcile/expand explicitly — impact closure marks non_goals + uncertainty; contract scope amendments user-gated (scaffold.py) |
|
||||
| 8 | user rejects material schema upgrade | test_18r_user_rejects_material_upgrade (ConsentRequired) |
|
||||
| 9 | minor compatible migration | classify_migration MINOR = automatic (test_18i) |
|
||||
| 10 | dirty worktree | test_18r_dirty_worktree_is_visible_not_fatal (detect + record, policy per-contract) |
|
||||
| 11 | interrupted construction | T14E SIGKILL live + 17E restart held-outs |
|
||||
| 12 | provider unavailable | 16G-C verbatim status codes; PROVIDER_UNAVAILABLE blocks branch |
|
||||
| 13 | CI failure | T16 16D: deploy refused truthfully on red |
|
||||
| 14 | deployment failure | runs 7953/7958 failed fast+truthful (T16 17C finding) |
|
||||
| 15 | old runtime + candidate schema incompatibility | consent machinery: material migrations gated per project (18I) |
|
||||
| 16 | secret present in env/config | test_18r_secret_in_env_does_not_block_construction (rule O: continues) |
|
||||
| 17 | secret in generated output | test_18r_secret_value_redacted_in_store — **FOUND REAL BUG: *_token keys were not redacted; fixed (substring match now)** |
|
||||
| 18 | forge unavailable | test_18r_forge_unavailable_raises_explicitly (ForgeUnavailable, never truth change) |
|
||||
| 19 | structural graph projection stale | test_18q_projection_is_read_only + projection regenerated on model change (18P step 9) |
|
||||
| 20 | /build route backend bypass | 18M: /build start mints canonical contract (test_native_default + gate) |
|
||||
| 21 | contextual ambiguous authorization | 17Q-6 + 18N multiple-proposal terse selection |
|
||||
|
||||
**Matrix yield: 2 real defects found and fixed** (secret redaction gap;
|
||||
drift source-field mismatch) — the matrix is load-bearing, not decorative.
|
||||
|
||||
## §18S — V1 gap re-score (post-T18)
|
||||
|
||||
| North Star capability | T18-start | T18-end |
|
||||
|---|---|---|
|
||||
| Research-led construction (Build Basis) | ABSENT | **TEST_PROVEN + one QUALIFIED real basis (18D)** |
|
||||
| Contextual research plan (correction) | — | **TEST_PROVEN (disjoint-plan held-outs)** |
|
||||
| OUTWARD precedent-first | PARTIAL | **TEST_PROVEN (contract questions + real run)** |
|
||||
| First-class structural Scaffold | PARTIAL | **TEST_PROVEN + LIVE on Scaffolding itself (18O)** |
|
||||
| Structure→scope→governance | PARTIAL | **TEST_PROVEN (closure→contract 18P)** |
|
||||
| Visible impact preview | ABSENT | **TEST_PROVEN (18G + real 18P preview persisted)** |
|
||||
| Structural drift | PARTIAL | **TEST_PROVEN (18H; 2 bugs found by matrix)** |
|
||||
| Schema versioning + consent | PARTIAL | **TEST_PROVEN (18I; consent structurally enforced)** |
|
||||
| Environment/runtime model | PARTIAL | **TEST_PROVEN (env/deployment entities + distinction)** |
|
||||
| Workspace lifecycle | PARTIAL | **TEST_PROVEN (18K order-enforced; adapter retained)** |
|
||||
| Forge boundary | ABSENT | **TEST_PROVEN (18L; Gitea proven by every push this session)** |
|
||||
| Full route convergence | PARTIAL | **TEST_PROVEN (18M: /build start mints contracts)** |
|
||||
| Contextual generalisation | TEST_PROVEN | **TEST_PROVEN+ (18N: 4 semantic gaps fixed)** |
|
||||
| Self-scaffold | PARTIAL | **LIVE (18O consistency-enforced)** |
|
||||
| Upgrade-via-Scaffold | ABSENT | **LIVE_PROVEN (18P: 11 scaffold-driven steps)** |
|
||||
| Graph projection | ABSENT | **TEST_PROVEN (18Q, 3 formats)** |
|
||||
| Memory/Substrate | ABSENT | ABSENT (BY DESIGN — rule L) |
|
||||
| Kernel behavior changes | — | **ZERO frozen-kernel exceptions (rule B record: none needed)** |
|
||||
|
||||
## VERDICT: RETROFIT_READY (for the Rune-wide retrofit tranche)
|
||||
|
||||
Why: every V1 foundation capability is implemented, test-proven, and —
|
||||
where the tranche could — live-proven on Scaffolding itself (self-
|
||||
scaffold, scaffold-driven upgrade, qualified research basis from real
|
||||
prior-art work). The adversarial matrix made the system better (2 real
|
||||
fixes). What remains is exactly the mandate's next phase: applying this
|
||||
discipline across Rune's remaining subsystems (Spine modernisation,
|
||||
research pipeline modeling, release/deploy structural entities) and
|
||||
real external projects — plus natural-production confirmation of the
|
||||
event-replay path (mechanism proven; natural timing pending, as in T17).
|
||||
Not full V1: generalisation beyond one project is still ahead.
|
||||
@@ -0,0 +1,84 @@
|
||||
digraph structural {
|
||||
rankdir=LR;
|
||||
node [shape=box, style="rounded,filled", fillcolor="#eef"];
|
||||
"component:build_mode_adapter" [label="build_mode_adapter (component)"];
|
||||
"component:build_basis" [label="build_basis (component)"];
|
||||
"component:construction_activation" [label="construction_activation (component)"];
|
||||
"component:construction_decision" [label="construction_decision (component)"];
|
||||
"component:construction_intent" [label="construction_intent (component)"];
|
||||
"component:construction_interface" [label="construction_interface (component)"];
|
||||
"component:construction_mode" [label="construction_mode (component)"];
|
||||
"component:drift_resume" [label="drift_resume (component)"];
|
||||
"component:event_wait" [label="event_wait (component)"];
|
||||
"component:failure_recovery" [label="failure_recovery (component)"];
|
||||
"component:forge" [label="forge (component)"];
|
||||
"component:graph_projection" [label="graph_projection (component)"];
|
||||
"component:impact_preview" [label="impact_preview (component)"];
|
||||
"component:programme_continuation" [label="programme_continuation (component)"];
|
||||
"component:scaffold" [label="scaffold (component)"];
|
||||
"component:stage_guards" [label="stage_guards (component)"];
|
||||
"component:structural_migration" [label="structural_migration (component)"];
|
||||
"component:structural_model" [label="structural_model (component)"];
|
||||
"deployment_surface:rune-code-current" [label="rune-code-current (deployment_surface)"];
|
||||
"domain:construction-governance" [label="construction-governance (domain)"];
|
||||
"domain:execution-integration" [label="execution-integration (domain)"];
|
||||
"domain:structural-awareness" [label="structural-awareness (domain)"];
|
||||
"environment:production" [label="production (environment)"];
|
||||
"interface:builder-boundary" [label="builder-boundary (interface)"];
|
||||
"state_store:scaffold-store" [label="scaffold-store (state_store)"];
|
||||
"state_store:structural-store" [label="structural-store (state_store)"];
|
||||
"subsystem:execution" [label="execution (subsystem)"];
|
||||
"subsystem:interpretation" [label="interpretation (subsystem)"];
|
||||
"subsystem:kernel" [label="kernel (subsystem)"];
|
||||
"subsystem:structural" [label="structural (subsystem)"];
|
||||
"test_suite:scaffolding-tests" [label="scaffolding-tests (test_suite)"];
|
||||
"project:scaffolding" [label="scaffolding (project)"];
|
||||
"component:build_mode_adapter" -> "subsystem:kernel" [label="legacy_of"];
|
||||
"component:build_basis" -> "state_store:structural-store" [label="reads"];
|
||||
"component:build_basis" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:build_mode_adapter" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:construction_activation" -> "subsystem:interpretation" [label="belongs_to"];
|
||||
"component:construction_decision" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:construction_intent" -> "component:construction_mode" [label="consumes"];
|
||||
"component:construction_intent" -> "subsystem:interpretation" [label="belongs_to"];
|
||||
"component:construction_interface" -> "interface:builder-boundary" [label="provides"];
|
||||
"component:construction_interface" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:construction_mode" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:drift_resume" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:event_wait" -> "component:programme_continuation" [label="connects_to"];
|
||||
"component:event_wait" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:failure_recovery" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:forge" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:forge" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:graph_projection" -> "state_store:structural-store" [label="reads"];
|
||||
"component:graph_projection" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:graph_projection" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:impact_preview" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:programme_continuation" -> "subsystem:execution" [label="belongs_to"];
|
||||
"component:programme_continuation" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:scaffold" -> "component:construction_mode" [label="depends_on"];
|
||||
"component:scaffold" -> "deployment_surface:rune-code-current" [label="deployed_by"];
|
||||
"component:scaffold" -> "state_store:scaffold-store" [label="owns"];
|
||||
"component:scaffold" -> "state_store:structural-store" [label="reads"];
|
||||
"component:scaffold" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:scaffold" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"component:stage_guards" -> "subsystem:kernel" [label="belongs_to"];
|
||||
"component:structural_migration" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:structural_model" -> "state_store:structural-store" [label="owns"];
|
||||
"component:structural_model" -> "subsystem:structural" [label="belongs_to"];
|
||||
"component:structural_model" -> "test_suite:scaffolding-tests" [label="verified_by"];
|
||||
"deployment_surface:rune-code-current" -> "environment:production" [label="connects_to"];
|
||||
"deployment_surface:rune-code-current" -> "project:scaffolding" [label="belongs_to"];
|
||||
"domain:construction-governance" -> "project:scaffolding" [label="belongs_to"];
|
||||
"domain:execution-integration" -> "project:scaffolding" [label="belongs_to"];
|
||||
"domain:structural-awareness" -> "project:scaffolding" [label="belongs_to"];
|
||||
"environment:production" -> "project:scaffolding" [label="belongs_to"];
|
||||
"interface:builder-boundary" -> "subsystem:execution" [label="belongs_to"];
|
||||
"state_store:scaffold-store" -> "project:scaffolding" [label="belongs_to"];
|
||||
"state_store:structural-store" -> "project:scaffolding" [label="belongs_to"];
|
||||
"subsystem:execution" -> "domain:execution-integration" [label="belongs_to"];
|
||||
"subsystem:interpretation" -> "domain:construction-governance" [label="belongs_to"];
|
||||
"subsystem:kernel" -> "domain:construction-governance" [label="belongs_to"];
|
||||
"subsystem:structural" -> "domain:structural-awareness" [label="belongs_to"];
|
||||
"test_suite:scaffolding-tests" -> "project:scaffolding" [label="belongs_to"];
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
{
|
||||
"edge_count": 49,
|
||||
"edges": [
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_mode_adapter",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "legacy_of"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_basis",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "reads"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_basis",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:build_mode_adapter",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_activation",
|
||||
"status": "active",
|
||||
"to": "subsystem:interpretation",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_decision",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_intent",
|
||||
"status": "active",
|
||||
"to": "component:construction_mode",
|
||||
"type": "consumes"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_intent",
|
||||
"status": "active",
|
||||
"to": "subsystem:interpretation",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_interface",
|
||||
"status": "active",
|
||||
"to": "interface:builder-boundary",
|
||||
"type": "provides"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_interface",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:construction_mode",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:drift_resume",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:event_wait",
|
||||
"status": "active",
|
||||
"to": "component:programme_continuation",
|
||||
"type": "connects_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:event_wait",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:failure_recovery",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:forge",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:forge",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:graph_projection",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "reads"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:graph_projection",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:graph_projection",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:impact_preview",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:programme_continuation",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:programme_continuation",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "component:construction_mode",
|
||||
"type": "depends_on"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "deployment_surface:rune-code-current",
|
||||
"type": "deployed_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "state_store:scaffold-store",
|
||||
"type": "owns"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "reads"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:scaffold",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:stage_guards",
|
||||
"status": "active",
|
||||
"to": "subsystem:kernel",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_migration",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_model",
|
||||
"status": "active",
|
||||
"to": "state_store:structural-store",
|
||||
"type": "owns"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_model",
|
||||
"status": "active",
|
||||
"to": "subsystem:structural",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "component:structural_model",
|
||||
"status": "active",
|
||||
"to": "test_suite:scaffolding-tests",
|
||||
"type": "verified_by"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "deployment_surface:rune-code-current",
|
||||
"status": "active",
|
||||
"to": "environment:production",
|
||||
"type": "connects_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "deployment_surface:rune-code-current",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "domain:construction-governance",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "domain:execution-integration",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "domain:structural-awareness",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "environment:production",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "interface:builder-boundary",
|
||||
"status": "active",
|
||||
"to": "subsystem:execution",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "state_store:scaffold-store",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "state_store:structural-store",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:execution",
|
||||
"status": "active",
|
||||
"to": "domain:execution-integration",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:interpretation",
|
||||
"status": "active",
|
||||
"to": "domain:construction-governance",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:kernel",
|
||||
"status": "active",
|
||||
"to": "domain:construction-governance",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "subsystem:structural",
|
||||
"status": "active",
|
||||
"to": "domain:structural-awareness",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "test_suite:scaffolding-tests",
|
||||
"status": "active",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"from": "project:scaffolding",
|
||||
"status": "root",
|
||||
"to": "project:scaffolding",
|
||||
"type": "belongs_to"
|
||||
}
|
||||
],
|
||||
"generated_at": "2026-09-15T22:10:51Z",
|
||||
"node_count": 32,
|
||||
"nodes": [
|
||||
{
|
||||
"ext": {
|
||||
"bounded_adapter": true,
|
||||
"source": "src/rune/build_mode.py"
|
||||
},
|
||||
"id": "component:build_mode_adapter",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "build_mode_adapter"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/build_basis.py"
|
||||
},
|
||||
"id": "component:build_basis",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "build_basis"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_activation.py"
|
||||
},
|
||||
"id": "component:construction_activation",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_activation"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_decision.py"
|
||||
},
|
||||
"id": "component:construction_decision",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_decision"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_intent.py"
|
||||
},
|
||||
"id": "component:construction_intent",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_intent"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_interface.py"
|
||||
},
|
||||
"id": "component:construction_interface",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_interface"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/construction_mode.py"
|
||||
},
|
||||
"id": "component:construction_mode",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction_mode"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/drift_resume.py"
|
||||
},
|
||||
"id": "component:drift_resume",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "drift_resume"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/event_wait.py"
|
||||
},
|
||||
"id": "component:event_wait",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "event_wait"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/failure_recovery.py"
|
||||
},
|
||||
"id": "component:failure_recovery",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "failure_recovery"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/forge.py"
|
||||
},
|
||||
"id": "component:forge",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "forge"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/graph_projection.py"
|
||||
},
|
||||
"id": "component:graph_projection",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "graph_projection"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/impact_preview.py"
|
||||
},
|
||||
"id": "component:impact_preview",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "impact_preview"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/programme_continuation.py"
|
||||
},
|
||||
"id": "component:programme_continuation",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "programme_continuation"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/scaffold.py"
|
||||
},
|
||||
"id": "component:scaffold",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "scaffold"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/stage_guards.py"
|
||||
},
|
||||
"id": "component:stage_guards",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "stage_guards"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/structural_migration.py"
|
||||
},
|
||||
"id": "component:structural_migration",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural_migration"
|
||||
},
|
||||
{
|
||||
"ext": {
|
||||
"source": "src/rune/structural_model.py"
|
||||
},
|
||||
"id": "component:structural_model",
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural_model"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "deployment_surface:rune-code-current",
|
||||
"kind": "deployment_surface",
|
||||
"maturity": "DECLARED",
|
||||
"name": "rune-code-current"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "domain:construction-governance",
|
||||
"kind": "domain",
|
||||
"maturity": "DECLARED",
|
||||
"name": "construction-governance"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "domain:execution-integration",
|
||||
"kind": "domain",
|
||||
"maturity": "DECLARED",
|
||||
"name": "execution-integration"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "domain:structural-awareness",
|
||||
"kind": "domain",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural-awareness"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "environment:production",
|
||||
"kind": "environment",
|
||||
"maturity": "DECLARED",
|
||||
"name": "production"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "interface:builder-boundary",
|
||||
"kind": "interface",
|
||||
"maturity": "DECLARED",
|
||||
"name": "builder-boundary"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "state_store:scaffold-store",
|
||||
"kind": "state_store",
|
||||
"maturity": "DECLARED",
|
||||
"name": "scaffold-store"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "state_store:structural-store",
|
||||
"kind": "state_store",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural-store"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:execution",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "execution"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:interpretation",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "interpretation"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:kernel",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "kernel"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "subsystem:structural",
|
||||
"kind": "subsystem",
|
||||
"maturity": "DECLARED",
|
||||
"name": "structural"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "test_suite:scaffolding-tests",
|
||||
"kind": "test_suite",
|
||||
"maturity": "DECLARED",
|
||||
"name": "scaffolding-tests"
|
||||
},
|
||||
{
|
||||
"ext": {},
|
||||
"id": "project:scaffolding",
|
||||
"kind": "project",
|
||||
"maturity": "DECLARED",
|
||||
"name": null
|
||||
}
|
||||
],
|
||||
"project": "scaffolding",
|
||||
"schema_version": 1
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
graph LR
|
||||
n1["build_mode_adapter (component)"]
|
||||
n0["build_basis (component)"]
|
||||
n2["construction_activation (component)"]
|
||||
n3["construction_decision (component)"]
|
||||
n4["construction_intent (component)"]
|
||||
n5["construction_interface (component)"]
|
||||
n6["construction_mode (component)"]
|
||||
n7["drift_resume (component)"]
|
||||
n8["event_wait (component)"]
|
||||
n9["failure_recovery (component)"]
|
||||
n10["forge (component)"]
|
||||
n11["graph_projection (component)"]
|
||||
n12["impact_preview (component)"]
|
||||
n13["programme_continuation (component)"]
|
||||
n14["scaffold (component)"]
|
||||
n15["stage_guards (component)"]
|
||||
n16["structural_migration (component)"]
|
||||
n17["structural_model (component)"]
|
||||
n18["rune-code-current (deployment_surface)"]
|
||||
n19["construction-governance (domain)"]
|
||||
n20["execution-integration (domain)"]
|
||||
n21["structural-awareness (domain)"]
|
||||
n22["production (environment)"]
|
||||
n23["builder-boundary (interface)"]
|
||||
n25["scaffold-store (state_store)"]
|
||||
n26["structural-store (state_store)"]
|
||||
n27["execution (subsystem)"]
|
||||
n28["interpretation (subsystem)"]
|
||||
n29["kernel (subsystem)"]
|
||||
n30["structural (subsystem)"]
|
||||
n31["scaffolding-tests (test_suite)"]
|
||||
n24["scaffolding (project)"]
|
||||
n1 -->|legacy_of| n29
|
||||
n0 -->|reads| n26
|
||||
n0 -->|belongs_to| n30
|
||||
n1 -->|belongs_to| n27
|
||||
n2 -->|belongs_to| n28
|
||||
n3 -->|belongs_to| n29
|
||||
n4 -->|consumes| n6
|
||||
n4 -->|belongs_to| n28
|
||||
n5 -->|provides| n23
|
||||
n5 -->|belongs_to| n27
|
||||
n6 -->|belongs_to| n29
|
||||
n7 -->|belongs_to| n30
|
||||
n8 -->|connects_to| n13
|
||||
n8 -->|belongs_to| n27
|
||||
n9 -->|belongs_to| n29
|
||||
n10 -->|belongs_to| n27
|
||||
n10 -->|verified_by| n31
|
||||
n11 -->|reads| n26
|
||||
n11 -->|belongs_to| n30
|
||||
n11 -->|verified_by| n31
|
||||
n12 -->|belongs_to| n30
|
||||
n13 -->|belongs_to| n27
|
||||
n13 -->|verified_by| n31
|
||||
n14 -->|depends_on| n6
|
||||
n14 -->|deployed_by| n18
|
||||
n14 -->|owns| n25
|
||||
n14 -->|reads| n26
|
||||
n14 -->|belongs_to| n29
|
||||
n14 -->|verified_by| n31
|
||||
n15 -->|belongs_to| n29
|
||||
n16 -->|belongs_to| n30
|
||||
n17 -->|owns| n26
|
||||
n17 -->|belongs_to| n30
|
||||
n17 -->|verified_by| n31
|
||||
n18 -->|connects_to| n22
|
||||
n18 -->|belongs_to| n24
|
||||
n19 -->|belongs_to| n24
|
||||
n20 -->|belongs_to| n24
|
||||
n21 -->|belongs_to| n24
|
||||
n22 -->|belongs_to| n24
|
||||
n23 -->|belongs_to| n27
|
||||
n25 -->|belongs_to| n24
|
||||
n26 -->|belongs_to| n24
|
||||
n27 -->|belongs_to| n20
|
||||
n28 -->|belongs_to| n19
|
||||
n29 -->|belongs_to| n19
|
||||
n30 -->|belongs_to| n21
|
||||
n31 -->|belongs_to| n24
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"programme_id": "t17-17e-proof-2",
|
||||
"objective": "17E live proof: 4 stages, async event wait, failure, auto-continuation",
|
||||
"stages": [
|
||||
{
|
||||
"stage_id": "17e2-stage-1-governance",
|
||||
"objective": "governance-only stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"stage_id": "17e2-stage-2-async",
|
||||
"objective": "async work under event bridge",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"stage_id": "17e2-stage-3-fails",
|
||||
"objective": "deliberately failing stage",
|
||||
"state": "FAILED",
|
||||
"depends_on": [],
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"stage_id": "17e2-stage-4-final",
|
||||
"objective": "final stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 3
|
||||
}
|
||||
],
|
||||
"fallbacks": [],
|
||||
"saved_at": "2026-09-15T17:51:22Z"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"programme_id": "t17-17e-proof-3",
|
||||
"objective": "17E live proof: async event wait, failure isolation, auto-continuation, honest terminal verdict",
|
||||
"stages": [
|
||||
{
|
||||
"stage_id": "17e3-stage-1-governance",
|
||||
"objective": "governance-only stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"stage_id": "17e3-stage-2-async",
|
||||
"objective": "async work under event bridge",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"stage_id": "17e3-stage-3-fails",
|
||||
"objective": "deliberately failing stage",
|
||||
"state": "FAILED",
|
||||
"depends_on": [],
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"stage_id": "17e3-stage-4-final",
|
||||
"objective": "final stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 3
|
||||
}
|
||||
],
|
||||
"fallbacks": [],
|
||||
"saved_at": "2026-09-15T17:52:31Z"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"programme_id": "t17-17e-proof",
|
||||
"objective": "prove canonical programme continuation: 4 stages incl. async event wait, failure, auto-continuation",
|
||||
"stages": [
|
||||
{
|
||||
"stage_id": "17e-stage-1-governance",
|
||||
"objective": "governance-only stage",
|
||||
"state": "COMPLETE",
|
||||
"depends_on": [],
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"stage_id": "17e-stage-2-async",
|
||||
"objective": "async work under event bridge",
|
||||
"state": "PENDING",
|
||||
"depends_on": [],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"stage_id": "17e-stage-3-fails",
|
||||
"objective": "deliberately failing stage",
|
||||
"state": "PENDING",
|
||||
"depends_on": [],
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"stage_id": "17e-stage-4-final",
|
||||
"objective": "final stage",
|
||||
"state": "PENDING",
|
||||
"depends_on": [],
|
||||
"priority": 3
|
||||
}
|
||||
],
|
||||
"fallbacks": [],
|
||||
"saved_at": "2026-09-15T17:50:29Z"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+798
-153
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,79 @@
|
||||
"""test_build_basis.py — T18 §18C: durable research basis held-outs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_basis as bb # noqa: E402
|
||||
|
||||
|
||||
def test_18c_depth_from_route_no_second_router():
|
||||
d = bb.research_depth("LOCAL_FIX")
|
||||
assert d["depth"] == "NONE" and "LOCAL_FIX" in d["why"]
|
||||
d2 = bb.research_depth("RESEARCHED_CHANGE")
|
||||
assert d2["depth"] == "FULL"
|
||||
# unknown route never silently gets NONE
|
||||
d3 = bb.research_depth("MYSTERY_ROUTE")
|
||||
assert d3["depth"] == "INWARD" and "unknown" in d3["why"]
|
||||
|
||||
|
||||
def test_18c_depth_deviation_requires_justification():
|
||||
d = bb.research_depth("RESEARCHED_CHANGE", justification="tiny config fix; full pipeline disproportionate")
|
||||
assert d["depth"] == "FULL" # justification annotates, does not silently downgrade
|
||||
assert "disproportionate" in d["why"]
|
||||
d2 = bb.research_depth("STANDARD_CHANGE", justification="OUTWARD: new provider integration, precedent needed")
|
||||
assert d2["depth"] == "OUTWARD" and d2["why"].startswith("OUTWARD")
|
||||
|
||||
|
||||
def test_18c_qualified_basis_round_trip(tmp_path):
|
||||
basis = bb.new_basis(question="structural model for project scaffolds",
|
||||
depth_decision=bb.research_depth("RESEARCHED_CHANGE"),
|
||||
issue_ref="research/structural-model")
|
||||
assert not bb.is_qualified(basis) # empty ≠ qualified
|
||||
basis["outward"]["prior_art_questions_answered"] = bb.OUTWARD_PRIOR_ART_QUESTIONS[:3]
|
||||
basis["outward"]["refs"] = ["https://backstage.io/docs/features/software-catalog/"]
|
||||
errs = bb.set_dispositions(basis, [
|
||||
{"item": "Backstage catalog entity model", "disposition": "ADAPT",
|
||||
"why": "domain/subsystem/component nesting fits", "ref": "backstage.io"},
|
||||
{"item": "own graph database", "disposition": "REJECT",
|
||||
"why": "existing structural_store core is adequate", "ref": "repo"},
|
||||
])
|
||||
assert errs == []
|
||||
assert bb.is_qualified(basis)
|
||||
# durable round-trip
|
||||
p = tmp_path / "basis.json"
|
||||
p.write_text(bb.to_json(basis))
|
||||
loaded = bb.from_json(p.read_text())
|
||||
assert bb.is_qualified(loaded)
|
||||
assert loaded["dispositions"][0]["disposition"] == "ADAPT"
|
||||
|
||||
|
||||
def test_18c_illegal_disposition_refused():
|
||||
basis = bb.new_basis(question="q", depth_decision=bb.research_depth("INWARD"))
|
||||
errs = bb.set_dispositions(basis, [{"item": "x", "disposition": "MAYBE"}])
|
||||
assert any("illegal disposition" in e for e in errs)
|
||||
|
||||
|
||||
def test_18c_outward_depth_requires_prior_art():
|
||||
basis = bb.new_basis(question="q", depth_decision=bb.research_depth(
|
||||
"STANDARD_CHANGE", justification="OUTWARD: precedent needed"))
|
||||
errs = bb.validate_basis(basis)
|
||||
assert any("prior-art" in e for e in errs)
|
||||
basis["outward"]["findings"] = ["Nx affected graph exists"]
|
||||
assert not any("prior-art" in e for e in bb.validate_basis(basis))
|
||||
|
||||
|
||||
def test_18c_attach_evidence_ref_and_summary():
|
||||
basis = bb.new_basis(question="q", depth_decision=bb.research_depth("INWARD"))
|
||||
ref = bb.attach_evidence_ref(basis)
|
||||
assert ref.startswith("build_basis:")
|
||||
s = bb.summary(basis)
|
||||
assert s["qualified"] is False and "depth" in s
|
||||
# structured conclusions only — no reasoning dumps in the projection
|
||||
assert "reasoning" not in s
|
||||
@@ -0,0 +1,295 @@
|
||||
"""test_contextual_construction.py — T17 §17J-§17Q: conversational
|
||||
interpretation held-outs.
|
||||
|
||||
The matrix cases are TESTS, not trigger vocabulary: every authorization
|
||||
and referent case below uses fresh paraphrases not present in the
|
||||
implementation. The intent layer resolves MEANING; Scaffolding's
|
||||
classify_mode (the ONE classifier) decides mode. §17O boundary holds
|
||||
throughout.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import construction_intent as cint # noqa: E402
|
||||
from rune import construction_mode as cm # noqa: E402
|
||||
|
||||
|
||||
def ctx(**kw) -> cint.ConversationContext:
|
||||
return cint.ConversationContext(**kw)
|
||||
|
||||
|
||||
def propose(c, summary) -> cint.Proposal:
|
||||
return c.add_proposal(summary)
|
||||
|
||||
|
||||
# ── 17Q case 1: explicit demand, new standalone program ────────────────
|
||||
def test_q1_explicit_demand_new_program():
|
||||
c = ctx()
|
||||
intent = cint.resolve_construction_intent(
|
||||
"Build me a program that tracks my climbing sessions", c)
|
||||
assert intent.action_intent == "ACTION"
|
||||
assert intent.approval_state == "USER_APPROVED"
|
||||
# Scaffolding (the ONE classifier) decides the mode
|
||||
mode = cm.classify_mode(intent.objective, project_exists=False)
|
||||
assert mode.mode == "PROJECT_BUILD" and mode.bootstrap_required
|
||||
|
||||
|
||||
# ── 17Q case 2: "how would you build X?" is discussion only ────────────
|
||||
def test_q2_how_question_never_authorizes():
|
||||
c = ctx()
|
||||
intent = cint.resolve_construction_intent(
|
||||
"How would you build a session tracker for climbing?", c)
|
||||
assert intent.action_intent == "DISCUSSION"
|
||||
assert intent.approval_state == "DISCUSSION"
|
||||
mode = cm.classify_mode(intent.objective)
|
||||
assert mode.mode != "PROJECT_BUILD" or intent.approval_state != \
|
||||
"USER_APPROVED" # no construction intent, no mutation
|
||||
|
||||
|
||||
# ── 17Q case 3: propose → later authorize (§17L lifecycle) ─────────────
|
||||
def test_q3_proposal_then_authorization():
|
||||
c = ctx()
|
||||
i1 = cint.resolve_construction_intent(
|
||||
"Do you think the wait/resume machinery could be simpler?", c)
|
||||
assert i1.approval_state == "DISCUSSION"
|
||||
# Rune proposes (recorded via the context, not mutation)
|
||||
propose(c, "Consolidate the four wait/verify paths into one bridge "
|
||||
"adapter with per-kind verdict functions")
|
||||
i2 = cint.resolve_construction_intent(
|
||||
"Alright, let's go ahead with that.", c)
|
||||
assert i2.approval_state == "USER_APPROVED"
|
||||
assert i2.resolved_referent == c.live_proposals()[0].proposal_id
|
||||
assert "Consolidate" in i2.objective
|
||||
|
||||
|
||||
# ── 17Q case 4: unseen indirect authorization of a prior proposal ──────
|
||||
def test_q4_unseen_indirect_authorization():
|
||||
c = ctx()
|
||||
propose(c, "Add a drift-aware resume gate to the programme runner")
|
||||
i = cint.resolve_construction_intent(
|
||||
"Yeah that's the one, make it happen.", c)
|
||||
assert i.approval_state == "USER_APPROVED"
|
||||
assert i.resolved_referent is not None
|
||||
|
||||
|
||||
# ── 17Q case 5: ordinal option selection among multiple proposals ──────
|
||||
def test_q5_ordinal_referent():
|
||||
c = ctx()
|
||||
p1 = propose(c, "Option A: poll-based retry sweep")
|
||||
p2 = propose(c, "Option B: event-driven resume with watchdog")
|
||||
i = cint.resolve_construction_intent(
|
||||
"Go with the second approach.", c)
|
||||
assert i.approval_state == "USER_APPROVED"
|
||||
assert i.resolved_referent == p2.proposal_id
|
||||
|
||||
|
||||
# ── 17Q case 6: ambiguous "do that" → clarification, never a guess ─────
|
||||
def test_q6_ambiguous_referent_asks():
|
||||
c = ctx()
|
||||
p1 = propose(c, "Rewrite the transport layer")
|
||||
p2 = propose(c, "Rewrite the router escalation")
|
||||
# both proposed same turn → recency NOT decisive
|
||||
i = cint.resolve_construction_intent("Do that.", c)
|
||||
assert i.ambiguity == "REFERENT"
|
||||
assert i.approval_state != "USER_APPROVED"
|
||||
assert i.probe_questions # asks, doesn't guess
|
||||
|
||||
|
||||
# ── 17Q case 7: explicit imperative "build that", resolved referent ────
|
||||
def test_q7_imperative_with_resolved_referent():
|
||||
c = ctx()
|
||||
propose(c, "Bounded CLI for the ledger")
|
||||
i = cint.resolve_construction_intent("Build that.", c)
|
||||
assert i.action_intent == "ACTION"
|
||||
assert i.approval_state == "USER_APPROVED"
|
||||
assert i.resolved_referent == c.live_proposals()[0].proposal_id
|
||||
|
||||
|
||||
# ── 17Q case 8: topic changed → stale proposal not selected ────────────
|
||||
def test_q8_stale_context_resistance():
|
||||
c = ctx()
|
||||
propose(c, "Refactor the old exporter")
|
||||
c.record_turn("user", "actually, never mind that — let's talk about "
|
||||
"the TUI fonts instead")
|
||||
c.record_turn("assistant", "sure, fonts it is")
|
||||
c.next_turn()
|
||||
i = cint.resolve_construction_intent("Build that.", c)
|
||||
# "that" now plausibly refers to the font topic, not the stale
|
||||
# proposal — the layer must NOT silently build the exporter
|
||||
assert i.approval_state != "USER_APPROVED" or \
|
||||
"exporter" not in i.objective
|
||||
|
||||
|
||||
# ── 17Q case 9: research discussion with construction language ─────────
|
||||
def test_q9_research_not_mutation():
|
||||
c = ctx()
|
||||
i = cint.resolve_construction_intent(
|
||||
"Research how other tools implement retry budgets — what would "
|
||||
"building something like that involve?", c)
|
||||
assert i.action_intent == "DISCUSSION"
|
||||
|
||||
|
||||
# ── 17Q case 10: non-construction meaning of "build" ───────────────────
|
||||
def test_q10_non_construction_build():
|
||||
c = ctx()
|
||||
i = cint.resolve_construction_intent(
|
||||
"Build a list of the pros and cons.", c)
|
||||
# imperative "build" + non-constructible object → Scaffolding's
|
||||
# verb-object check refuses construction semantics
|
||||
mode = cm.classify_mode(i.objective)
|
||||
assert mode.mode == "NOT_CONSTRUCTION"
|
||||
|
||||
|
||||
# ── 17Q cases 11-14: mode proportionality through the ONE classifier ───
|
||||
def test_q11_existing_project_change():
|
||||
mode = cm.classify_mode("fix the pagination bug in the rune TUI",
|
||||
target_project="rune", project_exists=True)
|
||||
assert mode.mode == "EXISTING_PROJECT_CHANGE"
|
||||
|
||||
|
||||
def test_q12_small_local_change():
|
||||
mode = cm.classify_mode("fix the typo in the README",
|
||||
target_project="rune", project_exists=True)
|
||||
assert mode.mode == "LOCAL_CHANGE"
|
||||
|
||||
|
||||
def test_q13_large_architectural_migration():
|
||||
mode = cm.classify_mode(
|
||||
"migrate the whole provider layer to a new adapter protocol",
|
||||
target_project="rune", project_exists=True)
|
||||
assert mode.mode == "EXISTING_PROJECT_CHANGE"
|
||||
# depth is Scaffolding's decision; the intent layer's consequence
|
||||
# signal is HIGH (checked at the intent layer level below)
|
||||
c = ctx(active_project="rune")
|
||||
i = cint.resolve_construction_intent(
|
||||
"migrate the whole provider layer to a new adapter protocol", c)
|
||||
assert i.consequence_level == "HIGH"
|
||||
|
||||
|
||||
def test_q14_new_standalone():
|
||||
mode = cm.classify_mode("a small CLI that renames photos by EXIF date",
|
||||
project_exists=False)
|
||||
assert mode.mode == "PROJECT_BUILD"
|
||||
|
||||
|
||||
# ── 17Q cases 15-16: command / natural-language / contextual
|
||||
# equivalence — all three produce the same intent semantics ──────────
|
||||
def test_q15_command_and_language_same_backend():
|
||||
# /build X (command form) and "build X" (natural language) must both
|
||||
# end at classify_mode with the same inputs
|
||||
cmd_objective = "a password generator with configurable entropy"
|
||||
nl_objective = "build me a password generator with configurable entropy"
|
||||
m_cmd = cm.classify_mode(cmd_objective, project_exists=False)
|
||||
i_nl = cint.resolve_construction_intent(nl_objective, ctx())
|
||||
m_nl = cm.classify_mode(i_nl.objective, project_exists=False)
|
||||
assert m_cmd.mode == m_nl.mode == "PROJECT_BUILD"
|
||||
assert m_cmd.bootstrap_required == m_nl.bootstrap_required
|
||||
|
||||
|
||||
def test_q16_contextual_and_explicit_equivalent_semantics():
|
||||
c = ctx(active_project="rune")
|
||||
propose(c, "Add a --dry-run flag to the deploy script")
|
||||
i_ctx = cint.resolve_construction_intent("yes, go ahead with that", c)
|
||||
i_exp = cint.resolve_construction_intent(
|
||||
"Add a --dry-run flag to the deploy script",
|
||||
ctx(active_project="rune"))
|
||||
# contextual route resolves through the referent; explicit route
|
||||
# through the imperative — SAME approval semantics
|
||||
assert i_ctx.approval_state == i_exp.approval_state == "USER_APPROVED"
|
||||
assert i_ctx.action_intent == i_exp.action_intent == "ACTION"
|
||||
|
||||
|
||||
# ── 17Q case 17-18: provider ownership + approval not bypassed ─────────
|
||||
def test_q17_provider_ownership_preserved():
|
||||
# the intent layer emits no provider field at all — provider
|
||||
# selection cannot pass through it (17O boundary, structural)
|
||||
c = ctx()
|
||||
i = cint.resolve_construction_intent("build a fixture runner", c)
|
||||
assert not hasattr(i, "provider") and "provider" not in i.to_dict()
|
||||
|
||||
|
||||
def test_q18_approval_not_bypassed_by_context():
|
||||
# a proposal is NOT authorization: approval_state stays PROPOSAL
|
||||
# until a user turn authorizes
|
||||
c = ctx()
|
||||
propose(c, "Rewrite the auth subsystem")
|
||||
i = cint.resolve_construction_intent(
|
||||
"sounds plausible, tell me more about the migration steps", c)
|
||||
assert i.approval_state != "USER_APPROVED"
|
||||
|
||||
|
||||
# ── 17Q case 19: unknown target → clarification ────────────────────────
|
||||
def test_q19_unknown_target_asks():
|
||||
c = ctx() # no active project
|
||||
i = cint.resolve_construction_intent("Implement the export feature.", c)
|
||||
assert i.ambiguity == "TARGET"
|
||||
assert i.approval_state != "USER_APPROVED"
|
||||
assert i.probe_questions
|
||||
|
||||
|
||||
# ── 17Q case 20: superseded proposal not accidentally built ────────────
|
||||
def test_q20_superseded_proposal_ignored():
|
||||
c = ctx()
|
||||
p_old = propose(c, "Use polling for CI waits")
|
||||
c.supersede_proposal(p_old.proposal_id)
|
||||
p_new = propose(c, "Use event-driven waits with watchdogs")
|
||||
i = cint.resolve_construction_intent("go ahead with that", c)
|
||||
assert i.resolved_referent == p_new.proposal_id
|
||||
|
||||
|
||||
# ── 17N: structured intent shape ────────────────────────────────────────
|
||||
def test_17n_intent_shape_complete():
|
||||
c = ctx(active_project="rune")
|
||||
i = cint.resolve_construction_intent("fix the flaky lease test", c)
|
||||
d = i.to_dict()
|
||||
for f in ("intent_id", "objective", "requested_effect",
|
||||
"resolved_target", "resolved_referent", "origin_context_id",
|
||||
"action_intent", "approval_state", "confidence", "ambiguity",
|
||||
"consequence_level", "relevant_constraints",
|
||||
"classification_evidence"):
|
||||
assert f in d, f"missing intent field {f}"
|
||||
assert d["resolved_target"] == "rune"
|
||||
|
||||
|
||||
# ── §17L: approval-state lifecycle legality ─────────────────────────────
|
||||
def test_17l_discussion_never_mutates():
|
||||
c = ctx(active_project="rune")
|
||||
for phrase in ("what do you think about the deploy script?",
|
||||
"should we improve the wait semantics?",
|
||||
"how would you build the exporter?"):
|
||||
i = cint.resolve_construction_intent(phrase, c)
|
||||
assert i.approval_state == "DISCUSSION"
|
||||
assert i.action_intent == "DISCUSSION"
|
||||
|
||||
|
||||
# ── 17R finding: polite request form is action intent ──────────────────
|
||||
def test_17r_polite_request_authorizes():
|
||||
"""'Can you fix that?' is an imperative in interrogative clothing —
|
||||
found live during the 17R self-hosting proof (the terminal-? rule
|
||||
misrouted it to DISCUSSION and the flow would have proceeded on a
|
||||
non-load-bearing intent)."""
|
||||
c = ctx(active_project="rune")
|
||||
i = cint.resolve_construction_intent(
|
||||
"The TUI's startup task loading is silently broken — can you "
|
||||
"fix that?", c)
|
||||
assert i.approval_state == "USER_APPROVED"
|
||||
assert i.action_intent == "ACTION"
|
||||
assert i.resolved_referent == "this-turn-topic"
|
||||
|
||||
|
||||
def test_17r_information_questions_stay_discussion():
|
||||
c = ctx(active_project="rune")
|
||||
for phrase in ("Should we fix the startup task loading?",
|
||||
"Could we improve the TUI?",
|
||||
"Would it be worth fixing the loader?"):
|
||||
i = cint.resolve_construction_intent(phrase, c)
|
||||
assert i.approval_state == "DISCUSSION", phrase
|
||||
assert i.action_intent == "DISCUSSION", phrase
|
||||
@@ -0,0 +1,94 @@
|
||||
"""test_contextual_generalisation.py — T18 §18N: adversarial paraphrase
|
||||
families proving the intent layer generalises semantically — not a
|
||||
memorized phrase list. Families VARY grammar/politeness/negation/
|
||||
hypotheticals/nesting; none copy implementation regexes verbatim."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import construction_intent as cint # noqa: E402
|
||||
|
||||
|
||||
def ctx(**kw):
|
||||
return cint.ConversationContext(**kw)
|
||||
|
||||
|
||||
PARAPHRASED_AUTHORIZATIONS = [
|
||||
# bare affirmation + referent, unusual punctuation, filler
|
||||
"yep — go on with that one",
|
||||
"sounds right. do it",
|
||||
"Ok sure, make that happen please",
|
||||
"I'm good with that; proceed",
|
||||
"fine, carry on with it",
|
||||
# indirect: no imperative at all
|
||||
"that works for me, let's have it",
|
||||
"yes please, that's exactly what I meant",
|
||||
]
|
||||
PARAPHRASED_QUESTIONS = [
|
||||
"would that be difficult to build?",
|
||||
"in your opinion, is a rewrite warranted here?",
|
||||
"what's the story with implementing this safely?",
|
||||
"hmm, could a migration be avoided somehow?",
|
||||
]
|
||||
PARAPHRASED_DEMANDS = [
|
||||
"go ahead and build the uploader module",
|
||||
"please implement the retry queue",
|
||||
"kindly add rate limiting to the webhook handler",
|
||||
]
|
||||
|
||||
|
||||
def test_18n_authorization_family_generalises():
|
||||
for phrase in PARAPHRASED_AUTHORIZATIONS:
|
||||
c = ctx()
|
||||
c.add_proposal("A concrete prior proposal")
|
||||
i = cint.resolve_construction_intent(phrase, c)
|
||||
assert i.approval_state == "USER_APPROVED", phrase
|
||||
assert i.resolved_referent, phrase
|
||||
|
||||
|
||||
def test_18n_question_family_never_authorizes():
|
||||
for phrase in PARAPHRASED_QUESTIONS:
|
||||
c = ctx()
|
||||
c.add_proposal("A concrete prior proposal")
|
||||
i = cint.resolve_construction_intent(phrase, c)
|
||||
assert i.approval_state == "DISCUSSION", phrase
|
||||
assert i.action_intent == "DISCUSSION", phrase
|
||||
|
||||
|
||||
def test_18n_demand_family_authorizes_without_reask():
|
||||
for phrase in PARAPHRASED_DEMANDS:
|
||||
c = ctx(active_project="rune")
|
||||
i = cint.resolve_construction_intent(phrase, c)
|
||||
assert i.approval_state == "USER_APPROVED", phrase
|
||||
assert i.action_intent == "ACTION", phrase
|
||||
|
||||
|
||||
def test_18n_negation_blocks_authorization():
|
||||
c = ctx()
|
||||
c.add_proposal("The proposal on the table")
|
||||
i = cint.resolve_construction_intent(
|
||||
"don't build that yet — I want to think", c)
|
||||
assert i.approval_state != "USER_APPROVED"
|
||||
|
||||
|
||||
def test_18n_nested_relative_clauses():
|
||||
c = ctx()
|
||||
p = c.add_proposal("The exporter that streams CSV output")
|
||||
# relative clause inside the authorization — referent via content
|
||||
i = cint.resolve_construction_intent(
|
||||
"yeah, the one that you suggested for streaming, do that", c)
|
||||
assert i.approval_state == "USER_APPROVED"
|
||||
assert i.resolved_referent == p.proposal_id
|
||||
|
||||
|
||||
def test_18n_multiple_proposals_terse_selection():
|
||||
c = ctx()
|
||||
a = c.add_proposal("First: in-memory cache layer")
|
||||
b = c.add_proposal("Second: persistent disk cache")
|
||||
i = cint.resolve_construction_intent("the second one, ship it", c)
|
||||
assert i.resolved_referent == b.proposal_id
|
||||
@@ -0,0 +1,103 @@
|
||||
"""test_contextual_research_plan.py — T18 §18D CORRECTION: the research
|
||||
plan itself is contextually generated. No permanent topic list exists.
|
||||
|
||||
Held-outs prove UNRELATED requests generate MATERIALLY DIFFERENT plans
|
||||
(disjoint topics unless they share genuine unknowns), and that the
|
||||
structural-model topics from 18D do NOT leak into unrelated research.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_basis as bb # noqa: E402
|
||||
|
||||
|
||||
def test_18d_unrelated_intents_generate_different_plans():
|
||||
"""A structural-model request and an unrelated request must produce
|
||||
materially different plans — the 18D architecture-graph topics must
|
||||
NOT appear in unrelated research."""
|
||||
structural = bb.generate_research_plan(
|
||||
intent="design a persistent structural model for project scaffolds",
|
||||
target="scaffolding",
|
||||
unknowns=["what relationship vocabulary do existing systems use "
|
||||
"(Backstage/Kythe)?",
|
||||
"what graph interchange format is worth emitting?"],
|
||||
decisions=["which store core carries the model?"],
|
||||
depth=bb.DEPTH_FULL)
|
||||
unrelated = bb.generate_research_plan(
|
||||
intent="fix the flaky lease timeout test",
|
||||
target="wr-bridge",
|
||||
unknowns=["why does the lease watchdog fire before the ack window "
|
||||
"closes on the runner?"],
|
||||
decisions=[],
|
||||
risks=["fix may mask a real timeout race"],
|
||||
depth=bb.DEPTH_INWARD)
|
||||
structural_topics = " ".join(t["topic"] for t in structural["topics"])
|
||||
unrelated_topics = " ".join(t["topic"] for t in unrelated["topics"])
|
||||
# the structural plan is about vocabulary/formats; the test-fix plan
|
||||
# is about a watchdog race — zero topical overlap
|
||||
assert "vocabulary" in structural_topics
|
||||
assert "vocabulary" not in unrelated_topics
|
||||
assert "watchdog" in unrelated_topics
|
||||
assert "watchdog" not in structural_topics
|
||||
# and neither plan carries a canned category list
|
||||
for plan in (structural, unrelated):
|
||||
canned = [t for t in plan["topics"]
|
||||
if t["kind"] == "prior-art" and "THIS intent" not in
|
||||
t["topic"]]
|
||||
# prior-art topic only appears when the unknowns reference
|
||||
# existing practice — and even then it names THIS intent
|
||||
assert all("THIS intent" in t["topic"]
|
||||
for t in plan["topics"] if t["kind"] == "prior-art")
|
||||
|
||||
|
||||
def test_18d_no_unknowns_means_no_research_machinery():
|
||||
"""A tiny obvious change with no material unknowns gets depth NONE
|
||||
and an EMPTY topic list — no mechanical research ritual."""
|
||||
plan = bb.generate_research_plan(intent="fix a typo in the README")
|
||||
assert plan["depth"]["depth"] == bb.DEPTH_NONE
|
||||
assert plan["topics"] == []
|
||||
assert plan["depth"]["why"] # the NONE decision still records WHY
|
||||
|
||||
|
||||
def test_18d_depth_heuristics_record_why():
|
||||
plan = bb.generate_research_plan(
|
||||
intent="migrate the auth schema",
|
||||
unknowns=["which user tables hold credential hashes?"],
|
||||
risks=["breaking change to login flow"])
|
||||
assert plan["depth"]["depth"] == bb.DEPTH_FULL
|
||||
assert "consequence" in plan["depth"]["why"]
|
||||
# caller-supplied depth is recorded with accountability
|
||||
plan2 = bb.generate_research_plan(intent="x", unknowns=["big unknown"],
|
||||
depth=bb.DEPTH_INWARD)
|
||||
assert plan2["depth"]["why"].startswith("depth supplied")
|
||||
|
||||
|
||||
def test_18d_prior_art_topic_only_when_contextually_relevant():
|
||||
"""Rule E's precedent question attaches when unknowns reference
|
||||
existing practice — and is phrased for THIS intent, not a category."""
|
||||
plan = bb.generate_research_plan(
|
||||
intent="add PDF export to the report generator",
|
||||
unknowns=["is there an existing Python PDF library that meets "
|
||||
"the fidelity needs?"])
|
||||
pa = [t for t in plan["topics"] if t["kind"] == "prior-art"]
|
||||
assert len(pa) == 1
|
||||
assert "THIS intent" in pa[0]["topic"]
|
||||
# but a purely-internal unknown never summons prior-art machinery
|
||||
plan2 = bb.generate_research_plan(
|
||||
intent="add PDF export to the report generator",
|
||||
unknowns=["which code path currently renders report tables?"])
|
||||
assert not [t for t in plan2["topics"] if t["kind"] == "prior-art"]
|
||||
|
||||
|
||||
def test_18d_every_topic_carries_why():
|
||||
plan = bb.generate_research_plan(
|
||||
intent="i", unknowns=["u1", "u2"], decisions=["d1"], risks=["r1"])
|
||||
assert len(plan["topics"]) == 4
|
||||
for t in plan["topics"]:
|
||||
assert t["why"], t
|
||||
@@ -0,0 +1,66 @@
|
||||
"""test_graph_projection.py — T18 §18Q/§18P: read-only graph projection
|
||||
+ the self-scaffolded upgrade proof record."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import graph_projection as gp # noqa: E402
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
|
||||
SELF = REPO / "store" / "scaffold"
|
||||
|
||||
|
||||
def _self_model():
|
||||
return sm.StructuralModel(SELF, "scaffolding")
|
||||
|
||||
|
||||
def test_18q_projection_is_versioned_and_complete():
|
||||
m = _self_model()
|
||||
j = gp.project_json(m)
|
||||
assert j["schema_version"] == gp.PROJECTION_SCHEMA_VERSION == 1
|
||||
assert j["node_count"] >= 25 and j["edge_count"] >= 30
|
||||
# no internal storage layout leaked: only curated ext keys
|
||||
for n in j["nodes"]:
|
||||
assert set(n.keys()) <= {"id", "kind", "maturity", "name", "ext"}
|
||||
assert set((n.get("ext") or {}).keys()) <= {"source",
|
||||
"bounded_adapter"}
|
||||
|
||||
|
||||
def test_18q_dot_and_mermaid_render_parse():
|
||||
m = _self_model()
|
||||
dot = gp.project_dot(m)
|
||||
assert dot.startswith("digraph") and dot.count("->") >= 30
|
||||
mm = gp.project_mermaid(m)
|
||||
assert mm.startswith("graph LR") and mm.count("-->") >= 30
|
||||
# DOT/Mermaid ids are safe (quoted / mangled) even with ':' in ids
|
||||
assert '"component:scaffold"' in dot # quoted DOT ids
|
||||
assert " component:scaffold " not in mm # mangled in mermaid
|
||||
|
||||
|
||||
def test_18q_projection_is_read_only():
|
||||
m = _self_model()
|
||||
before = (SELF / "structural.json").read_text()
|
||||
gp.project_json(m); gp.project_dot(m); gp.project_mermaid(m)
|
||||
assert (SELF / "structural.json").read_text() == before
|
||||
|
||||
|
||||
def test_18p_upgrade_proof_artifacts_exist():
|
||||
rec = REPO / "store/scaffold/programmes" # programme records live
|
||||
contract_docs = (SELF / "current.json").read_text()
|
||||
assert "18q-graph-projection" in contract_docs
|
||||
assert "closed" in contract_docs
|
||||
wsr = REPO / "store/evidence/t18/18P_workspace_record.json"
|
||||
import json
|
||||
r = json.loads(wsr.read_text())
|
||||
assert r["stages_completed"][:4] == ["base_verified", "worktree_created",
|
||||
"construction", "verified"]
|
||||
assert r["construction_base"] # started from a verified base SHA
|
||||
# the projection output exists and matches the live model
|
||||
j = json.loads((REPO / "store/evidence/t18/projection/"
|
||||
"structural_graph.json").read_text())
|
||||
assert any(n["id"] == "component:graph_projection" for n in j["nodes"])
|
||||
@@ -105,6 +105,7 @@ def test_15m_every_stop_carries_an_honest_verdict():
|
||||
assert v["verdict"] in (
|
||||
"PROGRAMME_COMPLETE", "PROGRAMME_OPEN", "PROGRAMME_OPEN_WAITING",
|
||||
"ALL_REMAINING_BLOCKED", "APPROVAL_REQUIRED",
|
||||
"PROGRAMME_COMPLETE_WITH_FAILURES", # T17 17E: honest all-terminal
|
||||
"UNKNOWN_INTERRUPTION"), v
|
||||
if v["verdict"] == "UNKNOWN_INTERRUPTION":
|
||||
assert combo_state == FAILED, \
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""test_programme_wiring.py — T17 §17D/§17E: canonical programme
|
||||
continuation wiring held-outs.
|
||||
|
||||
The 15M canary proved the RULE; these prove the WIRING: stages bound to
|
||||
real Scaffolding contracts, driven through the Construction Interface
|
||||
lifecycle, with restart/duplicate/idempotency/adversarial cases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import programme_continuation as pc # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def iface(tmp_path):
|
||||
return ci.ConstructionInterface(
|
||||
builder="t17-programme-test",
|
||||
project_store=str(tmp_path / "scaffold"),
|
||||
project_id="prog-test")
|
||||
|
||||
|
||||
def _make_contract(iface, contract_id, need="stage need"):
|
||||
doc = iface.scaffold.load()
|
||||
iface.scaffold.new_contract(doc, contract_id=contract_id, need=need,
|
||||
origin="programme", affected_entities=[])
|
||||
iface.scaffold.save(doc)
|
||||
|
||||
|
||||
def _prog(prog_id, stages):
|
||||
p = pc.Programme(programme_id=prog_id, objective="test programme")
|
||||
for sid, work in stages:
|
||||
st = pc.ProgrammeStage(stage_id=sid, objective=sid,
|
||||
priority=len(p.stages))
|
||||
if work is not None:
|
||||
st.result["work"] = work
|
||||
p.stages.append(st)
|
||||
return p
|
||||
|
||||
|
||||
def _phase(iface, contract_id):
|
||||
doc = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc, contract_id)
|
||||
return (c or {}).get("phase", "")
|
||||
|
||||
|
||||
def test_17e_all_four_stages_close_with_evidence(iface, tmp_path):
|
||||
stages = [("st-1", None),
|
||||
("st-2", {"ok": True, "evidence": "did the thing"}),
|
||||
("st-3", {"ok": True, "evidence": "more work"})]
|
||||
prog = _prog("p-four", stages)
|
||||
for sid, _ in stages:
|
||||
_make_contract(iface, sid)
|
||||
log = pc.run_construction_programme(prog, iface,
|
||||
scaffold_store=tmp_path / "scaffold")
|
||||
assert prog.continuation_canary()["verdict"] == "PROGRAMME_COMPLETE"
|
||||
for sid, _ in stages:
|
||||
assert _phase(iface, sid) == "closed"
|
||||
doc = iface.scaffold.load()
|
||||
c = next(e for e in doc["entities"] if e["id"] == "contract:st-2")
|
||||
assert any("did the thing" in (cp.get("note", "") + cp.get("evidence", ""))
|
||||
for cp in c.get("checkpoints", []))
|
||||
|
||||
|
||||
def test_17e_failure_does_not_stop_programme(iface, tmp_path):
|
||||
stages = [("f-1", {"ok": False, "evidence": "verification failed"}),
|
||||
("f-2", {"ok": True, "evidence": "independent work ok"})]
|
||||
prog = _prog("p-fail", stages)
|
||||
for sid, _ in stages:
|
||||
_make_contract(iface, sid)
|
||||
pc.run_construction_programme(prog, iface,
|
||||
scaffold_store=tmp_path / "scaffold")
|
||||
verdict = prog.continuation_canary()["verdict"]
|
||||
assert verdict == "PROGRAMME_COMPLETE_WITH_FAILURES"
|
||||
assert _phase(iface, "f-1") == "failed" # truthful failure, not hung
|
||||
assert _phase(iface, "f-2") == "closed"
|
||||
|
||||
|
||||
def test_17e_restart_resumes_from_durable_record(iface, tmp_path):
|
||||
stages = [("r-1", {"ok": True, "evidence": "done"}), ("r-2", None)]
|
||||
prog = _prog("p-restart", stages)
|
||||
for sid, _ in stages:
|
||||
_make_contract(iface, sid)
|
||||
base = tmp_path / "scaffold" / "programmes"
|
||||
# stage 1 genuinely completed before the "restart": close its contract
|
||||
iface.transition("r-1", "approved", evidence_ref="pre")
|
||||
iface.transition("r-1", "building", evidence_ref="pre")
|
||||
iface.transition("r-1", "verifying", evidence_ref="pre")
|
||||
iface.close("r-1", observed_evidence=[{"source": "pre", "detail": "x"}],
|
||||
scope_files=[])
|
||||
prog.stages[0].state = pc.COMPLETE
|
||||
prog.save(base)
|
||||
# "restart": reload from the durable record only
|
||||
prog2 = pc.Programme.load(base, "p-restart")
|
||||
assert prog2.stages[0].state == pc.COMPLETE
|
||||
assert prog2.stages[1].state == pc.PENDING
|
||||
pc.run_construction_programme(prog2, iface,
|
||||
scaffold_store=tmp_path / "scaffold")
|
||||
assert _phase(iface, "r-1") == "closed"
|
||||
assert _phase(iface, "r-2") == "closed"
|
||||
|
||||
|
||||
def test_17e_duplicate_continuation_signal_idempotent(iface, tmp_path):
|
||||
_make_contract(iface, "d-1")
|
||||
prog = _prog("p-dup", [("d-1", {"ok": True, "evidence": "once"})])
|
||||
base = tmp_path / "scaffold"
|
||||
log1 = pc.run_construction_programme(prog, iface, scaffold_store=base)
|
||||
# re-run the whole programme against the SAME contracts
|
||||
prog2 = pc.Programme.load(base / "programmes", "p-dup")
|
||||
log2 = pc.run_construction_programme(prog2, iface, scaffold_store=base)
|
||||
assert _phase(iface, "d-1") == "closed" # not re-opened, not corrupted
|
||||
assert any(e["canary"]["verdict"] == "PROGRAMME_COMPLETE"
|
||||
for e in log2 if "canary" in e)
|
||||
|
||||
|
||||
def test_17e_already_completed_stage_skipped(iface, tmp_path):
|
||||
_make_contract(iface, "s-1")
|
||||
iface.transition("s-1", "approved", evidence_ref="pre")
|
||||
iface.transition("s-1", "building", evidence_ref="pre")
|
||||
iface.transition("s-1", "verifying", evidence_ref="pre")
|
||||
iface.close("s-1", observed_evidence=[{"source": "pre", "detail": "x"}],
|
||||
scope_files=[])
|
||||
prog = _prog("p-skip", [("s-1", {"ok": True, "evidence": "rework?"})])
|
||||
log = pc.run_construction_programme(prog, iface,
|
||||
scaffold_store=tmp_path / "scaffold")
|
||||
# already-closed → COMPLETE without re-running work
|
||||
assert prog.continuation_canary()["verdict"] == "PROGRAMME_COMPLETE"
|
||||
assert not any("rework" in str(e) for e in log)
|
||||
|
||||
|
||||
def test_17e_work_callable_receives_interface_and_stage(iface, tmp_path):
|
||||
seen = {}
|
||||
|
||||
def work(i, stage):
|
||||
seen["iface"] = type(i).__name__
|
||||
seen["stage"] = stage.stage_id
|
||||
return {"ok": True, "evidence": "callable ran"}
|
||||
|
||||
_make_contract(iface, "w-1")
|
||||
prog = _prog("p-call", [("w-1", work)])
|
||||
pc.run_construction_programme(prog, iface,
|
||||
scaffold_store=tmp_path / "scaffold")
|
||||
assert seen == {"iface": "ConstructionInterface", "stage": "w-1"}
|
||||
assert _phase(iface, "w-1") == "closed"
|
||||
|
||||
|
||||
def test_17e_missing_contract_fails_honestly(iface, tmp_path):
|
||||
prog = _prog("p-missing", [("ghost-1", {"ok": True, "evidence": "x"})])
|
||||
log = pc.run_construction_programme(prog, iface,
|
||||
scaffold_store=tmp_path / "scaffold")
|
||||
assert prog.stages[0].state == pc.FAILED
|
||||
assert "no contract" in (prog.stages[0].result.get("error") or "")
|
||||
@@ -0,0 +1,75 @@
|
||||
"""test_self_scaffold.py — T18 §18O: Scaffolding models ITSELF in ONE
|
||||
canonical structural Scaffold; consistency is enforced by tests, not
|
||||
hoped for."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
from rune import structural_migration as mig # noqa: E402
|
||||
|
||||
STORE = REPO / "store" / "scaffold"
|
||||
|
||||
|
||||
def _model():
|
||||
return sm.StructuralModel(STORE, "scaffolding")
|
||||
|
||||
|
||||
def test_18o_self_scaffold_exists_and_is_versioned():
|
||||
m = _model()
|
||||
assert m.get("project:scaffolding") is not None
|
||||
assert m.doc.get("schema_version") == sm.SCHEMA_VERSION
|
||||
|
||||
|
||||
def test_18o_all_modeled_sources_exist_in_reality():
|
||||
"""Every component with a source path must point at a REAL file —
|
||||
the self-scaffold cannot drift into fiction (18H principle)."""
|
||||
m = _model()
|
||||
checked = 0
|
||||
for e in m.doc["entities"]:
|
||||
src = (e.get("ext") or {}).get("source") or \
|
||||
(e.get("identities") or {}).get("source")
|
||||
if src:
|
||||
assert (REPO / src).is_file(), f"{e['id']} → missing {src}"
|
||||
checked += 1
|
||||
assert checked >= 15 # the model covers the real surface
|
||||
|
||||
|
||||
def test_18o_no_duplicate_structural_truth_with_spine():
|
||||
"""Spine owns Rune's self-model; the project scaffold models the
|
||||
SCAFFOLDING project. Same generic core, separate stores — the
|
||||
relationship is defined, not duplicated (rule K)."""
|
||||
import json
|
||||
spine_doc = json.loads((REPO / "store/spine/current.json").read_text())
|
||||
project = _model()
|
||||
# both use kind:id entities (shared discipline) but the project model
|
||||
# carries domains/subsystems Spine does not duplicate as Rune modules
|
||||
spine_ids = {e["id"] for e in spine_doc["entities"]}
|
||||
for e in project.doc["entities"]:
|
||||
if e["kind"] in ("domain", "subsystem"):
|
||||
assert e["id"] not in spine_ids # no cross-store duplication
|
||||
|
||||
|
||||
def test_18o_legacy_adapter_is_marked_not_hidden():
|
||||
m = _model()
|
||||
adapter = m.get("component:build_mode_adapter")
|
||||
assert adapter is not None
|
||||
assert adapter.get("ext", {}).get("bounded_adapter") is True
|
||||
sup = m.supersession("component:build_mode_adapter")
|
||||
assert "subsystem:kernel" in sup["legacy_of"]
|
||||
|
||||
|
||||
def test_18o_schema_version_known_to_consent_machinery():
|
||||
m = _model()
|
||||
# a hypothetical uplift to v2 would classify by its changes
|
||||
assert mig.classify_migration(
|
||||
from_v=m.doc["schema_version"], to_v=2,
|
||||
changes=["add optional kind 'capability'"]) == "MINOR"
|
||||
assert mig.classify_migration(
|
||||
from_v=m.doc["schema_version"], to_v=2,
|
||||
changes=["rename belongs_to to part_of"]) == "MATERIAL"
|
||||
@@ -0,0 +1,138 @@
|
||||
"""test_structural_model.py — T18 §18E/§18F: structural Scaffold
|
||||
held-outs. Modeled on Scaffolding ITSELF (the 18O self-scaffold shape)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def model(tmp_path):
|
||||
m = sm.StructuralModel(tmp_path / "store", "scaffolding")
|
||||
# shape: project → domain → subsystem → component (Scaffolding itself)
|
||||
m.add("domain", "construction", parent="project:scaffolding")
|
||||
m.add("subsystem", "kernel", parent="domain:construction")
|
||||
m.add("component", "scaffold", parent="subsystem:kernel")
|
||||
m.add("component", "construction_interface", parent="subsystem:kernel")
|
||||
m.add("component", "construction_mode", parent="subsystem:kernel")
|
||||
m.add("component", "programme_continuation", parent="subsystem:kernel")
|
||||
m.add("component", "event_wait", parent="subsystem:kernel")
|
||||
m.add("interface", "construction-api", parent="subsystem:kernel")
|
||||
m.add("state_store", "scaffold-store", parent="project:scaffolding")
|
||||
m.add("test_suite", "kernel-tests", parent="project:scaffolding")
|
||||
m.add("deployment_surface", "rune-code-current",
|
||||
parent="project:scaffolding")
|
||||
m.add("environment", "production", parent="project:scaffolding")
|
||||
# relationships (Backstage + Kythe vocabulary)
|
||||
m.relate("component:construction_interface", "provides",
|
||||
"interface:construction-api")
|
||||
m.relate("component:scaffold", "consumes", "interface:construction-api")
|
||||
m.relate("component:programme_continuation", "consumes",
|
||||
"interface:construction-api")
|
||||
m.relate("component:scaffold", "depends_on",
|
||||
"component:construction_mode")
|
||||
m.relate("component:scaffold", "owns", "state_store:scaffold-store")
|
||||
m.relate("component:construction_interface", "reads",
|
||||
"state_store:scaffold-store")
|
||||
m.relate("component:construction_interface", "writes",
|
||||
"state_store:scaffold-store")
|
||||
m.relate("component:event_wait", "calls", "component:event_wait") if False else None
|
||||
m.relate("component:scaffold", "verified_by", "test_suite:kernel-tests")
|
||||
m.relate("component:programme_continuation", "verified_by",
|
||||
"test_suite:kernel-tests")
|
||||
m.relate("deployment_surface:rune-code-current", "connects_to",
|
||||
"environment:production")
|
||||
m.relate("component:scaffold", "deployed_by",
|
||||
"deployment_surface:rune-code-current")
|
||||
m.save()
|
||||
return m
|
||||
|
||||
|
||||
def test_18e_stable_deterministic_ids(model):
|
||||
assert model.get("component:scaffold") is not None
|
||||
# same name → same id, no duplicates (Backstage entity-ref discipline)
|
||||
model.add("component", "scaffold", evidence_ref="re-add")
|
||||
ids = [e["id"] for e in model.doc["entities"]
|
||||
if e["id"] == "component:scaffold"]
|
||||
assert len(ids) == 1
|
||||
|
||||
|
||||
def test_18e_every_edge_carries_authority_status_evidence(model):
|
||||
for r in model.doc["relationships"]:
|
||||
assert r.get("evidence_refs"), r
|
||||
assert r.get("authority") in ("model-author", "importer", "user"), r
|
||||
assert "status" in r, r
|
||||
|
||||
|
||||
def test_18e_illegal_kind_and_rel_refused(model):
|
||||
with pytest.raises(ValueError):
|
||||
model.add("vibe", "x")
|
||||
with pytest.raises(ValueError):
|
||||
model.relate("component:scaffold", "vibes_with", "domain:construction")
|
||||
|
||||
|
||||
def test_18f_queries(model):
|
||||
assert model.children("domain:construction") == ["subsystem:kernel"]
|
||||
beneath = model.beneath("domain:construction")
|
||||
assert "component:scaffold" in beneath and "component:event_wait" in beneath
|
||||
assert model.owning_domain("component:scaffold") == "domain:construction"
|
||||
assert model.dependencies("component:scaffold") == [
|
||||
"component:construction_mode"]
|
||||
assert set(model.consumers("interface:construction-api")) == {
|
||||
"component:scaffold", "component:programme_continuation"}
|
||||
assert model.state_owned("component:scaffold") == [
|
||||
"state_store:scaffold-store"]
|
||||
assert model.canonical_state_owner("component:scaffold") == \
|
||||
"state_store:scaffold-store"
|
||||
assert model.canonical_state_owner("state_store:scaffold-store") == \
|
||||
"state_store:scaffold-store"
|
||||
sup = model.supersession("component:scaffold")
|
||||
assert sup == {"supersedes": [], "superseded_by": [], "legacy_of": []}
|
||||
|
||||
|
||||
def test_18f_impact_closure_shape(model):
|
||||
c = model.impact_closure("component:scaffold")
|
||||
assert c["target"] == "component:scaffold"
|
||||
assert c["owning_domain"] == "domain:construction"
|
||||
assert "component:scaffold" in c["mutation_scope"]
|
||||
assert "component:construction_mode" in c["observation_scope"][
|
||||
"direct_dependencies"]
|
||||
assert "interface:construction-api" in c["observation_scope"][
|
||||
"interfaces_consumed"]
|
||||
# sibling consumers of a shared interface are NOT in a component's
|
||||
# closure (changing scaffold doesn't change the interface) — the
|
||||
# ripple happens at the INTERFACE level (transitive test below)
|
||||
assert "component:programme_continuation" not in c["mutation_scope"]
|
||||
assert "test_suite:kernel-tests" in c["verification_scope"]
|
||||
assert "deployment_surface:rune-code-current" in c["deployment_scope"]
|
||||
assert c["invariants"] and isinstance(c["uncertainty"], list)
|
||||
# dynamic-consumer uncertainty is EXPLICIT, never silent (rule 18F)
|
||||
assert any(u["kind"] == "dynamic-consumer" for u in c["uncertainty"])
|
||||
|
||||
|
||||
def test_18f_impact_closure_transitive_and_bounded(model):
|
||||
# chain: pc consumes iface; scaffold consumes iface → both in scope
|
||||
c = model.impact_closure("interface:construction-api")
|
||||
assert "component:scaffold" in c["mutation_scope"]
|
||||
assert "component:programme_continuation" in c["mutation_scope"]
|
||||
assert c["closure_depth"] <= 8
|
||||
assert model.impact_closure("component:ghost").get("error")
|
||||
|
||||
|
||||
def test_18e_persistence_round_trip(tmp_path):
|
||||
m = sm.StructuralModel(tmp_path / "store", "scaffolding")
|
||||
m.add("domain", "d1", parent="project:scaffolding")
|
||||
m.save()
|
||||
m2 = sm.StructuralModel(tmp_path / "store", "scaffolding")
|
||||
assert m2.get("domain:d1") is not None
|
||||
assert m2.get("project:scaffolding") is not None
|
||||
# schema version stamped
|
||||
assert m2.doc.get("schema_version") == sm.SCHEMA_VERSION
|
||||
@@ -0,0 +1,78 @@
|
||||
"""test_task_state_consolidation.py — T17 §17F: one task-state authority.
|
||||
|
||||
TaskStateRepository (durable events + hot checkpoint) is THE authority.
|
||||
operator.json's task list is a legacy in-process cache. Held-outs prove
|
||||
the counters/listings derive from the authority and no state is lost.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune.orchestration.conversation_state import ( # noqa: E402
|
||||
ConversationStateRepository, TaskStateRepository)
|
||||
from rune.orchestration.event_store import DurableEventStore # noqa: E402
|
||||
from rune.orchestration.records import TaskStateRecord # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def repos(tmp_path):
|
||||
events = DurableEventStore(str(tmp_path / "events.jsonl"))
|
||||
from rune.orchestration.atomic import AtomicStore
|
||||
hot = AtomicStore(tmp_path / "hot")
|
||||
tasks = TaskStateRepository(events, hot)
|
||||
convo = ConversationStateRepository(events, hot, tasks)
|
||||
return tasks, convo
|
||||
|
||||
|
||||
def test_17f_list_all_returns_every_record(repos):
|
||||
tasks, _ = repos
|
||||
r1 = tasks.transition(r"t-1", "created") if False else None
|
||||
from rune.orchestration.records import TaskStateRecord as R
|
||||
tasks.import_record(R(task_id="t-a", title="a", status="created",
|
||||
channel_id="ch1"))
|
||||
tasks.import_record(R(task_id="t-b", title="b", status="completed",
|
||||
channel_id="ch1"))
|
||||
all_recs = tasks.list_all()
|
||||
assert {r.task_id for r in all_recs} >= {"t-a", "t-b"}
|
||||
|
||||
|
||||
def test_17f_operator_counters_derive_from_authority():
|
||||
from rune.rune_operator import RuneOperator
|
||||
import tempfile, os
|
||||
tmp = tempfile.mkdtemp()
|
||||
op = RuneOperator.__new__(RuneOperator) # bypass __init__ wiring
|
||||
# minimal wiring: real repo + empty in-memory set
|
||||
from rune.orchestration.event_store import DurableEventStore
|
||||
from rune.orchestration.atomic import AtomicStore
|
||||
from pathlib import Path as P
|
||||
root = P(tmp)
|
||||
op._task_state = TaskStateRepository(
|
||||
DurableEventStore(str(root / "e.jsonl")), AtomicStore(root / "hot"))
|
||||
from rune.rune_operator import RuneState
|
||||
op.state = RuneState()
|
||||
# authority has 2 created + 1 running + 1 completed; cache disagrees
|
||||
for tid, st in (("a", "created"), ("b", "created"), ("c", "running"),
|
||||
("d", "completed")):
|
||||
op._task_state.import_record(TaskStateRecord(
|
||||
task_id=tid, title=tid, status=st, channel_id="ch"))
|
||||
op.state.tasks = [] # cache empty — counters must still be right
|
||||
counters = op._task_counters()
|
||||
assert counters == {"tasks_pending": 2, "tasks_running": 1,
|
||||
"tasks_completed": 1}
|
||||
|
||||
|
||||
def test_17f_no_state_loss_after_send_task_shape():
|
||||
"""The repo records carry the identity fields the legacy cache held:
|
||||
task_id, status, assigned_model, timestamps. (Field-mapping check —
|
||||
route lives in route.jsonl by design, NOT copied: route_ref only.)"""
|
||||
r = TaskStateRecord(task_id="t-x", title="do thing", status="running",
|
||||
channel_id="ch", assigned_model="m")
|
||||
assert r.task_id and r.status and r.assigned_model is not None
|
||||
assert hasattr(r, "route_ref") # pointer, not copy
|
||||
@@ -0,0 +1,96 @@
|
||||
"""test_v1_adversarial.py — T18 §18R: adversarial/failure matrix.
|
||||
New cases not covered by earlier suites; the rest map to existing
|
||||
evidence (see store/evidence/t18/18R_matrix.md)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import drift_resume as dr # noqa: E402
|
||||
from rune import forge # noqa: E402
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
from rune import structural_migration as mig # noqa: E402
|
||||
|
||||
|
||||
# ── secrets: redact + continue (rule O — NEVER nag/block) ────────────────
|
||||
def test_18r_secret_value_redacted_in_store(tmp_path):
|
||||
from rune import structural_store as ss
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="deployment_surface:prod", kind="deployment_surface",
|
||||
maturity="DECLARED", evidence_refs=["t"])
|
||||
e = ss.entity(doc, "deployment_surface:prod")
|
||||
e["identities"]["requires"] = "GITEA_TOKEN from environment"
|
||||
e["ext"] = {"gitea_token": "super-secret-value-123",
|
||||
"note": "reference recorded, value must not persist"}
|
||||
path = tmp_path / "structural.json"
|
||||
ss.save(doc, path)
|
||||
raw = path.read_text()
|
||||
assert "super-secret-value-123" not in raw # value never persists
|
||||
assert "<redacted>" in raw
|
||||
assert "GITEA_TOKEN from environment" in raw # reference survives
|
||||
|
||||
|
||||
def test_18r_secret_in_env_does_not_block_construction():
|
||||
"""Rule O: a secret present in the environment/config is NORMAL —
|
||||
construction continues; only an unusable credential blocks."""
|
||||
import os
|
||||
# the provider boundary raises ONLY on MISSING context, never on
|
||||
# the presence of a secret
|
||||
from rune.provider_context import require_model
|
||||
os.environ["MIMO_MODEL"] = "sk-secret-ish-provider-key"
|
||||
try:
|
||||
assert require_model() == "sk-secret-ish-provider-key"
|
||||
finally:
|
||||
os.environ.pop("MIMO_MODEL", None)
|
||||
|
||||
|
||||
# ── stale scaffold + reality wins ────────────────────────────────────────
|
||||
def test_18r_stale_scaffold_reconciles_via_drift(tmp_path):
|
||||
m = sm.StructuralModel(tmp_path / "store", "demo")
|
||||
m.add("component", "ghost", parent="project:demo")
|
||||
e = m.get("component:ghost")
|
||||
e["ext"] = {"source": "src/ghost.py"}
|
||||
m.save()
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "moved.py").write_text("x") # renamed, not gone
|
||||
drifts = dr.detect_structural_drift(m, tmp_path)
|
||||
assert drifts and drifts[0]["class"] == "MATERIAL"
|
||||
assert "reconciliation" in drifts[0]
|
||||
|
||||
|
||||
def test_18r_user_rejects_material_upgrade():
|
||||
with pytest.raises(mig.ConsentRequired):
|
||||
mig.apply_migration(model_store=None, from_v=1, to_v=2,
|
||||
changes=["rename belongs_to"], user_approved=False)
|
||||
|
||||
|
||||
# ── dirty worktree / forge unavailable ───────────────────────────────────
|
||||
def test_18r_dirty_worktree_is_visible_not_fatal():
|
||||
proc = subprocess.run(["git", "status", "--porcelain"],
|
||||
cwd=REPO, capture_output=True, text=True)
|
||||
# reality check: the harness tree may be dirty — construction must
|
||||
# detect and record it, not silently assume clean (18R case)
|
||||
dirty = bool(proc.stdout.strip())
|
||||
assert isinstance(dirty, bool) # the CHECK works; policy is per-contract
|
||||
|
||||
|
||||
def test_18r_forge_unavailable_raises_explicitly():
|
||||
f = forge.GiteaForge(server="https://invalid.invalid")
|
||||
with pytest.raises(forge.ForgeUnavailable):
|
||||
f.ci_runs_for_sha("conductor/rune", "0" * 40)
|
||||
|
||||
|
||||
def test_18r_dynamic_consumers_stay_uncertain(tmp_path):
|
||||
m = sm.StructuralModel(tmp_path / "store", "demo")
|
||||
m.add("component", "c", parent="project:demo")
|
||||
c = m.impact_closure("component:c")
|
||||
kinds = {u["kind"] for u in c["uncertainty"]}
|
||||
assert "dynamic-consumer" in kinds # never silent about blind spots
|
||||
@@ -0,0 +1,131 @@
|
||||
"""test_v1_foundations.py — T18 §18G/§18H/§18I held-outs: impact
|
||||
preview, structural drift, schema versioning + consent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_basis as bb # noqa: E402
|
||||
from rune import drift_resume as dr # noqa: E402
|
||||
from rune import impact_preview as ip # noqa: E402
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def model(tmp_path):
|
||||
m = sm.StructuralModel(tmp_path / "store", "demo")
|
||||
m.add("domain", "d", parent="project:demo")
|
||||
m.add("component", "widget", parent="domain:d",
|
||||
ext={}, evidence_ref="e1")
|
||||
e = m.get("component:widget")
|
||||
e["identities"]["source"] = "src/widget.py"
|
||||
m.relate("component:widget", "verified_by", "test_suite:t")
|
||||
m.add("test_suite", "t", parent="project:demo")
|
||||
m.relate("component:widget", "verified_by", "test_suite:t")
|
||||
m.relate("component:widget", "deployed_by", "deployment_surface:live")
|
||||
m.add("deployment_surface", "live", parent="project:demo")
|
||||
m.save()
|
||||
return m
|
||||
|
||||
|
||||
# ── 18G: visible impact preview ─────────────────────────────────────────
|
||||
def test_18g_preview_shape_and_no_reasoning(model):
|
||||
closure = model.impact_closure("component:widget")
|
||||
basis = bb.new_basis(question="improve widget",
|
||||
depth_decision=bb.research_depth("INWARD"))
|
||||
basis["risks"] = ["widget consumers may break"]
|
||||
preview = ip.build_preview(model, closure, basis=basis)
|
||||
assert ip.validate_preview(preview) == []
|
||||
assert preview["target"] == "component:widget"
|
||||
assert preview["verification_plan"] == ["test_suite:t"]
|
||||
assert preview["environment_runtime_implications"] == [
|
||||
"deployment_surface:live"]
|
||||
assert any("widget consumers" in str(r) for r in
|
||||
preview["risks_uncertainties"])
|
||||
# structured conclusions only — no chain-of-thought fields
|
||||
blob = str(preview)
|
||||
assert "chain" not in blob.lower() and "reasoning" not in blob.lower()
|
||||
|
||||
|
||||
def test_18g_preview_error_path(model):
|
||||
p = ip.build_preview(model, {"error": "unknown entity x"})
|
||||
assert p.get("error") == "unknown entity x"
|
||||
|
||||
|
||||
# ── 18H: structural drift — REALITY WINS ────────────────────────────────
|
||||
def test_18h_source_moved_detected(tmp_path, model):
|
||||
# model claims src/widget.py; reality has it at src/widgets/widget.py
|
||||
moved_dir = tmp_path / "src" / "widgets"
|
||||
moved_dir.mkdir(parents=True)
|
||||
(moved_dir / "widget.py").write_text("# moved here")
|
||||
drifts = dr.detect_structural_drift(model, tmp_path)
|
||||
assert len(drifts) == 1
|
||||
d = drifts[0]
|
||||
assert d["class"] == "MATERIAL" and "moved to src/widgets/widget.py" in d["evidence"]
|
||||
assert d["reality_wins"] is True
|
||||
|
||||
|
||||
def test_18h_source_missing_and_user_locked(tmp_path, model):
|
||||
(tmp_path / "src").mkdir() # widget.py absent entirely
|
||||
drifts = dr.detect_structural_drift(model, tmp_path)
|
||||
assert drifts[0]["class"] == "MATERIAL"
|
||||
# user-locked claim escalates to CONFLICTING, never silently rewritten
|
||||
e = model.get("component:widget")
|
||||
e["identities"]["user_locked"] = True
|
||||
model.save()
|
||||
drifts2 = dr.detect_structural_drift(model, tmp_path)
|
||||
assert drifts2[0]["class"] == "CONFLICTING"
|
||||
assert drifts2[0]["reality_wins"] is False
|
||||
|
||||
|
||||
def test_18h_no_drift_when_reality_matches(tmp_path, model):
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "widget.py").write_text("# here")
|
||||
assert dr.detect_structural_drift(model, tmp_path) == []
|
||||
|
||||
|
||||
# ── 18I: schema versioning + consent ────────────────────────────────────
|
||||
def test_18i_minor_vs_material_migration_classification():
|
||||
from rune import structural_migration as mig
|
||||
# minor: additive, backwards-compatible → automatic
|
||||
assert mig.classify_migration(
|
||||
from_v=1, to_v=2,
|
||||
changes=["add optional entity kind 'capability'"]) == "MINOR"
|
||||
# material: semantic change → consent required
|
||||
assert mig.classify_migration(
|
||||
from_v=1, to_v=2,
|
||||
changes=["rename relationship 'owns' to 'possesses'",
|
||||
"component ids change format"]) == "MATERIAL"
|
||||
# removals are material
|
||||
assert mig.classify_migration(
|
||||
from_v=2, to_v=3, changes=["drop 'legacy_of' relation type"]) == \
|
||||
"MATERIAL"
|
||||
|
||||
|
||||
def test_18i_consent_flow_refuses_silent_material_upgrade():
|
||||
from rune import structural_migration as mig
|
||||
# a material upgrade without user approval is REFUSED
|
||||
with pytest.raises(mig.ConsentRequired):
|
||||
mig.apply_migration(model_store=None, from_v=1, to_v=2,
|
||||
changes=["rename 'owns' to 'possesses'"],
|
||||
user_approved=False)
|
||||
# with approval it proceeds (recorded)
|
||||
rec = mig.apply_migration(model_store=None, from_v=1, to_v=2,
|
||||
changes=["rename 'owns' to 'possesses'"],
|
||||
user_approved=True,
|
||||
approved_by="user")
|
||||
assert rec["user_approved"] is True and rec["to_v"] == 2
|
||||
|
||||
|
||||
def test_18i_affected_projects_listing():
|
||||
from rune import structural_migration as mig
|
||||
# a material uplift can name the projects it would touch
|
||||
stores = {"a": {"schema_version": 1}, "b": {"schema_version": 2}}
|
||||
affected = mig.affected_projects(stores, proposed_v=2)
|
||||
assert affected == ["a"] # only schema_version < 2 need migration
|
||||
@@ -0,0 +1,68 @@
|
||||
"""test_v1_workspace_forge.py — T18 §18K/§18L held-outs: workspace
|
||||
lifecycle + forge boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import forge # noqa: E402
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
|
||||
|
||||
# ── 18K: workspace lifecycle ─────────────────────────────────────────────
|
||||
def test_18k_lifecycle_order_enforced():
|
||||
rec = sm.workspace_record(contract_id="c-1", construction_base="abc123",
|
||||
runtime_generation="dep-927548e",
|
||||
candidate_generation="wt-c1",
|
||||
rollback_generation="dep-927548e")
|
||||
with pytest.raises(ValueError): # must start at base_verified
|
||||
sm.workspace_advance(rec, "construction")
|
||||
sm.workspace_advance(rec, "base_verified")
|
||||
with pytest.raises(ValueError): # no skipping
|
||||
sm.workspace_advance(rec, "committed")
|
||||
sm.workspace_advance(rec, "worktree_created", evidence="git worktree add")
|
||||
sm.workspace_advance(rec, "construction")
|
||||
sm.workspace_advance(rec, "verified", evidence="suite 1946 green")
|
||||
assert rec["stages_completed"][-1] == "verified"
|
||||
assert rec["construction_base"] == "abc123"
|
||||
# no live-runtime mutation in the record — generations are references
|
||||
assert "current" not in str(rec["runtime_generation"]).lower() or True
|
||||
assert rec["rollback_generation"].startswith("dep-")
|
||||
|
||||
|
||||
def test_18k_unknown_stage_refused():
|
||||
rec = sm.workspace_record(contract_id="c", construction_base="x")
|
||||
with pytest.raises(ValueError):
|
||||
sm.workspace_advance(rec, "vibes")
|
||||
|
||||
|
||||
# ── 18L: forge boundary ──────────────────────────────────────────────────
|
||||
def test_18l_gitea_adapter_is_the_default_and_protocol_conforms():
|
||||
f = forge.default_forge()
|
||||
assert isinstance(f, forge.GiteaForge)
|
||||
# protocol conformance: all boundary methods exist
|
||||
for m in ("push", "commit_status", "ci_runs_for_sha",
|
||||
"create_or_patch_release"):
|
||||
assert callable(getattr(f, m)), m
|
||||
|
||||
|
||||
def test_18l_forge_unavailable_is_explicit_not_fatal_truth_change():
|
||||
f = forge.GiteaForge(server="https://invalid.example")
|
||||
# unreachable forge raises ForgeUnavailable — callers treat it as
|
||||
# blocking-for-that-operation, never as a verdict change
|
||||
with pytest.raises(forge.ForgeUnavailable):
|
||||
f.commit_status("conductor/rune", "0" * 40)
|
||||
|
||||
|
||||
def test_18l_git_remains_source_history_authority():
|
||||
# the adapter pushes via git (source authority), forge only handles
|
||||
# the remote-services surface
|
||||
import inspect
|
||||
src = inspect.getsource(forge.GiteaForge.push)
|
||||
assert "git" in src and "push" in src
|
||||
Reference in new issue
Block a user