diff --git a/.gitignore b/.gitignore index 73defebd..f674876e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ temp # Local caches .cache +.codex/ artifacts/pr-review/ __pycache__/ *.pyc diff --git a/ADVANCED_GUIDE.md b/ADVANCED_GUIDE.md new file mode 100644 index 00000000..8d3af5c9 --- /dev/null +++ b/ADVANCED_GUIDE.md @@ -0,0 +1,57 @@ + + + +# Advanced Guide — Echo + +This is the second-track manual for Echo. Use it when you need the deeper doctrine behind the theoretical foundations (AION Foundations), Wesley schema integration, and internal Spec details. + +For orientation and the productive-fast path, use the [GUIDE.md](./GUIDE.md). + +## Theoretical Foundations + +Echo implements ideas from the **AIΩN Foundations** paper series. + +### WARP Graphs (OG-I) + +A worldline algebra for recursive provenance. State is a finite directed multigraph where nodes and edges can contain nested graphs. + +### Deterministic Convergence (OG-IV) + +Independent rewrites commute under footprint conflict rules. State convergence does not imply provenance convergence—two worldlines can arrive at the same state through different histories. + +### Rulial Distance & Observer Geometry + +An observer is a structural five-tuple (Projection, Basis, State, Update, Emission). Echo surfaces what survives each layer of observation. + +## Internal Specs + +- **[warp-core spec](./docs/spec-warp-core.md)**: The transactional kernel and commit semantics. +- **[Tick Patch spec](./docs/spec-warp-tick-patch.md)**: The binary boundary for causal transitions. +- **[Merkle Commit](./docs/spec-merkle-commit.md)**: Snapshot hashing and state integrity. +- **[Deterministic Math](./docs/determinism/SPEC_DETERMINISTIC_MATH.md)**: Rules for 0-ULP cross-platform math. + +## Deterministic Policy + +Echo enforces a strict **'No-Network'** and **'No-Entropy'** policy in core paths to preserve causal replayability. This is verified in CI via `scripts/ban-nondeterminism.sh`. + +### Prohibited Patterns + +- **Network I/O**: `std::net`, `reqwest`, `ureq`, and other network crates are banned from deterministic paths. Causal history must be self-contained. +- **Entropy & Time**: `std::time::SystemTime`, `Instant::now`, `rand`, and `getrandom` are prohibited. Use the deterministic `Tick` and `Seed` provided by the kernel. +- **Unordered Collections**: `std::collections::HashMap` and `HashSet` are banned due to iteration order nondeterminism (DoS resistance/hashing variability). Use `BTreeMap`, `BTreeSet`, or stable-sort patterns on vectors before iteration. +- **Floating Point**: Direct use of `sin`, `cos`, `sqrt`, etc., is restricted. Use the `FixedTrig` oracles in `docs/determinism/SPEC_DETERMINISTIC_MATH.md` to ensure bit-exact convergence across platforms. + +## Wesley Integration + +The simulation protocol and graph schemas are increasingly defined via Wesley. + +- **Schema**: `schemas/runtime-schema.graphql` +- **Compiler**: Wesley generates bit-exact Rust/TS bridges and Zod validators. + +## Performance & Scaling + +Echo uses **WSC (Write-Streaming Columnar)**, a zero-copy snapshot format for fast state reload and verification. The hot render loop is optimized through reusable framebuffers and footprint-based scheduling. + +--- + +**The goal is inevitability. Every continuation from the past is explicit, capability-gated, and provenance-bearing.** diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5ea69c90 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ + + + +# AGENTS + +This guide is for AI agents and human operators recovering context in the Echo repository. + +## Git Rules + +- **NEVER** amend commits. +- **NEVER** rebase or force-push. +- **NEVER** push to `main` without explicit permission. +- Always use standard commits and regular pushes. + +## Documentation & Planning Map + +Do not audit the repository by recursively walking the filesystem. Follow the authoritative manifests: + +### 1. The Entrance + +- **`README.md`**: Public front door, core value prop, and quick tour. +- **`GUIDE.md`**: Orientation and productive-fast path. +- **`docs/index.md`**: VitePress documentation map. + +### 2. The Bedrock + +- **`ARCHITECTURE.md`**: Authoritative structural reference (Hexagonal, Core, Memory). +- **`VISION.md`**: Core tenets and the causal mission. +- **`METHOD.md`**: Repo work doctrine (Backlog lanes, Cycle loop). + +### 3. The Direction + +- **`docs/BEARING.md`**: Current execution gravity and active tensions. +- **`docs/design/ROADMAP.md`**: Broad strategic horizon and targets. +- **`backlog/`**: The active source of truth for pending work. + +### 4. The Proof + +- **`CHANGELOG.md`**: Historical truth of merged behavior. +- **`cargo xtask dind`**: Determinism convergence verification. + +## Context Recovery Protocol + +When starting a new session or recovering from context loss: + +1. **Read `docs/BEARING.md`** to find the current execution gravity. +2. **Read `METHOD.md`** to understand the work doctrine. +3. **Check `backlog/asap/`** for imminent work. +4. **Check `git log -n 5` and `git status`** to verify the current branch state. + +## Executable Claim Protocol + +Engineering work must converge around executable evidence, not broad repository +interpretation. Before editing code, reduce the task to one executable claim: + +1. **Bound the Claim**: State the behavior, invariant, or artifact that must + change. +2. **Name the Witness**: Identify the smallest test, check, script, compile + contract, golden vector, schema validation, or artifact inspection that can + prove the claim. +3. **Run the Witness When Feasible**: Prefer a failing witness before the fix. + If the witness cannot execute because the repository is already broken, + repair only the minimal compile/runtime blocker required to run it. +4. **Do Not Expand from an Unblocker**: Do not turn a compile blocker into + nearby architecture cleanup, docs cleanup, lint cleanup, migration sweep, or + opportunistic refactor. +5. **Green the Claim**: Implement the smallest fix, rerun the witness, and run + only directly relevant surrounding checks unless broader validation is + explicitly requested. +6. **Stop on Green**: When the witness passes and the requested scope is + satisfied, stop. Do not inspect more PR comments, audit more files, or clean + unrelated residue without a new executable claim. + +If unrelated failures remain, isolate them in the final report instead of +absorbing them into the task. Report files changed, symbols or behavior changed, +witness commands, pass/fail results, and intentional non-actions such as no +commit, no push, or no PR comment. + +## End of Turn Checklist + +After altering files: + +1. **Verify Truth**: Ensure documentation is updated if behavior or structure changed. +2. **Log Debt**: Add follow-on backlog items to `bad-code/` or `cool-ideas/`. +3. **Commit**: Use focused, conventional commit messages. Propose a draft before executing. +4. **Validate**: Run `cargo check` and relevant tests. + +--- + +**The goal is inevitability. Every feature is defined by its tests.** diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..02d02bdc --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,67 @@ + + + +# ARCHITECTURE + +Echo is an industrial-grade graph-rewrite simulation engine organized around a strict Hexagonal (Ports and Adapters) architecture and the WARP graph substrate. + +## System Shape + +Echo is the `hot` runtime within the larger Continuum platform. It is optimized for deterministic execution, parallel rule processing, and high-fidelity replay. + +```mermaid +flowchart TD + subgraph Ingress["Ingress Surfaces"] + WASM[Echo WASM Guest] + CLI[warp-cli] + APP[echo-app-core] + end + subgraph Core["warp-core (Engine)"] + RE[Rewrite Engine] + SCH[Deterministic Scheduler] + MATH[Deterministic Math] + MBUS[Materialization Bus] + end + subgraph Memory["Memory"] + WL[Worldlines] + RCP[Tick Receipts] + PROV[Provenance Store] + end + subgraph Ports["Driven Ports"] + SP[ScenePort] + TP[TTD Port] + end + + Ingress --> Core + Core --> Memory + Core --> Ports +``` + +## Core Tenets + +- **Structural Determinism**: Concurrency is structurally prevented by the snapshot-delta-merge model. Same rules + same hashes = same result. +- **WARP Substrate**: State is a typed, directed multigraph. Time is a hash chain of ticks. +- **Genealogy of Reality**: Every state transition traces back to a causal receipt. Provenance is a first-class citizen. +- **0-ULP Inevitability**: Cross-platform math convergence is enforced for consensus/default execution paths at the binary level; standard floats are excluded there, and wall-clock time is not part of state transition semantics. + +## Internal Pipeline (Hot Path) + +The Echo tick loop is a deterministic sequence: + +1. **Snapshot**: Capture the current immutable WARP graph state. +2. **Execute**: Run rewrite rules in parallel. Each rule reads the snapshot and writes to a private delta. +3. **Merge**: Merge deltas in canonical order. Conflict detection occurs via footprint enforcement. +4. **Commit**: Finalize the tick as a cryptographic commit in the worldline hash chain. +5. **Emit**: Project changes to driven ports (Scene, TTD, Materialization Bus). + +## WARP: Structural Worldline Memory + +Echo uses **WARP Graphs**—a worldline algebra for recursive provenance. + +- **Ticks**: Lamport clock values on a worldline. +- **Receipts**: Per-operation provenance from a materialized tick. +- **Strands**: Speculative causal lanes for counterfactual exploration. + +--- + +**The goal is inevitability. Every state transition is a provable consequence of its causal history.** diff --git a/CLAUDE.md b/CLAUDE.md index b99ac85d..da18f61f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ honestly — no skipping phases, no post-hoc design docs. ### The loop (agent responsibilities) -**Phase 1 — Pull and design** (when the human says "pull \"): +**Phase 1 — Pull and design** (when the human says "pull ``"): 1. Create a `cycle/` branch off `main` (e.g., `cycle/0003-dt-policy`). All cycle work happens on this branch. diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 00000000..fb57711e --- /dev/null +++ b/GUIDE.md @@ -0,0 +1,65 @@ + + + +# Guide — Echo + +This is the developer-level operator guide for Echo. Use it for orientation, the productive-fast path, and to understand how the simulation engine orchestrates the causal graph. + +For deep-track doctrine, theoretical foundations (AION Foundations), and internal spec details, use [ADVANCED_GUIDE.md](./ADVANCED_GUIDE.md). + +## Choose Your Lane + +### 1. Build a Causal Simulation + +Integrate deterministic graph rewriting into your application or game. + +- **Read**: [Start Here](./docs/guide/start-here.md) +- **Host**: [Architecture](./ARCHITECTURE.md) (Engine pipeline) + +### 2. Verify Determinism (DIND) + +Use the "Drill Sergeant" discipline to prove cross-platform convergence. + +- **Read**: [DIND Harness](./docs/dind-harness.md) +- **Run**: `cargo xtask dind run` + +### 3. Time Travel Debugging + +Explore the worldline algebra through the interactive debugger. + +- **WASM**: [ttd-browser](./crates/ttd-browser) +- **Host**: [echo-ttd](./crates/echo-ttd) + +### 4. Continuous Integration + +Understand the guardrails that prevent non-determinism from entering main. + +- **Check**: [`det-policy.yaml`](./det-policy.yaml) +- **Scripts**: `scripts/ban-nondeterminism.sh` + +## Big Picture: System Orchestration + +Echo is a tiered engine. You choose your depth based on the task: + +1. **Ingress Surfaces (Surfaces)**: The CLI, WASM guest, and App Core are thin interfaces that communicate with the engine. They ensure that transitions are always structured. +2. **warp-core (The Engine)**: The primary domain kernel. It orchestrates parallel rule execution, private deltas, and canonical merge. It ensures that concurrency is structurally prevented. +3. **WARP (Memory)**: The Structural Worldline Memory that tracks the evolution of your simulation state through hash-locked ticks. + +## Orientation Checklist + +- [ ] **I am setting up the repo**: Run `make hooks` and `cargo check`. +- [ ] **I am writing a new rule**: Declare your `Footprint` and test against `delta_validate`. +- [ ] **I am debugging a desync**: Run `cargo xtask dind run --seed ` to reproduce. +- [ ] **I am contributing to Echo**: Read `METHOD.md` and `docs/BEARING.md`. + +## Rule of Thumb + +If you need a comprehensive spec, use the [docs/index.md](./docs/index.md) map. + +If you need to know "what's true right now," use [docs/BEARING.md](./docs/BEARING.md). + +If you are just starting, use the [README.md](./README.md) and the orientation tracks above. + +--- + +**The goal is inevitability. Every state transition is a provable consequence of its causal history.** diff --git a/METHOD.md b/METHOD.md new file mode 100644 index 00000000..912a0754 --- /dev/null +++ b/METHOD.md @@ -0,0 +1,61 @@ + + + +# METHOD + +The Echo work doctrine: A backlog, a loop, and honest bookkeeping. + +## Principles + +- **The agent and the human sit at the same table.** Both matter. Both are named in every design. +- **Determinism is binary.** A system is either deterministic or it is not. Tests prove the inevitability. +- **The filesystem is the database.** A directory is a priority. A filename is an identity. Moving a file is a decision. +- **Process is calm.** No sprints or velocity theater. A backlog tiered by judgment, and a loop for doing it well. + +## Structure + +| Signpost | Role | +| :-------------------- | :-------------------------------------------------- | +| **`README.md`** | Public front door and project identity. | +| **`GUIDE.md`** | Orientation and productive-fast path. | +| **`BEARING.md`** | Current direction and active tensions. | +| **`VISION.md`** | Core tenets and the causal mission. | +| **`ARCHITECTURE.md`** | Authoritative structural reference. | +| **`CONTINUUM.md`** | Platform memo: hot/cold split and shared contracts. | +| **`METHOD.md`** | Repo work doctrine (this document). | + +## Backlog Lanes + +| Lane | Purpose | +| :---------------- | :--------------------------------------- | +| **`asap/`** | Imminent work; pull into the next cycle. | +| **`up-next/`** | Queued after `asap/`. | +| **`cool-ideas/`** | Uncommitted experiments. | +| **`bad-code/`** | Technical debt that must be addressed. | +| **`inbox/`** | Raw ideas. | + +## The Cycle Loop + +```mermaid +stateDiagram-v2 + direction LR + [*] --> Pull: asap/ + Pull --> Branch: cycle/ + Branch --> Red: failing tests + Red --> Green: passing tests + Green --> Retro: findings/debt + Retro --> Ship: PR to main + Ship --> [*] +``` + +1. **Pull**: Move an item from `backlog/asap/` to `docs/design/`. +2. **Branch**: Create `cycle/-`. +3. **Red**: Write failing tests. Playback questions become specs. +4. **Green**: Make them pass. Fix determinism drift (DIND). +5. **Retro**: Document findings and follow-on debt in the cycle doc. +6. **Ship**: Open a PR to `main`. Update `BEARING.md` and `CHANGELOG.md` after merge. + +## Naming Convention + +Backlog and cycle files follow: `-.md` or `-.md` +Example: `RE-017-byte-pipeline.md` diff --git a/VISION.md b/VISION.md new file mode 100644 index 00000000..328cde87 --- /dev/null +++ b/VISION.md @@ -0,0 +1,57 @@ + + + +# VISION + +Echo is an industrial-grade graph-rewrite simulation engine where state is a graph, time is a hash chain, and determinism is structurally enforced. + +```mermaid +mindmap + root((Echo)) + Structural Determinism + Parallel Rule Execution + Private Deltas + Canonical Merge + Inevitability + 0-ULP Cross-Platform + BTreeMap Everything + Banned Non-determinism + Causal Substrate + WARP Graph (DPO) + Recursive Provenance + Hash-Locked Ticks + Native Replay + Time Travel Debugging + Counterfactual Forking + Worldline algebra + Geometric Lawfulness + Footprint Enforcement + Guarded Views + Transactional Commits +``` + +## Core Tenets + +### 1. Concurrency is Structural + +Echo does not solve the concurrency problem; it structurally prevents it from existing. Rules read from immutable snapshots and write to private deltas. Order-independence is a property of the bedrock, not a side-effect of synchronization. + +### 2. Determinism is Binary + +A system is either deterministic or it is not. Echo bans the "approximately correct." Identical hashes across Linux, macOS, and Windows are the minimum bar. We ban non-deterministic sources (floats, system time, unseeded randomness) at the pre-commit and CI gates. + +### 3. Proof Over Honor + +Independency is declared via footprints and enforced at runtime. Footprint guards reject undeclared access, and violations poison deltas. We do not trust the rule-author; we trust the runtime proof. + +### 4. Replay as a Substrate Property + +Deterministic replay is not a feature you turn on; it is how the engine works. Every tick is a cryptographic commit in a hash chain. Rewind, fork, and diff are inherent capabilities of the worldline algebra. + +### 5. Systems Integrity + +The engine is built for the systems engineer. Strict lints, panic-free paths (Mr. Clean), and comprehensive determinism drills (DIND) ensure that Echo remains a professional-grade bedrock for causal simulation. + +--- + +**The goal is inevitability. Every state transition is a provable consequence of its causal history.** diff --git a/backlog/bad-code/RE-028-snapshot-accumulator-memoization.md b/backlog/bad-code/RE-028-snapshot-accumulator-memoization.md new file mode 100644 index 00000000..92f47a9d --- /dev/null +++ b/backlog/bad-code/RE-028-snapshot-accumulator-memoization.md @@ -0,0 +1,22 @@ + + + +# RE-028 — Merkle-Tree Memoization in Snapshot Accumulator + +Legend: [RE — Runtime Engine] + +## Idea + +The `SnapshotAccumulator` currently re-calculates the BLAKE3 hash for the entire graph hierarchy on every tick. In systems with very deep WARP nesting (graphs all the way down), this results in redundant hashing of unchanged sub-graphs. + +Implement a memoization strategy: cache the hash of each `WarpInstance` keyed by `(WarpId, content_fingerprint)`. If a sub-graph was not modified during the current tick (determined by its footprint), reuse the cached hash instead of descending. + +## Why + +1. **Performance**: Significantly reduces hashing overhead for complex simulations. +2. **Scalability**: Enables massive-scale causal graphs with thousands of nested instances. +3. **Efficiency**: Moves the cost of state integrity from O(TotalNodes) to O(ModifiedNodes). + +## Effort + +Medium — requires adding a hash-cache to the accumulator and integrating it with the instance-dirty tracking. diff --git a/backlog/bad-code/RE-029-concurrent-snapshot-fetching.md b/backlog/bad-code/RE-029-concurrent-snapshot-fetching.md new file mode 100644 index 00000000..91abe699 --- /dev/null +++ b/backlog/bad-code/RE-029-concurrent-snapshot-fetching.md @@ -0,0 +1,22 @@ + + + +# RE-029 — Enforce det_fixed by Default + +Legend: [RE — Runtime Engine] + +## Idea + +The project currently supports both `det_fixed` (DFix64) and optimistic `F32Scalar` math profiles. While useful for local experimentation, allowing `f32` in core paths introduces a high risk of cross-platform determinism poisoning if an external crate uses standard floats. + +Enforce the `det_fixed` feature by default in the main workspace and release profiles. Make `F32Scalar` an explicit, gated opt-in that triggers a compiler warning about "non-consensus path risk." + +## Why + +1. **Security**: Hardens the system against floating-point drift attacks. +2. **Predictability**: Ensures that all published artifacts satisfy the "0-ULP Inevitability" claim. +3. **Governance**: Aligns the implementation with the binary "Determinism is sacred" tenet. + +## Effort + +Small — update Cargo.toml default features and workspace-wide build scripts. diff --git a/backlog/cool-ideas/CI-001-causal-puzzle-engine.md b/backlog/cool-ideas/CI-001-causal-puzzle-engine.md new file mode 100644 index 00000000..43e518eb --- /dev/null +++ b/backlog/cool-ideas/CI-001-causal-puzzle-engine.md @@ -0,0 +1,22 @@ + + + +# CI-001 — Causal "Multiverse" Puzzle Engine + +Legend: [SURFACE — User Surfaces] + +## Idea + +Leverage Echo's native counterfactual forking and `kairos` (possibility) ticks to build a specialized engine for time-manipulation puzzles. + +Create a set of high-level rule templates that handle "Temporal Paradox" resolution: if an action in strand B contradicts an observation in worldline A, the engine should emit a "Causal Conflict" artifact that the game can use as a narrative or mechanical trigger. + +## Why + +1. **Product Differentiation**: Showcases Echo not just as a simulation engine, but as a "Causal Computer." +2. **Theory Proof**: Provides a practical application for the OG-IV (Suffix Transport) and worldline algebra foundations. +3. **Engagement**: Makes complex systems engineering concepts tangible through interactive gameplay. + +## Effort + +Large — requires a higher-level "Paradox API" and a set of demo scenarios. diff --git a/backlog/cool-ideas/CI-002-deterministic-flamegraphs.md b/backlog/cool-ideas/CI-002-deterministic-flamegraphs.md new file mode 100644 index 00000000..8c13e870 --- /dev/null +++ b/backlog/cool-ideas/CI-002-deterministic-flamegraphs.md @@ -0,0 +1,22 @@ + + + +# CI-002 — Deterministic Rule Profiling (Flamegraphs) + +Legend: [DX — Developer Experience] + +## Idea + +Traditional profiling is non-deterministic. Echo has the unique capability to know exactly which rule touched which graph region. + +Integrate a deterministic profiler into the scheduler that records canonical rule cost units per emission rather than wall-clock time or host CPU cycles. Export this data as a "Causal Flamegraph" where the Y-axis is the rule dependency stack and the X-axis is the deterministic cost. + +## Why + +1. **Performance Debugging**: Allows builders to find "heavy rules" without wall-clock noise. +2. **Reproducibility**: Profiling results are identical across runs, making optimization verification a science. +3. **Auditability**: Provides a machine-readable "cost receipt" for every tick. + +## Effort + +Medium-Large — requires canonical cost instrumentation in the scheduler and a data-export adapter. diff --git a/crates/echo-dind-tests/Cargo.toml b/crates/echo-dind-tests/Cargo.toml index 1d5dccd7..6612c7f1 100644 --- a/crates/echo-dind-tests/Cargo.toml +++ b/crates/echo-dind-tests/Cargo.toml @@ -4,6 +4,7 @@ name = "echo-dind-tests" version = "0.1.0" edition = "2021" +publish = false rust-version = "1.90.0" license = "Apache-2.0" description = "Determinism test kernel for DIND scenarios" @@ -16,7 +17,7 @@ categories = ["development-tools"] path = "src/lib.rs" [dependencies] -warp-core = { workspace = true } +warp-core = { workspace = true, features = ["native_rule_bootstrap"] } echo-dry-tests = { workspace = true } echo-wasm-abi = { workspace = true } bytes = "1.0" diff --git a/crates/echo-dry-tests/Cargo.toml b/crates/echo-dry-tests/Cargo.toml index 483cc146..fc09b6cd 100644 --- a/crates/echo-dry-tests/Cargo.toml +++ b/crates/echo-dry-tests/Cargo.toml @@ -5,6 +5,7 @@ name = "echo-dry-tests" version = "0.1.0" edition = "2021" +publish = false license.workspace = true repository.workspace = true rust-version.workspace = true @@ -15,7 +16,7 @@ categories = ["development-tools::testing"] [dependencies] # Core crates for test doubles -warp-core = { workspace = true } +warp-core = { workspace = true, features = ["native_rule_bootstrap"] } echo-graph = { workspace = true } echo-app-core = { workspace = true } diff --git a/crates/echo-wasm-abi/src/kernel_port.rs b/crates/echo-wasm-abi/src/kernel_port.rs index 409e621e..544fc5fa 100644 --- a/crates/echo-wasm-abi/src/kernel_port.rs +++ b/crates/echo-wasm-abi/src/kernel_port.rs @@ -1799,6 +1799,82 @@ pub struct NeighborhoodSite { pub participants: Vec, } +/// Shared lawful outcome kind for observer/debugger publication families. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AdmissionOutcomeKind { + /// One lawful derived result exists. + Derived, + /// Multiple lawful results coexist. + Plural, + /// A lawful conflict artifact was produced. + Conflict, + /// Lawful admission was obstructed. + Obstruction, +} + +/// Shared plurality for one published neighborhood core. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum NeighborhoodPlurality { + /// Only the primary lane participates. + Singleton, + /// Multiple lanes participate in the local site. + Plural, +} + +/// Shared participant role for one published neighborhood core. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum NeighborhoodParticipantRole { + /// The directly observed lane. + Primary, + /// The fork/source lane anchoring the primary strand. + BasisAnchor, + /// A read-only support lane. + Support, +} + +/// Shared participant for one published neighborhood core. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NeighborhoodParticipant { + /// Stable participant identity within the published site. + pub participant_id: String, + /// Stable lane identity for the participant carrier. + pub lane_id: String, + /// Optional strand identity when the participant is strand-backed. + pub strand_id: Option, + /// Participant role within the site. + pub role: NeighborhoodParticipantRole, + /// Exact participant frame index. + pub frame_index: u64, + /// Canonical state hash for the participant at that frame. + pub state_hash: String, +} + +/// Shared observer/debugger publication for one local neighborhood core. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NeighborhoodCore { + /// Stable identity for this published neighborhood core. + pub site_id: String, + /// Stable lane identity for the anchor worldline. + pub anchor_lane_id: String, + /// Exact resolved anchor frame index. + pub anchor_frame_index: u64, + /// Optional anchor head identity, when the kernel publishes one truthfully. + pub anchor_head_id: Option, + /// Top-level lawful outcome kind for the site. + pub outcome_kind: AdmissionOutcomeKind, + /// Shared singleton-vs-plural truth. + pub plurality: NeighborhoodPlurality, + /// Participating lanes for the published site. + pub participants: Vec, + /// Narrow human-readable summary for debugger surfaces. + pub summary: String, +} + /// Request payload for strand settlement publication. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SettlementRequest { @@ -2523,6 +2599,22 @@ pub trait KernelPort { }) } + /// Publish the shared neighborhood-core family projection for an explicit + /// observation request. + /// + /// This is the canonical shared observer/debugger read for the first + /// neighborhood-core family slice. The default implementation reports that + /// this projection is not supported by the kernel implementation. + fn observe_neighborhood_core( + &self, + _request: ObservationRequest, + ) -> Result { + Err(AbiError { + code: error_codes::NOT_SUPPORTED, + message: "observe_neighborhood_core is not supported by this kernel".into(), + }) + } + /// Compare a strand suffix against its recorded base coordinate. /// /// The default implementation reports that settlement publication is not diff --git a/crates/echo-wesley-gen/tests/fixtures/rewrite_api.generated.rs b/crates/echo-wesley-gen/tests/fixtures/rewrite_api.generated.rs new file mode 100644 index 00000000..23fcf476 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/rewrite_api.generated.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +// Generated by @wesley/generator-echo. Do not edit. +// Proof-slice rewrite API: capability-bounded Rust authoring surface. + +pub trait ReadCounter { + fn read_counter(&self) -> &Counter; +} + +pub trait WriteCounter { + fn write_counter(&mut self, value: Counter); +} + +#[derive(Debug, Clone, PartialEq)] +pub struct IncrementCounterArgs { + pub counter_id: String, +} + +pub trait IncrementCounterContext: ReadCounter + WriteCounter {} +impl IncrementCounterContext for T where T: ReadCounter + WriteCounter {} + +pub trait IncrementCounterRewrite { + type Error; + + fn apply(&self, ctx: &mut C, args: IncrementCounterArgs) -> Result + where C: IncrementCounterContext; +} diff --git a/crates/echo-wesley-gen/tests/fixtures/rewrite_api_structured.generated.rs b/crates/echo-wesley-gen/tests/fixtures/rewrite_api_structured.generated.rs new file mode 100644 index 00000000..0e8178d7 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/rewrite_api_structured.generated.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +// Generated by @wesley/generator-echo. Do not edit. +// Proof-slice rewrite API: capability-bounded Rust authoring surface. + +pub trait CreateBufferWorldlineCreateWorldlineSlot { + fn create_worldline_slot(&mut self, value: BufferWorldline) -> BufferWorldline; +} + +pub trait CreateBufferWorldlineCreateHeadSlot { + fn create_head_slot(&mut self, value: RopeHead) -> RopeHead; +} + +pub trait CreateBufferWorldlineCreateCheckpointSlot { + fn create_checkpoint_slot(&mut self, value: Checkpoint) -> Checkpoint; +} + +pub trait CreateBufferWorldlineCreateInitialBlobSlot { + fn create_initial_blob_slot(&mut self, value: TextBlob) -> TextBlob; +} + +pub trait CreateBufferWorldlineCreateInitialLeafSlot { + fn create_initial_leaf_slot(&mut self, value: RopeLeaf) -> RopeLeaf; +} + +pub trait CreateCheckpointReadWorldlineSlot { + fn read_worldline_slot(&self) -> &BufferWorldline; +} + +pub trait CreateCheckpointReadCurrentHeadSlot { + fn read_current_head_slot(&self) -> &RopeHead; +} + +pub trait CreateCheckpointCreateCheckpointSlot { + fn create_checkpoint_slot(&mut self, value: Checkpoint) -> Checkpoint; +} + +pub trait ReplaceRangeAsTickReadWorldlineSlot { + fn read_worldline_slot(&self) -> &BufferWorldline; +} + +pub trait ReplaceRangeAsTickWriteWorldlineSlot { + fn write_worldline_slot(&mut self, value: BufferWorldline); +} + +pub trait ReplaceRangeAsTickReadBaseHeadSlot { + fn read_base_head_slot(&self) -> &RopeHead; +} + +pub enum ReplaceRangeAsTickTouchedRopeClosureItemRef<'a> { + RopeBranch(&'a RopeBranch), + RopeLeaf(&'a RopeLeaf), + TextBlob(&'a TextBlob), +} + +pub trait ReplaceRangeAsTickReadTouchedRopeClosure { + fn read_touched_rope_closure(&self) -> Vec>; +} + +pub enum ReplaceRangeAsTickAffectedAnchorsClosureItemRef<'a> { + Anchor(&'a Anchor), +} + +pub trait ReplaceRangeAsTickReadAffectedAnchorsClosure { + fn read_affected_anchors_closure(&self) + -> Vec>; +} + +pub trait ReplaceRangeAsTickCreateNewBlobSlot { + fn create_new_blob_slot(&mut self, value: TextBlob) -> TextBlob; +} + +pub trait ReplaceRangeAsTickCreateNewLeavesSlot { + fn create_new_leaves_slot(&mut self, value: RopeLeaf) -> RopeLeaf; +} + +pub trait ReplaceRangeAsTickCreateNewBranchesSlot { + fn create_new_branches_slot(&mut self, value: RopeBranch) -> RopeBranch; +} + +pub trait ReplaceRangeAsTickCreateNextHeadSlot { + fn create_next_head_slot(&mut self, value: RopeHead) -> RopeHead; +} + +pub trait ReplaceRangeAsTickCreateTickSlot { + fn create_tick_slot(&mut self, value: Tick) -> Tick; +} + +pub trait ReplaceRangeAsTickCreateReceiptSlot { + fn create_receipt_slot(&mut self, value: TickReceipt) -> TickReceipt; +} + +pub trait ReplaceRangeAsTickUpdateWorldlineCanonicalHead { + fn update_worldline_canonical_head(&mut self, value: String); +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateBufferWorldlineArgs { + pub input: CreateBufferWorldlineInput, +} + +// CreateBufferWorldline forbidden surfaces: AstState, Diagnostics, GitWitness, UiState +pub trait CreateBufferWorldlineContext: + CreateBufferWorldlineCreateWorldlineSlot + + CreateBufferWorldlineCreateHeadSlot + + CreateBufferWorldlineCreateCheckpointSlot + + CreateBufferWorldlineCreateInitialBlobSlot + + CreateBufferWorldlineCreateInitialLeafSlot +{ +} +impl CreateBufferWorldlineContext for T where + T: CreateBufferWorldlineCreateWorldlineSlot + + CreateBufferWorldlineCreateHeadSlot + + CreateBufferWorldlineCreateCheckpointSlot + + CreateBufferWorldlineCreateInitialBlobSlot + + CreateBufferWorldlineCreateInitialLeafSlot +{ +} + +pub trait CreateBufferWorldlineRewrite { + type Error; + + fn apply( + &self, + ctx: &mut C, + args: CreateBufferWorldlineArgs, + ) -> Result + where + C: CreateBufferWorldlineContext; +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateCheckpointArgs { + pub input: CreateCheckpointInput, +} + +// CreateCheckpoint forbidden surfaces: RopeBranch, RopeLeaf, TextBlob, Tick, TickReceipt, AstState, Diagnostics, GitWitness, UiState +pub trait CreateCheckpointContext: + CreateCheckpointReadWorldlineSlot + + CreateCheckpointReadCurrentHeadSlot + + CreateCheckpointCreateCheckpointSlot +{ +} +impl CreateCheckpointContext for T where + T: CreateCheckpointReadWorldlineSlot + + CreateCheckpointReadCurrentHeadSlot + + CreateCheckpointCreateCheckpointSlot +{ +} + +pub trait CreateCheckpointRewrite { + type Error; + + fn apply(&self, ctx: &mut C, args: CreateCheckpointArgs) + -> Result + where + C: CreateCheckpointContext; +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ReplaceRangeAsTickArgs { + pub input: ReplaceRangeAsTickInput, +} + +// ReplaceRangeAsTick forbidden surfaces: AstState, Diagnostics, GitWitness, UiState +pub trait ReplaceRangeAsTickContext: + ReplaceRangeAsTickReadWorldlineSlot + + ReplaceRangeAsTickWriteWorldlineSlot + + ReplaceRangeAsTickReadBaseHeadSlot + + ReplaceRangeAsTickReadTouchedRopeClosure + + ReplaceRangeAsTickReadAffectedAnchorsClosure + + ReplaceRangeAsTickCreateNewBlobSlot + + ReplaceRangeAsTickCreateNewLeavesSlot + + ReplaceRangeAsTickCreateNewBranchesSlot + + ReplaceRangeAsTickCreateNextHeadSlot + + ReplaceRangeAsTickCreateTickSlot + + ReplaceRangeAsTickCreateReceiptSlot + + ReplaceRangeAsTickUpdateWorldlineCanonicalHead +{ +} +impl ReplaceRangeAsTickContext for T where + T: ReplaceRangeAsTickReadWorldlineSlot + + ReplaceRangeAsTickWriteWorldlineSlot + + ReplaceRangeAsTickReadBaseHeadSlot + + ReplaceRangeAsTickReadTouchedRopeClosure + + ReplaceRangeAsTickReadAffectedAnchorsClosure + + ReplaceRangeAsTickCreateNewBlobSlot + + ReplaceRangeAsTickCreateNewLeavesSlot + + ReplaceRangeAsTickCreateNewBranchesSlot + + ReplaceRangeAsTickCreateNextHeadSlot + + ReplaceRangeAsTickCreateTickSlot + + ReplaceRangeAsTickCreateReceiptSlot + + ReplaceRangeAsTickUpdateWorldlineCanonicalHead +{ +} + +pub trait ReplaceRangeAsTickRewrite { + type Error; + + fn apply( + &self, + ctx: &mut C, + args: ReplaceRangeAsTickArgs, + ) -> Result + where + C: ReplaceRangeAsTickContext; +} diff --git a/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_dishonest_echo_side.rs b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_dishonest_echo_side.rs new file mode 100644 index 00000000..bbd0e3ed --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_dishonest_echo_side.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#[derive(Debug, Clone, PartialEq)] +pub struct Counter { + pub id: String, + pub value: i64, +} + +pub struct CounterStore { + pub counter: Counter, +} + +impl ReadCounter for CounterStore { + fn read_counter(&self) -> &Counter { + &self.counter + } +} + +impl WriteCounter for CounterStore { + fn write_counter(&mut self, value: Counter) { + self.counter = value; + } +} + +pub struct Increment; + +impl IncrementCounterRewrite for Increment { + type Error = (); + + fn apply(&self, ctx: &mut C, _args: IncrementCounterArgs) -> Result + where + C: IncrementCounterContext, + { + ctx.delete_counter(); + Ok(ctx.read_counter().clone()) + } +} diff --git a/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_dishonest_structured_echo_side.rs b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_dishonest_structured_echo_side.rs new file mode 100644 index 00000000..6daf26b1 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_dishonest_structured_echo_side.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#[derive(Debug, Clone, PartialEq)] +pub struct BufferWorldline { + pub worldline_id: String, + pub canonical_head_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RopeHead { + pub head_id: String, + pub worldline_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RopeBranch { + pub branch_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RopeLeaf { + pub leaf_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TextBlob { + pub blob_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Anchor { + pub anchor_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Tick { + pub tick_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TickReceipt { + pub receipt_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Checkpoint { + pub checkpoint_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateBufferWorldlineInput { + pub buffer_key: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateBufferWorldlineResult { + pub worldline_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateCheckpointInput { + pub worldline_id: String, + pub kind: String, + pub label: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateCheckpointResult { + pub checkpoint_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ReplaceRangeAsTickInput { + pub worldline_id: String, + pub base_head_id: String, + pub start_byte: i64, + pub end_byte: i64, + pub insert_text: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ReplaceRangeAsTickResult { + pub worldline_id: String, + pub next_head_id: String, + pub tick_id: String, + pub receipt_id: String, +} + +pub struct ReplaceRangeStore { + pub worldline: BufferWorldline, + pub base_head: RopeHead, + pub branch: RopeBranch, + pub leaf: RopeLeaf, + pub blob: TextBlob, + pub anchor: Anchor, + pub next_head: RopeHead, + pub tick: Tick, + pub receipt: TickReceipt, +} + +impl ReplaceRangeAsTickReadWorldlineSlot for ReplaceRangeStore { + fn read_worldline_slot(&self) -> &BufferWorldline { + &self.worldline + } +} + +impl ReplaceRangeAsTickWriteWorldlineSlot for ReplaceRangeStore { + fn write_worldline_slot(&mut self, value: BufferWorldline) { + self.worldline = value; + } +} + +impl ReplaceRangeAsTickReadBaseHeadSlot for ReplaceRangeStore { + fn read_base_head_slot(&self) -> &RopeHead { + &self.base_head + } +} + +impl ReplaceRangeAsTickReadTouchedRopeClosure for ReplaceRangeStore { + fn read_touched_rope_closure(&self) -> Vec> { + vec![ + ReplaceRangeAsTickTouchedRopeClosureItemRef::RopeBranch(&self.branch), + ReplaceRangeAsTickTouchedRopeClosureItemRef::RopeLeaf(&self.leaf), + ReplaceRangeAsTickTouchedRopeClosureItemRef::TextBlob(&self.blob), + ] + } +} + +impl ReplaceRangeAsTickReadAffectedAnchorsClosure for ReplaceRangeStore { + fn read_affected_anchors_closure( + &self, + ) -> Vec> { + vec![ReplaceRangeAsTickAffectedAnchorsClosureItemRef::Anchor(&self.anchor)] + } +} + +impl ReplaceRangeAsTickCreateNewBlobSlot for ReplaceRangeStore { + fn create_new_blob_slot(&mut self, value: TextBlob) -> TextBlob { + self.blob = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateNewLeavesSlot for ReplaceRangeStore { + fn create_new_leaves_slot(&mut self, value: RopeLeaf) -> RopeLeaf { + self.leaf = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateNewBranchesSlot for ReplaceRangeStore { + fn create_new_branches_slot(&mut self, value: RopeBranch) -> RopeBranch { + self.branch = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateNextHeadSlot for ReplaceRangeStore { + fn create_next_head_slot(&mut self, value: RopeHead) -> RopeHead { + self.next_head = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateTickSlot for ReplaceRangeStore { + fn create_tick_slot(&mut self, value: Tick) -> Tick { + self.tick = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateReceiptSlot for ReplaceRangeStore { + fn create_receipt_slot(&mut self, value: TickReceipt) -> TickReceipt { + self.receipt = value.clone(); + value + } +} + +impl ReplaceRangeAsTickUpdateWorldlineCanonicalHead for ReplaceRangeStore { + fn update_worldline_canonical_head(&mut self, value: String) { + self.worldline.canonical_head_id = value; + } +} + +pub struct ReplaceRange; + +impl ReplaceRangeAsTickRewrite for ReplaceRange { + type Error = (); + + fn apply( + &self, + ctx: &mut C, + _args: ReplaceRangeAsTickArgs, + ) -> Result + where + C: ReplaceRangeAsTickContext, + { + ctx.read_ast_state_slot(); + Ok(ReplaceRangeAsTickResult { + worldline_id: ctx.read_worldline_slot().worldline_id.clone(), + next_head_id: String::new(), + tick_id: String::new(), + receipt_id: String::new(), + }) + } +} diff --git a/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_valid_echo_side.rs b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_valid_echo_side.rs new file mode 100644 index 00000000..78e16a94 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_valid_echo_side.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#[derive(Debug, Clone, PartialEq)] +pub struct Counter { + pub id: String, + pub value: i64, +} + +pub struct CounterStore { + pub counter: Counter, +} + +impl ReadCounter for CounterStore { + fn read_counter(&self) -> &Counter { + &self.counter + } +} + +impl WriteCounter for CounterStore { + fn write_counter(&mut self, value: Counter) { + self.counter = value; + } +} + +pub struct Increment; + +impl IncrementCounterRewrite for Increment { + type Error = (); + + fn apply(&self, ctx: &mut C, _args: IncrementCounterArgs) -> Result + where + C: IncrementCounterContext, + { + let mut next = ctx.read_counter().clone(); + next.value += 1; + ctx.write_counter(next.clone()); + Ok(next) + } +} diff --git a/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_valid_structured_echo_side.rs b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_valid_structured_echo_side.rs new file mode 100644 index 00000000..224a12c3 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/rewrite_contract_valid_structured_echo_side.rs @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#[derive(Debug, Clone, PartialEq)] +pub struct BufferWorldline { + pub worldline_id: String, + pub canonical_head_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RopeHead { + pub head_id: String, + pub worldline_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RopeBranch { + pub branch_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RopeLeaf { + pub leaf_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TextBlob { + pub blob_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Anchor { + pub anchor_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Tick { + pub tick_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TickReceipt { + pub receipt_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Checkpoint { + pub checkpoint_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateBufferWorldlineInput { + pub buffer_key: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateBufferWorldlineResult { + pub worldline_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateCheckpointInput { + pub worldline_id: String, + pub kind: String, + pub label: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CreateCheckpointResult { + pub checkpoint_id: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ReplaceRangeAsTickInput { + pub worldline_id: String, + pub base_head_id: String, + pub start_byte: i64, + pub end_byte: i64, + pub insert_text: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ReplaceRangeAsTickResult { + pub worldline_id: String, + pub next_head_id: String, + pub tick_id: String, + pub receipt_id: String, +} + +pub struct ReplaceRangeStore { + pub worldline: BufferWorldline, + pub base_head: RopeHead, + pub branch: RopeBranch, + pub leaf: RopeLeaf, + pub blob: TextBlob, + pub anchor: Anchor, + pub next_head: RopeHead, + pub tick: Tick, + pub receipt: TickReceipt, +} + +impl ReplaceRangeAsTickReadWorldlineSlot for ReplaceRangeStore { + fn read_worldline_slot(&self) -> &BufferWorldline { + &self.worldline + } +} + +impl ReplaceRangeAsTickWriteWorldlineSlot for ReplaceRangeStore { + fn write_worldline_slot(&mut self, value: BufferWorldline) { + self.worldline = value; + } +} + +impl ReplaceRangeAsTickReadBaseHeadSlot for ReplaceRangeStore { + fn read_base_head_slot(&self) -> &RopeHead { + &self.base_head + } +} + +impl ReplaceRangeAsTickReadTouchedRopeClosure for ReplaceRangeStore { + fn read_touched_rope_closure(&self) -> Vec> { + vec![ + ReplaceRangeAsTickTouchedRopeClosureItemRef::RopeBranch(&self.branch), + ReplaceRangeAsTickTouchedRopeClosureItemRef::RopeLeaf(&self.leaf), + ReplaceRangeAsTickTouchedRopeClosureItemRef::TextBlob(&self.blob), + ] + } +} + +impl ReplaceRangeAsTickReadAffectedAnchorsClosure for ReplaceRangeStore { + fn read_affected_anchors_closure( + &self, + ) -> Vec> { + vec![ReplaceRangeAsTickAffectedAnchorsClosureItemRef::Anchor(&self.anchor)] + } +} + +impl ReplaceRangeAsTickCreateNewBlobSlot for ReplaceRangeStore { + fn create_new_blob_slot(&mut self, value: TextBlob) -> TextBlob { + self.blob = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateNewLeavesSlot for ReplaceRangeStore { + fn create_new_leaves_slot(&mut self, value: RopeLeaf) -> RopeLeaf { + self.leaf = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateNewBranchesSlot for ReplaceRangeStore { + fn create_new_branches_slot(&mut self, value: RopeBranch) -> RopeBranch { + self.branch = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateNextHeadSlot for ReplaceRangeStore { + fn create_next_head_slot(&mut self, value: RopeHead) -> RopeHead { + self.next_head = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateTickSlot for ReplaceRangeStore { + fn create_tick_slot(&mut self, value: Tick) -> Tick { + self.tick = value.clone(); + value + } +} + +impl ReplaceRangeAsTickCreateReceiptSlot for ReplaceRangeStore { + fn create_receipt_slot(&mut self, value: TickReceipt) -> TickReceipt { + self.receipt = value.clone(); + value + } +} + +impl ReplaceRangeAsTickUpdateWorldlineCanonicalHead for ReplaceRangeStore { + fn update_worldline_canonical_head(&mut self, value: String) { + self.worldline.canonical_head_id = value; + } +} + +pub struct ReplaceRange; + +impl ReplaceRangeAsTickRewrite for ReplaceRange { + type Error = (); + + fn apply( + &self, + ctx: &mut C, + args: ReplaceRangeAsTickArgs, + ) -> Result + where + C: ReplaceRangeAsTickContext, + { + let _ = args.input.start_byte; + let _ = ctx.read_touched_rope_closure(); + let _ = ctx.read_affected_anchors_closure(); + let next_head = ctx.create_next_head_slot(RopeHead { + head_id: "head-2".to_owned(), + worldline_id: ctx.read_worldline_slot().worldline_id.clone(), + }); + let tick = ctx.create_tick_slot(Tick { + tick_id: "tick-1".to_owned(), + }); + let receipt = ctx.create_receipt_slot(TickReceipt { + receipt_id: "receipt-1".to_owned(), + }); + ctx.update_worldline_canonical_head(next_head.head_id.clone()); + Ok(ReplaceRangeAsTickResult { + worldline_id: ctx.read_worldline_slot().worldline_id.clone(), + next_head_id: next_head.head_id, + tick_id: tick.tick_id, + receipt_id: receipt.receipt_id, + }) + } +} diff --git a/crates/echo-wesley-gen/tests/rewrite_api_contract.rs b/crates/echo-wesley-gen/tests/rewrite_api_contract.rs new file mode 100644 index 00000000..727bbf04 --- /dev/null +++ b/crates/echo-wesley-gen/tests/rewrite_api_contract.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::unwrap_used)] +//! Consumer-side proof that Echo can compile against Wesley's bounded rewrite API. + +use std::fs::{create_dir_all, remove_dir_all, write}; +use std::path::PathBuf; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +const GENERATED_REWRITE_API: &str = include_str!("fixtures/rewrite_api.generated.rs"); +const GENERATED_STRUCTURED_REWRITE_API: &str = + include_str!("fixtures/rewrite_api_structured.generated.rs"); +const VALID_ECHO_SIDE_IMPLEMENTATION: &str = + include_str!("fixtures/rewrite_contract_valid_echo_side.rs"); +const DISHONEST_ECHO_SIDE_IMPLEMENTATION: &str = + include_str!("fixtures/rewrite_contract_dishonest_echo_side.rs"); +const VALID_STRUCTURED_ECHO_SIDE_IMPLEMENTATION: &str = + include_str!("fixtures/rewrite_contract_valid_structured_echo_side.rs"); +const DISHONEST_STRUCTURED_ECHO_SIDE_IMPLEMENTATION: &str = + include_str!("fixtures/rewrite_contract_dishonest_structured_echo_side.rs"); + +static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); + +fn compile_rust(source: &str) -> std::process::Output { + let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "echo-wesley-gen-rewrite-proof-{}-{}-{}", + std::process::id(), + nanos, + unique + )); + create_dir_all(&dir).expect("failed to create temp dir"); + + let src_path: PathBuf = dir.join("proof.rs"); + let out_path: PathBuf = dir.join("proof.rlib"); + write(&src_path, source).expect("failed to write proof source"); + + let output = Command::new("rustc") + .args([ + "--edition", + "2021", + "--crate-type", + "lib", + src_path.to_str().expect("non-utf8 source path"), + "-o", + out_path.to_str().expect("non-utf8 output path"), + ]) + .output() + .expect("failed to invoke rustc"); + + remove_dir_all(&dir).expect("failed to remove temp dir"); + output +} + +#[test] +fn fixture_exposes_only_declared_counter_capabilities() { + assert!(GENERATED_REWRITE_API.contains("pub trait ReadCounter")); + assert!(GENERATED_REWRITE_API.contains("pub trait WriteCounter")); + assert!(GENERATED_REWRITE_API.contains("pub trait IncrementCounterContext")); + assert!(GENERATED_REWRITE_API.contains("pub trait IncrementCounterRewrite")); + assert!(!GENERATED_REWRITE_API.contains("DeleteCounter")); +} + +#[test] +fn valid_echo_side_implementation_compiles() { + let compile = compile_rust(&format!( + "{GENERATED_REWRITE_API}\n{VALID_ECHO_SIDE_IMPLEMENTATION}" + )); + + assert!( + compile.status.success(), + "rustc failed: {}", + String::from_utf8_lossy(&compile.stderr) + ); +} + +#[test] +fn dishonest_echo_side_implementation_fails_to_compile() { + let compile = compile_rust(&format!( + "{GENERATED_REWRITE_API}\n{DISHONEST_ECHO_SIDE_IMPLEMENTATION}" + )); + + assert!( + !compile.status.success(), + "expected rustc failure, got success" + ); + let stderr = String::from_utf8_lossy(&compile.stderr); + assert!( + stderr.contains("delete_counter"), + "unexpected stderr: {stderr}" + ); +} + +#[test] +fn structured_fixture_exposes_only_declared_replace_range_capabilities() { + assert!( + GENERATED_STRUCTURED_REWRITE_API.contains("pub trait ReplaceRangeAsTickReadWorldlineSlot") + ); + assert!(GENERATED_STRUCTURED_REWRITE_API + .contains("pub trait ReplaceRangeAsTickReadTouchedRopeClosure")); + assert!( + GENERATED_STRUCTURED_REWRITE_API.contains("pub trait ReplaceRangeAsTickCreateNextHeadSlot") + ); + assert!(GENERATED_STRUCTURED_REWRITE_API + .contains("pub trait ReplaceRangeAsTickUpdateWorldlineCanonicalHead")); + assert!(GENERATED_STRUCTURED_REWRITE_API.contains( + "// ReplaceRangeAsTick forbidden surfaces: AstState, Diagnostics, GitWitness, UiState" + )); + assert!(!GENERATED_STRUCTURED_REWRITE_API.contains("read_ast_state_slot")); +} + +#[test] +fn valid_structured_echo_side_implementation_compiles() { + let compile = compile_rust(&format!( + "{GENERATED_STRUCTURED_REWRITE_API}\n{VALID_STRUCTURED_ECHO_SIDE_IMPLEMENTATION}" + )); + + assert!( + compile.status.success(), + "rustc failed: {}", + String::from_utf8_lossy(&compile.stderr) + ); +} + +#[test] +fn dishonest_structured_echo_side_implementation_fails_to_compile() { + let compile = compile_rust(&format!( + "{GENERATED_STRUCTURED_REWRITE_API}\n{DISHONEST_STRUCTURED_ECHO_SIDE_IMPLEMENTATION}" + )); + + assert!( + !compile.status.success(), + "expected rustc failure, got success" + ); + let stderr = String::from_utf8_lossy(&compile.stderr); + assert!( + stderr.contains("read_ast_state_slot"), + "unexpected stderr: {stderr}" + ); +} diff --git a/crates/warp-benches/Cargo.toml b/crates/warp-benches/Cargo.toml index 109e72b9..2a979ae8 100644 --- a/crates/warp-benches/Cargo.toml +++ b/crates/warp-benches/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] # Pin version alongside path to satisfy cargo-deny wildcard bans -warp-core = { version = "0.1.0", path = "../warp-core" } +warp-core = { version = "0.1.0", path = "../warp-core", features = ["native_rule_bootstrap"] } echo-dry-tests = { version = "0.1.0", path = "../echo-dry-tests" } rayon = "~1.10" diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 8694ea3b..ae2b95ff 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -34,6 +34,10 @@ echo-dry-tests = { workspace = true } [features] default = [] +# INTERNAL ONLY: exposes native Rust rule authoring seams for Echo's own +# bootstrap fixtures, tests, and benchmarks. This is not the public authoring +# path for application rewrites. +native_rule_bootstrap = [] # Optional regression check for PRNG sequences; off by default to avoid # freezing algorithm choices. Used only in tests guarded with `cfg(feature)`. golden_prng = [] diff --git a/crates/warp-core/src/admission.rs b/crates/warp-core/src/admission.rs new file mode 100644 index 00000000..48354356 --- /dev/null +++ b/crates/warp-core/src/admission.rs @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Admission-side nouns for Echo's bounded-site law. +//! +//! A [`BoundedSite`] is the admission-side formalisation of focal closure. It +//! is intentionally derived from existing runtime truth rather than introducing +//! a second geometry system. In the first cut, the site is derived directly +//! from a rewrite's declared [`Footprint`](crate::footprint::Footprint): +//! +//! - `claim_footprint` preserves the full declared read/write claim +//! - `affected_region` captures the local write wake +//! - `reintegration_boundary` preserves the boundary ports implicated by the claim +//! +//! Later revisions may enrich the affected region and reintegration boundary +//! without changing the single-tick law that all admission still happens under +//! `super_tick()`. + +use crate::footprint::{AttachmentSet, EdgeSet, Footprint, NodeSet, PortSet}; +use crate::ident::Hash; + +/// Engine-defined policy identity for one admission act. +/// +/// This is intentionally narrow in the first cut: it names the deterministic +/// law selected by the host/runtime without permitting host-authored execution +/// code to redefine admission semantics. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AdmissionPolicyRef { + /// Stable policy identifier committed into downstream shell artifacts. + pub policy_id: Hash, +} + +/// Locally affected region derived from a claim footprint. +/// +/// In the first cut, this is the direct write wake of the claim. Future +/// revisions may extend it with richer derived closure without changing the +/// primary `BoundedSite` contract. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct AffectedRegion { + /// Nodes locally affected by the claim. + pub nodes: NodeSet, + /// Edges locally affected by the claim. + pub edges: EdgeSet, + /// Attachments locally affected by the claim. + pub attachments: AttachmentSet, +} + +impl AffectedRegion { + /// Derives the first-cut affected region from a claim footprint. + #[must_use] + pub fn from_footprint(footprint: &Footprint) -> Self { + Self { + nodes: footprint.n_write.clone(), + edges: footprint.e_write.clone(), + attachments: footprint.a_write.clone(), + } + } +} + +/// Boundary surface that later comparison or settlement must respect. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ReintegrationBoundary { + /// Input ports implicated by the claim. + pub inputs: PortSet, + /// Output ports implicated by the claim. + pub outputs: PortSet, +} + +impl ReintegrationBoundary { + /// Derives the first-cut reintegration boundary from a claim footprint. + #[must_use] + pub fn from_footprint(footprint: &Footprint) -> Self { + Self { + inputs: footprint.b_in.clone(), + outputs: footprint.b_out.clone(), + } + } +} + +/// Admission-side focal closure for one local claim site. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct BoundedSite { + /// Full declared read/write claim for the site. + pub claim_footprint: Footprint, + /// Local write wake used for first-cut admission reasoning. + pub affected_region: AffectedRegion, + /// Boundary surface implicated by the claim. + pub reintegration_boundary: ReintegrationBoundary, +} + +impl BoundedSite { + /// Builds a bounded site from the current footprint model. + #[must_use] + pub fn from_footprint(footprint: &Footprint) -> Self { + Self { + claim_footprint: footprint.clone(), + affected_region: AffectedRegion::from_footprint(footprint), + reintegration_boundary: ReintegrationBoundary::from_footprint(footprint), + } + } +} + +impl From<&Footprint> for BoundedSite { + fn from(value: &Footprint) -> Self { + Self::from_footprint(value) + } +} + +impl From for BoundedSite { + fn from(value: Footprint) -> Self { + Self::from_footprint(&value) + } +} + +/// First-class lawful plurality over one bounded site. +/// +/// Echo does not require every subsystem to emit this exact struct today, but +/// the runtime needs one shared noun family for lawful plurality rather than +/// flattening it into ad hoc arrays or generic failure residue. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PluralArtifact { + /// Site over which plurality remained lawful. + pub site: BoundedSite, + /// Participants whose claims coexist at that site. + pub participants: Vec

, + /// Claims that remained irreducibly plural. + pub claims: Vec, +} + +impl PluralArtifact { + /// Constructs a plural artifact with parallel participant and claim lists. + #[must_use] + pub fn new(site: BoundedSite, participants: Vec

, claims: Vec) -> Self { + assert_eq!( + participants.len(), + claims.len(), + "plural participants must stay parallel to claims" + ); + Self { + site, + participants, + claims, + } + } +} + +/// Shared lawful outcome family for admission acts. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AdmissionOutcomeKind { + /// A single derived result was admitted. + Derived, + /// Multiple claims remained lawfully plural over one bounded site. + Plural, + /// The act produced explicit conflict residue. + Conflict, + /// The act was obstructed before a derived or plural result could be admitted. + Obstruction, +} + +/// Shared lawful outcome family for admission acts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AdmissionOutcome { + /// A single derived result was admitted. + Derived(R), + /// Multiple claims remained lawfully plural over the bounded site. + Plural(Box>), + /// The act produced explicit conflict residue. + Conflict(C), + /// The act was obstructed before a derived or plural result could be admitted. + Obstruction(O), +} + +impl AdmissionOutcome { + /// Returns the top-level lawful outcome kind. + #[must_use] + pub fn kind(&self) -> AdmissionOutcomeKind { + match self { + Self::Derived(_) => AdmissionOutcomeKind::Derived, + Self::Plural(_) => AdmissionOutcomeKind::Plural, + Self::Conflict(_) => AdmissionOutcomeKind::Conflict, + Self::Obstruction(_) => AdmissionOutcomeKind::Obstruction, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::attachment::AttachmentKey; + use crate::footprint::pack_port_key; + use crate::ident::{make_edge_id, make_node_id, make_warp_id, EdgeKey, NodeKey}; + + #[test] + fn bounded_site_derives_from_footprint_without_inventing_new_geometry() { + let warp_id = make_warp_id("bounded-site-warp"); + let read_node = make_node_id("read-node"); + let write_node = make_node_id("write-node"); + let read_edge = make_edge_id("read-edge"); + let write_edge = make_edge_id("write-edge"); + + let mut footprint = Footprint::default(); + footprint.n_read.insert_with_warp(warp_id, read_node); + footprint.n_write.insert_with_warp(warp_id, write_node); + footprint.e_read.insert_with_warp(warp_id, read_edge); + footprint.e_write.insert_with_warp(warp_id, write_edge); + footprint.a_read.insert(AttachmentKey::node_alpha(NodeKey { + warp_id, + local_id: read_node, + })); + footprint.a_write.insert(AttachmentKey::edge_beta(EdgeKey { + warp_id, + local_id: write_edge, + })); + footprint + .b_in + .insert(warp_id, pack_port_key(&read_node, 0, true)); + footprint + .b_out + .insert(warp_id, pack_port_key(&write_node, 0, false)); + footprint.factor_mask = 0b1010; + + let site = BoundedSite::from_footprint(&footprint); + + assert_eq!(site.claim_footprint, footprint); + assert_eq!(site.affected_region.nodes, footprint.n_write); + assert_eq!(site.affected_region.edges, footprint.e_write); + assert_eq!(site.affected_region.attachments, footprint.a_write); + assert_eq!(site.reintegration_boundary.inputs, footprint.b_in); + assert_eq!(site.reintegration_boundary.outputs, footprint.b_out); + } + + #[test] + fn plural_artifact_requires_parallel_participants_and_claims() { + let site = BoundedSite::default(); + let artifact = PluralArtifact::new(site, vec!["lane-a", "lane-b"], vec![1, 2]); + + assert_eq!(artifact.participants, vec!["lane-a", "lane-b"]); + assert_eq!(artifact.claims, vec![1, 2]); + } + + #[test] + fn admission_outcome_reports_top_level_kind() { + let derived: AdmissionOutcome = AdmissionOutcome::Derived(7); + let plural: AdmissionOutcome = + AdmissionOutcome::Plural(Box::new(PluralArtifact::new( + BoundedSite::default(), + vec!["lane-a", "lane-b"], + vec![1u8, 2u8], + ))); + let conflict: AdmissionOutcome = + AdmissionOutcome::Conflict("conflict"); + let obstruction: AdmissionOutcome = + AdmissionOutcome::Obstruction("blocked"); + + assert_eq!(derived.kind(), AdmissionOutcomeKind::Derived); + assert_eq!(plural.kind(), AdmissionOutcomeKind::Plural); + assert_eq!(conflict.kind(), AdmissionOutcomeKind::Conflict); + assert_eq!(obstruction.kind(), AdmissionOutcomeKind::Obstruction); + } +} diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index f119bb18..a13f0685 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -19,8 +19,9 @@ use crate::head_inbox::{InboxAddress, InboxIngestResult, IngressEnvelope, Ingres use crate::ident::Hash; use crate::provenance_store::{ HistoryError, ProvenanceCheckpoint, ProvenanceEntry, ProvenanceService, ProvenanceStore, + ReplayError, }; -use crate::strand::{Strand, StrandError, StrandId, StrandRegistry, SupportPin}; +use crate::strand::{ForkBasisRef, Strand, StrandError, StrandId, StrandRegistry, SupportPin}; use crate::worldline::WorldlineId; use crate::worldline_registry::WorldlineRegistry; use crate::worldline_state::{WorldlineFrontier, WorldlineState}; @@ -75,6 +76,9 @@ pub enum RuntimeError { /// Provenance append or lookup failed during a runtime step. #[error(transparent)] Provenance(#[from] HistoryError), + /// Historical replay or materialization failed. + #[error(transparent)] + Replay(#[from] ReplayError), /// Strand registry or support-pin operation failed. #[error(transparent)] Strand(#[from] StrandError), @@ -105,6 +109,34 @@ pub enum IngressDisposition { }, } +/// Request to fork a strand from one precise source-lane coordinate. +#[derive(Clone, Debug)] +pub struct ForkStrandRequest { + /// Stable strand identity to register. + pub strand_id: StrandId, + /// Source lane carrying the fork basis in v1. + pub source_lane_id: WorldlineId, + /// Last included tick in the copied prefix. + pub fork_tick: WorldlineTick, + /// Child worldline that will carry the speculative frontier. + pub child_worldline_id: WorldlineId, + /// Writer heads to register for the child worldline. + pub writer_heads: Vec, +} + +/// Receipt returned after a strand fork succeeds. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ForkStrandReceipt { + /// Stable strand identity. + pub strand_id: StrandId, + /// Immutable fork basis recorded for the new strand. + pub fork_basis_ref: ForkBasisRef, + /// Child worldline carrying the strand's live state. + pub child_worldline_id: WorldlineId, + /// Writer heads authorized for the child worldline. + pub writer_heads: Vec, +} + // ============================================================================= // WorldlineRuntime // ============================================================================= @@ -256,10 +288,10 @@ impl WorldlineRuntime { pub fn register_strand(&mut self, strand: Strand) -> Result<(), RuntimeError> { if !self .worldlines - .contains(&strand.base_ref.source_worldline_id) + .contains(&strand.fork_basis_ref.source_lane_id) { return Err(RuntimeError::UnknownWorldline( - strand.base_ref.source_worldline_id, + strand.fork_basis_ref.source_lane_id, )); } if !self.worldlines.contains(&strand.child_worldline_id) { @@ -273,6 +305,101 @@ impl WorldlineRuntime { self.strands.insert(strand).map_err(RuntimeError::Strand) } + /// Forks a new strand from one precise source-lane coordinate. + /// + /// This copies provenance history, materializes the child frontier at the + /// requested fork basis, registers the child worldline and writer heads, + /// then inserts the strand relation object. + /// + /// On failure, both runtime and provenance are restored to their pre-fork + /// state so forking does not leave partial truth behind. + /// + /// # Errors + /// + /// Returns an error if the source lane is missing, provenance cannot fork + /// or replay the child history, writer-head registration fails, or strand + /// invariants are violated. + pub fn fork_strand( + &mut self, + provenance: &mut ProvenanceService, + request: ForkStrandRequest, + ) -> Result { + let runtime_before = self.clone(); + let provenance_before = provenance.clone(); + + let outcome = + (|| { + let source_state = self + .worldlines + .get(&request.source_lane_id) + .ok_or(RuntimeError::UnknownWorldline(request.source_lane_id))? + .state() + .clone(); + let historical_source_state = provenance.replay_worldline_state_at( + request.source_lane_id, + &source_state, + request.fork_tick, + )?; + + provenance.fork( + request.source_lane_id, + request.fork_tick, + request.child_worldline_id, + )?; + + let child_target_tick = request.fork_tick.checked_increment().ok_or( + RuntimeError::FrontierTickOverflow(request.child_worldline_id), + )?; + let child_state = provenance.replay_worldline_state_at( + request.child_worldline_id, + &historical_source_state, + child_target_tick, + )?; + + let source_entry = provenance.entry(request.source_lane_id, request.fork_tick)?; + let fork_basis_ref = ForkBasisRef { + source_lane_id: request.source_lane_id, + fork_tick: request.fork_tick, + commit_hash: source_entry.expected.commit_hash, + boundary_hash: source_entry.expected.state_root, + provenance_ref: source_entry.as_ref(), + }; + let writer_heads = request + .writer_heads + .iter() + .map(|head| *head.key()) + .collect::>(); + + self.register_worldline(request.child_worldline_id, child_state)?; + for head in request.writer_heads { + self.register_writer_head(head)?; + } + self.register_strand(Strand { + strand_id: request.strand_id, + fork_basis_ref, + child_worldline_id: request.child_worldline_id, + writer_heads: writer_heads.clone(), + support_pins: Vec::new(), + })?; + + Ok(ForkStrandReceipt { + strand_id: request.strand_id, + fork_basis_ref, + child_worldline_id: request.child_worldline_id, + writer_heads, + }) + })(); + + match outcome { + Ok(receipt) => Ok(receipt), + Err(err) => { + *self = runtime_before; + *provenance = provenance_before; + Err(err) + } + } + } + /// Adds a validated support pin between two live strands. /// /// # Errors @@ -476,6 +603,12 @@ impl SchedulerCoordinator { /// Executes one SuperTick: admits inbox work in canonical head order and /// commits each non-empty head against its worldline frontier. /// + /// In the admission-law vocabulary, `super_tick()` is the single place + /// where Echo judges ingress claims at bounded sites under engine-defined + /// deterministic policy. The current runtime still derives bounded sites + /// from rule footprints and commits policy identity through the emitted + /// patch/shell family, but it does not introduce a second execution model. + /// /// The SuperTick is failure-atomic with respect to runtime state: if any /// head commit fails, all prior runtime and provenance mutations from this /// pass are discarded and both subsystems are restored to their @@ -656,6 +789,7 @@ mod tests { use crate::head_inbox::{make_intent_kind, InboxPolicy}; use crate::playback::PlaybackMode; use crate::rule::{ConflictPolicy, PatternGraph, RewriteRule}; + use crate::strand::make_strand_id; use crate::worldline::WorldlineId; use crate::{ make_node_id, make_type_id, EngineBuilder, GraphStore, GraphView, NodeId, NodeRecord, @@ -728,6 +862,23 @@ mod tests { provenance } + fn commit_one_tick( + runtime: &mut WorldlineRuntime, + provenance: &mut ProvenanceService, + engine: &mut Engine, + worldline_id: WorldlineId, + label: &str, + ) { + runtime + .ingest(IngressEnvelope::local_intent( + IngressTarget::DefaultWriter { worldline_id }, + make_intent_kind(label), + label.as_bytes().to_vec(), + )) + .unwrap(); + SchedulerCoordinator::super_tick(runtime, provenance, engine).unwrap(); + } + fn runtime_marker_matches(view: GraphView<'_>, scope: &NodeId) -> bool { matches!( view.node_attachment(scope), @@ -742,6 +893,220 @@ mod tests { ) } + #[test] + fn fork_strand_registers_child_frontier_and_strand_relation() { + let mut runtime = WorldlineRuntime::new(); + let mut engine = empty_engine(); + let source_lane_id = wl(1); + let child_worldline_id = wl(2); + let strand_id = make_strand_id("fork-runtime"); + + runtime + .register_worldline(source_lane_id, WorldlineState::empty()) + .unwrap(); + register_head( + &mut runtime, + source_lane_id, + "source-default", + None, + true, + InboxPolicy::AcceptAll, + ); + + let mut provenance = mirrored_provenance(&runtime); + commit_one_tick( + &mut runtime, + &mut provenance, + &mut engine, + source_lane_id, + "fork-source-commit", + ); + + let child_head_key = WriterHeadKey { + worldline_id: child_worldline_id, + head_id: make_head_id("child-default"), + }; + let receipt = runtime + .fork_strand( + &mut provenance, + ForkStrandRequest { + strand_id, + source_lane_id, + fork_tick: wt(0), + child_worldline_id, + writer_heads: vec![WriterHead::with_routing( + child_head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )], + }, + ) + .unwrap(); + + let child_frontier = runtime.worldlines().get(&child_worldline_id).unwrap(); + assert_eq!(child_frontier.frontier_tick(), wt(1)); + assert_eq!(child_frontier.state().current_tick(), wt(1)); + assert_eq!( + child_frontier.state().state_root(), + runtime + .worldlines() + .get(&source_lane_id) + .unwrap() + .state() + .state_root() + ); + + let strand = runtime.strands().get(&strand_id).unwrap(); + assert_eq!(strand.fork_basis_ref, receipt.fork_basis_ref); + assert_eq!(receipt.fork_basis_ref.source_lane_id, source_lane_id); + assert_eq!(receipt.fork_basis_ref.fork_tick, wt(0)); + assert_eq!(receipt.child_worldline_id, child_worldline_id); + assert_eq!(receipt.writer_heads, vec![child_head_key]); + assert!(runtime.heads().get(&child_head_key).is_some()); + assert_eq!(provenance.len(child_worldline_id).unwrap(), 1); + } + + #[test] + fn fork_strand_from_non_tip_tick_materializes_historical_basis() { + let mut runtime = WorldlineRuntime::new(); + let mut engine = empty_engine(); + let source_lane_id = wl(1); + let child_worldline_id = wl(2); + let strand_id = make_strand_id("fork-non-tip"); + + runtime + .register_worldline(source_lane_id, WorldlineState::empty()) + .unwrap(); + register_head( + &mut runtime, + source_lane_id, + "source-default", + None, + true, + InboxPolicy::AcceptAll, + ); + + let mut provenance = mirrored_provenance(&runtime); + commit_one_tick( + &mut runtime, + &mut provenance, + &mut engine, + source_lane_id, + "fork-source-commit-a", + ); + let historical_state = provenance + .replay_worldline_state_at( + source_lane_id, + runtime.worldlines().get(&source_lane_id).unwrap().state(), + wt(1), + ) + .unwrap(); + commit_one_tick( + &mut runtime, + &mut provenance, + &mut engine, + source_lane_id, + "fork-source-commit-b", + ); + + let child_head_key = WriterHeadKey { + worldline_id: child_worldline_id, + head_id: make_head_id("child-default"), + }; + runtime + .fork_strand( + &mut provenance, + ForkStrandRequest { + strand_id, + source_lane_id, + fork_tick: wt(0), + child_worldline_id, + writer_heads: vec![WriterHead::with_routing( + child_head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )], + }, + ) + .unwrap(); + + let child_frontier = runtime.worldlines().get(&child_worldline_id).unwrap(); + assert_eq!(child_frontier.frontier_tick(), wt(1)); + assert_eq!(child_frontier.state().current_tick(), wt(1)); + assert_eq!( + child_frontier.state().state_root(), + historical_state.state_root() + ); + assert_eq!(provenance.len(child_worldline_id).unwrap(), 1); + } + + #[test] + fn fork_strand_rolls_back_runtime_and_provenance_on_error() { + let mut runtime = WorldlineRuntime::new(); + let mut engine = empty_engine(); + let source_lane_id = wl(1); + let child_worldline_id = wl(2); + let strand_id = make_strand_id("fork-rollback"); + + runtime + .register_worldline(source_lane_id, WorldlineState::empty()) + .unwrap(); + register_head( + &mut runtime, + source_lane_id, + "source-default", + None, + true, + InboxPolicy::AcceptAll, + ); + + let mut provenance = mirrored_provenance(&runtime); + commit_one_tick( + &mut runtime, + &mut provenance, + &mut engine, + source_lane_id, + "fork-rollback-source", + ); + + let wrong_head_key = WriterHeadKey { + worldline_id: source_lane_id, + head_id: make_head_id("wrong-worldline"), + }; + let err = runtime + .fork_strand( + &mut provenance, + ForkStrandRequest { + strand_id, + source_lane_id, + fork_tick: wt(0), + child_worldline_id, + writer_heads: vec![WriterHead::with_routing( + wrong_head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + Some(InboxAddress("wrong-worldline".to_owned())), + false, + )], + }, + ) + .unwrap_err(); + + assert!(matches!( + err, + RuntimeError::Strand(StrandError::InvariantViolation(_)) + )); + assert!(runtime.worldlines().get(&child_worldline_id).is_none()); + assert!(runtime.strands().get(&strand_id).is_none()); + assert!(runtime.heads().get(&wrong_head_key).is_none()); + assert!(provenance.len(child_worldline_id).is_err()); + assert_eq!(provenance.len(source_lane_id).unwrap(), 1); + } + fn noop_runtime_rule(rule_name: &'static str) -> RewriteRule { RewriteRule { id: [1; 32], diff --git a/crates/warp-core/src/dynamic_binding.rs b/crates/warp-core/src/dynamic_binding.rs new file mode 100644 index 00000000..bb687a40 --- /dev/null +++ b/crates/warp-core/src/dynamic_binding.rs @@ -0,0 +1,1109 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime nouns for Wesley-authored structured footprint binding. +//! +//! Wesley owns the static schema for: +//! +//! - slots +//! - relation bindings +//! - closure operators +//! - create/update surfaces +//! - forbidden surfaces +//! +//! Echo owns the runtime truth of binding those declarations to concrete graph +//! entities. This module is the first explicit home for that runtime binding +//! vocabulary: direct slots, relation-derived slots, and resolved closures. + +use std::collections::BTreeMap; + +use crate::ident::{NodeId, NodeKey, WarpId}; + +/// Concrete graph-native reference bound to one structured slot or closure item. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BoundNodeRef { + /// Authored graph kind name from the Wesley contract. + pub kind: String, + /// Concrete graph node key resolved by Echo. + pub node: NodeKey, +} + +impl BoundNodeRef { + /// Constructs a bound node reference from its authored kind and resolved node key. + #[must_use] + pub fn new(kind: impl Into, node: NodeKey) -> Self { + Self { + kind: kind.into(), + node, + } + } + + /// Convenience constructor for separate warp/node ids. + #[must_use] + pub fn from_ids(kind: impl Into, warp_id: WarpId, node_id: NodeId) -> Self { + Self::new( + kind, + NodeKey { + warp_id, + local_id: node_id, + }, + ) + } +} + +/// Direct slot bound from explicit invocation arguments. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirectSlotBinding { + /// Authored slot name from the Wesley footprint contract. + pub slot: String, + /// Resolved graph-native node reference for that slot. + pub binding: BoundNodeRef, +} + +/// Slot derived from another bound slot through one declared relation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RelationSlotBinding { + /// Authored slot name from the Wesley footprint contract. + pub slot: String, + /// Upstream slot from which the relation was resolved. + pub from_slot: String, + /// Declared relation operator used for the bind. + pub relation: String, + /// Resolved graph-native node reference for the derived slot. + pub binding: BoundNodeRef, +} + +/// One resolved member of a declared closure. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ClosureMemberBinding { + /// Authored graph kind name of this member. + pub kind: String, + /// Resolved graph-native node reference for the member. + pub node: NodeKey, +} + +impl ClosureMemberBinding { + /// Constructs one closure member binding. + #[must_use] + pub fn new(kind: impl Into, node: NodeKey) -> Self { + Self { + kind: kind.into(), + node, + } + } +} + +/// Resolved closure over runtime graph truth. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedClosureBinding { + /// Authored closure slot name from the Wesley footprint contract. + pub slot: String, + /// Upstream slot from which the closure was derived. + pub from_slot: String, + /// Declared closure operator used to derive the members. + pub operator: String, + /// Resolved closure members in runtime order. + pub members: Vec, +} + +/// Resolved slot binding, either direct from args or relation-derived. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolvedSlotBinding { + /// Slot bound directly from invocation arguments. + Direct(DirectSlotBinding), + /// Slot bound by following one declared relation from another bound slot. + Relation(RelationSlotBinding), +} + +impl ResolvedSlotBinding { + /// Returns the authored slot name. + #[must_use] + pub fn slot(&self) -> &str { + match self { + Self::Direct(binding) => &binding.slot, + Self::Relation(binding) => &binding.slot, + } + } + + /// Returns the resolved graph-native binding. + #[must_use] + pub fn binding(&self) -> &BoundNodeRef { + match self { + Self::Direct(binding) => &binding.binding, + Self::Relation(binding) => &binding.binding, + } + } +} + +/// Runtime binding failures for structured footprints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DynamicBindingError { + /// Attempted to bind the same slot twice. + DuplicateSlot { + /// Authored slot name that was bound more than once. + slot: String, + }, + /// Attempted to resolve the same closure slot twice. + DuplicateClosure { + /// Authored closure slot name that was resolved more than once. + slot: String, + }, +} + +/// Runtime failures while resolving concrete structured bindings. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DynamicBindingRuntimeError { + /// A direct slot could not bind because the referenced target does not exist. + MissingDirectSlotTarget { + /// Authored slot name that failed to resolve. + slot: String, + /// Invocation-supplied id or label used for the lookup. + reference: String, + }, + /// A relation-derived slot could not bind because its source slot was absent. + MissingRelationSource { + /// Authored slot name that failed to resolve. + slot: String, + /// Upstream authored slot name that should have been bound first. + from_slot: String, + }, + /// A relation-derived slot could not bind because the declared relation had no target. + MissingRelationTarget { + /// Authored slot name that failed to resolve. + slot: String, + /// Upstream authored slot name used for the relation. + from_slot: String, + /// Declared relation that produced no target. + relation: String, + }, + /// A closure source slot was absent at binding time. + MissingClosureSource { + /// Authored closure slot name that failed to resolve. + slot: String, + /// Upstream authored slot name that should have been bound first. + from_slot: String, + }, + /// A declared closure operator was not recognized by the runtime resolver. + UnknownClosureOperator { + /// Authored closure slot name that failed to resolve. + slot: String, + /// Closure operator name that the runtime did not recognize. + operator: String, + }, + /// A closure range request was invalid for the resolved basis head. + InvalidClosureRange { + /// Authored closure slot name that failed to resolve. + slot: String, + /// Requested start byte. + start: usize, + /// Requested end byte. + end: usize, + /// Available byte length on the basis head. + limit: usize, + }, + /// A base head did not belong to the requested worldline. + BasisHeadMismatch { + /// Requested worldline binding. + worldline: String, + /// Requested basis head binding. + head: String, + }, + /// Lower-level binding insertion failed after resolution. + BindingCollision(DynamicBindingError), +} + +impl From for DynamicBindingRuntimeError { + fn from(value: DynamicBindingError) -> Self { + Self::BindingCollision(value) + } +} + +/// Request for resolving one declared range-scoped closure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RangeClosureBindingRequest<'a> { + /// Authored closure slot name. + pub slot: &'a str, + /// Authored source slot from which the closure is derived. + pub from_slot: &'a str, + /// Declared closure operator. + pub operator: &'a str, + /// Optional second authored slot needed by the operator. + pub related_slot: Option<&'a str>, + /// Requested range start. + pub start: usize, + /// Requested range end. + pub end: usize, +} + +/// Runtime adapter that resolves structured footprint bindings against graph truth. +pub trait StructuredBindingRuntime { + /// Resolves one direct slot from invocation-supplied reference material. + fn resolve_direct_slot( + &self, + slot: &str, + kind: &str, + reference: &str, + ) -> Result; + + /// Resolves one relation-derived slot from an already bound source slot. + fn resolve_relation_slot( + &self, + slot: &str, + from_slot: &str, + relation: &str, + source: &BoundNodeRef, + expected_kind: &str, + ) -> Result; + + /// Resolves one range-scoped closure from an already bound source slot. + fn resolve_range_closure( + &self, + request: &RangeClosureBindingRequest<'_>, + source: &BoundNodeRef, + related: Option<&BoundNodeRef>, + ) -> Result, DynamicBindingRuntimeError>; +} + +/// Internal resolver that accumulates structured runtime bindings. +#[derive(Debug)] +pub struct StructuredBindingResolver<'a, R> { + runtime: &'a R, + bindings: StructuredRuntimeBindings, +} + +impl<'a, R> StructuredBindingResolver<'a, R> +where + R: StructuredBindingRuntime, +{ + /// Creates a new resolver over `runtime`. + #[must_use] + pub fn new(runtime: &'a R) -> Self { + Self { + runtime, + bindings: StructuredRuntimeBindings::new(), + } + } + + /// Returns the currently accumulated runtime bindings. + #[must_use] + pub fn bindings(&self) -> &StructuredRuntimeBindings { + &self.bindings + } + + /// Consumes the resolver and returns the accumulated runtime bindings. + #[must_use] + pub fn into_bindings(self) -> StructuredRuntimeBindings { + self.bindings + } + + /// Resolves and records one direct slot from invocation input. + pub fn bind_direct_slot( + &mut self, + slot: &str, + kind: &str, + reference: &str, + ) -> Result<(), DynamicBindingRuntimeError> { + let binding = self.runtime.resolve_direct_slot(slot, kind, reference)?; + self.bindings.bind_direct_slot(slot, binding)?; + Ok(()) + } + + /// Resolves and records one relation-derived slot from an existing binding. + pub fn bind_relation_slot( + &mut self, + slot: &str, + kind: &str, + from_slot: &str, + relation: &str, + ) -> Result<(), DynamicBindingRuntimeError> { + let source = self.bindings.slot(from_slot).ok_or_else(|| { + DynamicBindingRuntimeError::MissingRelationSource { + slot: slot.to_owned(), + from_slot: from_slot.to_owned(), + } + })?; + let binding = self.runtime.resolve_relation_slot( + slot, + from_slot, + relation, + source.binding(), + kind, + )?; + self.bindings + .bind_relation_slot(slot, from_slot, relation, binding)?; + Ok(()) + } + + /// Resolves and records one declared range-scoped closure. + pub fn bind_range_closure( + &mut self, + request: RangeClosureBindingRequest<'_>, + ) -> Result<(), DynamicBindingRuntimeError> { + let source = self.bindings.slot(request.from_slot).ok_or_else(|| { + DynamicBindingRuntimeError::MissingClosureSource { + slot: request.slot.to_owned(), + from_slot: request.from_slot.to_owned(), + } + })?; + let related = request + .related_slot + .map(|slot_name| { + self.bindings.slot(slot_name).ok_or_else(|| { + DynamicBindingRuntimeError::MissingClosureSource { + slot: request.slot.to_owned(), + from_slot: slot_name.to_owned(), + } + }) + }) + .transpose()?; + let mut members = self.runtime.resolve_range_closure( + &request, + source.binding(), + related.map(ResolvedSlotBinding::binding), + )?; + members.sort(); + self.bindings + .bind_closure(request.slot, request.from_slot, request.operator, members)?; + Ok(()) + } +} + +/// Concrete runtime bindings for one structured footprint invocation. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct StructuredRuntimeBindings { + slots: BTreeMap, + closures: BTreeMap, +} + +impl StructuredRuntimeBindings { + /// Creates an empty runtime binding set. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Binds one direct slot from invocation arguments. + pub fn bind_direct_slot( + &mut self, + slot: impl Into, + binding: BoundNodeRef, + ) -> Result<(), DynamicBindingError> { + let slot = slot.into(); + match self.slots.entry(slot.clone()) { + std::collections::btree_map::Entry::Occupied(_) => { + Err(DynamicBindingError::DuplicateSlot { slot }) + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(ResolvedSlotBinding::Direct(DirectSlotBinding { + slot, + binding, + })); + Ok(()) + } + } + } + + /// Binds one relation-derived slot from an existing authored slot. + pub fn bind_relation_slot( + &mut self, + slot: impl Into, + from_slot: impl Into, + relation: impl Into, + binding: BoundNodeRef, + ) -> Result<(), DynamicBindingError> { + let slot = slot.into(); + match self.slots.entry(slot.clone()) { + std::collections::btree_map::Entry::Occupied(_) => { + Err(DynamicBindingError::DuplicateSlot { slot }) + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(ResolvedSlotBinding::Relation(RelationSlotBinding { + slot, + from_slot: from_slot.into(), + relation: relation.into(), + binding, + })); + Ok(()) + } + } + } + + /// Records a resolved closure against its authored slot name. + pub fn bind_closure( + &mut self, + slot: impl Into, + from_slot: impl Into, + operator: impl Into, + members: Vec, + ) -> Result<(), DynamicBindingError> { + let slot = slot.into(); + match self.closures.entry(slot.clone()) { + std::collections::btree_map::Entry::Occupied(_) => { + Err(DynamicBindingError::DuplicateClosure { slot }) + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(ResolvedClosureBinding { + slot, + from_slot: from_slot.into(), + operator: operator.into(), + members, + }); + Ok(()) + } + } + } + + /// Returns one resolved slot by authored name. + #[must_use] + pub fn slot(&self, slot: &str) -> Option<&ResolvedSlotBinding> { + self.slots.get(slot) + } + + /// Returns one resolved closure by authored name. + #[must_use] + pub fn closure(&self, slot: &str) -> Option<&ResolvedClosureBinding> { + self.closures.get(slot) + } + + /// Returns all resolved slots in deterministic authored-name order. + pub fn slots(&self) -> impl Iterator { + self.slots.values() + } + + /// Returns all resolved closures in deterministic authored-name order. + pub fn closures(&self) -> impl Iterator { + self.closures.values() + } +} + +#[cfg(test)] +#[allow( + clippy::expect_used, + clippy::panic, + clippy::unwrap_used, + reason = "dynamic binding tests use direct assertion-style extraction" +)] +mod tests { + use super::*; + use crate::ident::{make_node_id, make_warp_id}; + use std::collections::BTreeMap; + + #[derive(Debug, Clone)] + struct MockWorldline { + id: String, + binding: BoundNodeRef, + canonical_head_id: Option, + } + + #[derive(Debug, Clone)] + struct MockHead { + id: String, + binding: BoundNodeRef, + worldline_id: String, + byte_length: usize, + rope_members: Vec, + } + + #[derive(Debug, Clone)] + struct MockRopeMember { + start: usize, + end: usize, + binding: ClosureMemberBinding, + } + + #[derive(Debug, Clone)] + struct MockAnchor { + basis_head_id: String, + start: usize, + end: usize, + binding: ClosureMemberBinding, + } + + #[derive(Debug, Clone)] + struct MockTextRuntime { + worldlines: BTreeMap, + heads: BTreeMap, + anchors: Vec, + } + + #[derive(Debug, Clone)] + struct ReplaceRangeAsTickBindingRequest { + worldline_id: String, + base_head_id: String, + start_byte: usize, + end_byte: usize, + } + + #[derive(Debug, Clone)] + struct CreateCheckpointBindingRequest { + worldline_id: String, + } + + fn overlap(start: usize, end: usize, other_start: usize, other_end: usize) -> bool { + start < other_end && other_start < end + } + + impl StructuredBindingRuntime for MockTextRuntime { + fn resolve_direct_slot( + &self, + slot: &str, + _kind: &str, + reference: &str, + ) -> Result { + match slot { + "worldline" => self + .worldlines + .get(reference) + .map(|worldline| worldline.binding.clone()) + .ok_or_else(|| DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: slot.to_owned(), + reference: reference.to_owned(), + }), + "baseHead" => self + .heads + .get(reference) + .map(|head| head.binding.clone()) + .ok_or_else(|| DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: slot.to_owned(), + reference: reference.to_owned(), + }), + _ => Err(DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: slot.to_owned(), + reference: reference.to_owned(), + }), + } + } + + fn resolve_relation_slot( + &self, + slot: &str, + from_slot: &str, + relation: &str, + source: &BoundNodeRef, + _expected_kind: &str, + ) -> Result { + if from_slot != "worldline" || relation != "CANONICAL_HEAD" { + return Err(DynamicBindingRuntimeError::MissingRelationTarget { + slot: slot.to_owned(), + from_slot: from_slot.to_owned(), + relation: relation.to_owned(), + }); + } + + let worldline = self + .worldlines + .values() + .find(|worldline| worldline.binding == *source) + .ok_or_else(|| DynamicBindingRuntimeError::MissingRelationTarget { + slot: slot.to_owned(), + from_slot: from_slot.to_owned(), + relation: relation.to_owned(), + })?; + + let canonical_head_id = worldline.canonical_head_id.clone().ok_or_else(|| { + DynamicBindingRuntimeError::MissingRelationTarget { + slot: slot.to_owned(), + from_slot: from_slot.to_owned(), + relation: relation.to_owned(), + } + })?; + + self.heads + .get(&canonical_head_id) + .map(|head| head.binding.clone()) + .ok_or_else(|| DynamicBindingRuntimeError::MissingRelationTarget { + slot: slot.to_owned(), + from_slot: from_slot.to_owned(), + relation: relation.to_owned(), + }) + } + + fn resolve_range_closure( + &self, + request: &RangeClosureBindingRequest<'_>, + source: &BoundNodeRef, + related: Option<&BoundNodeRef>, + ) -> Result, DynamicBindingRuntimeError> { + match request.operator { + "ropeRangeClosure" => { + let base_head = self + .heads + .values() + .find(|head| head.binding == *source) + .ok_or_else(|| DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: request.slot.to_owned(), + reference: source.kind.clone(), + })?; + + if request.start > request.end { + return Err(DynamicBindingRuntimeError::InvalidClosureRange { + slot: request.slot.to_owned(), + start: request.start, + end: request.end, + limit: base_head.byte_length, + }); + } + if request.end > base_head.byte_length { + return Err(DynamicBindingRuntimeError::InvalidClosureRange { + slot: request.slot.to_owned(), + start: request.start, + end: request.end, + limit: base_head.byte_length, + }); + } + + Ok(base_head + .rope_members + .iter() + .filter(|member| { + overlap(request.start, request.end, member.start, member.end) + }) + .map(|member| member.binding.clone()) + .collect()) + } + "anchorsIntersectingEditWindow" => { + let worldline = self + .worldlines + .values() + .find(|worldline| worldline.binding == *source) + .ok_or_else(|| DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: request.slot.to_owned(), + reference: source.kind.clone(), + })?; + let related = related.ok_or_else(|| { + DynamicBindingRuntimeError::MissingClosureSource { + slot: request.slot.to_owned(), + from_slot: "baseHead".to_owned(), + } + })?; + let base_head = self + .heads + .values() + .find(|head| head.binding == *related) + .ok_or_else(|| DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: request.slot.to_owned(), + reference: related.kind.clone(), + })?; + + if base_head.worldline_id != worldline.id { + return Err(DynamicBindingRuntimeError::BasisHeadMismatch { + worldline: worldline.id.clone(), + head: base_head.id.clone(), + }); + } + + Ok(self + .anchors + .iter() + .filter(|anchor| { + anchor.basis_head_id == base_head.id + && overlap(request.start, request.end, anchor.start, anchor.end) + }) + .map(|anchor| anchor.binding.clone()) + .collect()) + } + _ => Err(DynamicBindingRuntimeError::UnknownClosureOperator { + slot: request.slot.to_owned(), + operator: request.operator.to_owned(), + }), + } + } + } + + fn bind_replace_range_as_tick( + runtime: &MockTextRuntime, + request: &ReplaceRangeAsTickBindingRequest, + ) -> Result { + let mut resolver = StructuredBindingResolver::new(runtime); + resolver.bind_direct_slot("worldline", "BufferWorldline", &request.worldline_id)?; + resolver.bind_direct_slot("baseHead", "RopeHead", &request.base_head_id)?; + resolver.bind_range_closure(RangeClosureBindingRequest { + slot: "touchedRope", + from_slot: "baseHead", + operator: "ropeRangeClosure", + related_slot: None, + start: request.start_byte, + end: request.end_byte, + })?; + resolver.bind_range_closure(RangeClosureBindingRequest { + slot: "affectedAnchors", + from_slot: "worldline", + operator: "anchorsIntersectingEditWindow", + related_slot: Some("baseHead"), + start: request.start_byte, + end: request.end_byte, + })?; + Ok(resolver.into_bindings()) + } + + fn bind_create_checkpoint( + runtime: &MockTextRuntime, + request: &CreateCheckpointBindingRequest, + ) -> Result { + let mut resolver = StructuredBindingResolver::new(runtime); + resolver.bind_direct_slot("worldline", "BufferWorldline", &request.worldline_id)?; + resolver.bind_relation_slot("currentHead", "RopeHead", "worldline", "CANONICAL_HEAD")?; + Ok(resolver.into_bindings()) + } + + fn mock_runtime() -> MockTextRuntime { + let warp_id = make_warp_id("binding-warp"); + let worldline_id = "wl:buf-1".to_owned(); + let canonical_head_id = "head:current".to_owned(); + let stale_head_id = "head:stale".to_owned(); + + let worldline = MockWorldline { + id: worldline_id.clone(), + binding: BoundNodeRef::from_ids( + "BufferWorldline", + warp_id, + make_node_id("buffer-worldline"), + ), + canonical_head_id: Some(canonical_head_id.clone()), + }; + let current_head = MockHead { + id: canonical_head_id.clone(), + binding: BoundNodeRef::from_ids("RopeHead", warp_id, make_node_id("head-current")), + worldline_id: worldline_id.clone(), + byte_length: 20, + rope_members: vec![ + MockRopeMember { + start: 0, + end: 5, + binding: ClosureMemberBinding::new( + "RopeLeaf", + NodeKey { + warp_id, + local_id: make_node_id("leaf-0-5"), + }, + ), + }, + MockRopeMember { + start: 5, + end: 10, + binding: ClosureMemberBinding::new( + "RopeBranch", + NodeKey { + warp_id, + local_id: make_node_id("branch-5-10"), + }, + ), + }, + MockRopeMember { + start: 10, + end: 15, + binding: ClosureMemberBinding::new( + "TextBlob", + NodeKey { + warp_id, + local_id: make_node_id("blob-10-15"), + }, + ), + }, + ], + }; + let stale_head = MockHead { + id: stale_head_id, + binding: BoundNodeRef::from_ids("RopeHead", warp_id, make_node_id("head-stale")), + worldline_id: "wl:other".to_owned(), + byte_length: 8, + rope_members: vec![], + }; + + MockTextRuntime { + worldlines: BTreeMap::from([(worldline_id, worldline)]), + heads: BTreeMap::from([ + (canonical_head_id.clone(), current_head), + ("head:stale".to_owned(), stale_head), + ]), + anchors: vec![ + MockAnchor { + basis_head_id: canonical_head_id.clone(), + start: 4, + end: 7, + binding: ClosureMemberBinding::new( + "Anchor", + NodeKey { + warp_id, + local_id: make_node_id("anchor-overlap"), + }, + ), + }, + MockAnchor { + basis_head_id: canonical_head_id, + start: 15, + end: 18, + binding: ClosureMemberBinding::new( + "Anchor", + NodeKey { + warp_id, + local_id: make_node_id("anchor-outside"), + }, + ), + }, + ], + } + } + + #[test] + fn structured_runtime_bindings_store_direct_relation_and_closure_bindings() { + let warp_id = make_warp_id("binding-warp"); + let worldline = + BoundNodeRef::from_ids("BufferWorldline", warp_id, make_node_id("worldline")); + let base_head = BoundNodeRef::from_ids("RopeHead", warp_id, make_node_id("base-head")); + let branch = ClosureMemberBinding::new( + "RopeBranch", + NodeKey { + warp_id, + local_id: make_node_id("branch"), + }, + ); + let leaf = ClosureMemberBinding::new( + "RopeLeaf", + NodeKey { + warp_id, + local_id: make_node_id("leaf"), + }, + ); + let blob = ClosureMemberBinding::new( + "TextBlob", + NodeKey { + warp_id, + local_id: make_node_id("blob"), + }, + ); + + let mut bindings = StructuredRuntimeBindings::new(); + bindings + .bind_direct_slot("worldline", worldline.clone()) + .expect("bind direct slot"); + bindings + .bind_relation_slot("baseHead", "worldline", "CANONICAL_HEAD", base_head.clone()) + .expect("bind relation slot"); + bindings + .bind_closure( + "touchedRope", + "baseHead", + "ropeRangeClosure", + vec![branch.clone(), leaf.clone(), blob.clone()], + ) + .expect("bind closure"); + + let worldline_binding = bindings.slot("worldline").expect("worldline slot"); + assert_eq!(worldline_binding.binding(), &worldline); + assert_eq!(worldline_binding.slot(), "worldline"); + + let base_head_binding = bindings.slot("baseHead").expect("baseHead slot"); + assert_eq!(base_head_binding.binding(), &base_head); + match base_head_binding { + ResolvedSlotBinding::Relation(binding) => { + assert_eq!(binding.from_slot, "worldline"); + assert_eq!(binding.relation, "CANONICAL_HEAD"); + } + ResolvedSlotBinding::Direct(_) => panic!("expected relation-derived slot"), + } + + let closure = bindings + .closure("touchedRope") + .expect("touchedRope closure"); + assert_eq!(closure.from_slot, "baseHead"); + assert_eq!(closure.operator, "ropeRangeClosure"); + assert_eq!(closure.members, vec![branch, leaf, blob]); + } + + #[test] + fn structured_runtime_bindings_reject_duplicate_slot_and_closure_names() { + let warp_id = make_warp_id("binding-warp"); + let mut bindings = StructuredRuntimeBindings::new(); + let original_worldline = + BoundNodeRef::from_ids("BufferWorldline", warp_id, make_node_id("worldline")); + + bindings + .bind_direct_slot("worldline", original_worldline.clone()) + .expect("initial direct slot bind"); + assert_eq!( + bindings.bind_relation_slot( + "worldline", + "worldline", + "CANONICAL_HEAD", + BoundNodeRef::from_ids("RopeHead", warp_id, make_node_id("head")), + ), + Err(DynamicBindingError::DuplicateSlot { + slot: "worldline".to_owned(), + }) + ); + assert_eq!( + bindings.slot("worldline").unwrap().binding(), + &original_worldline + ); + + let original_closure = vec![ClosureMemberBinding::new( + "RopeLeaf", + NodeKey { + warp_id, + local_id: make_node_id("leaf"), + }, + )]; + bindings + .bind_closure( + "touchedRope", + "worldline", + "ropeRangeClosure", + original_closure.clone(), + ) + .expect("initial closure bind"); + assert_eq!( + bindings.bind_closure("touchedRope", "worldline", "ropeRangeClosure", vec![]), + Err(DynamicBindingError::DuplicateClosure { + slot: "touchedRope".to_owned(), + }) + ); + assert_eq!( + bindings.closure("touchedRope").unwrap().members, + original_closure + ); + } + + #[test] + fn mock_replace_range_runtime_binds_direct_slots_and_resolves_declared_closures() { + let runtime = mock_runtime(); + let bindings = bind_replace_range_as_tick( + &runtime, + &ReplaceRangeAsTickBindingRequest { + worldline_id: "wl:buf-1".to_owned(), + base_head_id: "head:current".to_owned(), + start_byte: 4, + end_byte: 12, + }, + ) + .expect("bind replace-range runtime"); + + let worldline = bindings.slot("worldline").expect("worldline"); + assert_eq!(worldline.binding().kind, "BufferWorldline"); + + let base_head = bindings.slot("baseHead").expect("baseHead"); + assert_eq!(base_head.binding().kind, "RopeHead"); + + let touched_rope = bindings.closure("touchedRope").expect("touchedRope"); + assert_eq!(touched_rope.operator, "ropeRangeClosure"); + assert_eq!(touched_rope.members.len(), 3); + assert_eq!( + touched_rope + .members + .iter() + .map(|member| member.kind.as_str()) + .collect::>(), + vec!["RopeBranch", "RopeLeaf", "TextBlob"] + ); + + let affected_anchors = bindings + .closure("affectedAnchors") + .expect("affectedAnchors"); + assert_eq!(affected_anchors.operator, "anchorsIntersectingEditWindow"); + assert_eq!(affected_anchors.members.len(), 1); + assert_eq!(affected_anchors.members[0].kind, "Anchor"); + } + + #[test] + fn mock_checkpoint_runtime_binds_relation_derived_current_head() { + let runtime = mock_runtime(); + let bindings = bind_create_checkpoint( + &runtime, + &CreateCheckpointBindingRequest { + worldline_id: "wl:buf-1".to_owned(), + }, + ) + .expect("bind checkpoint"); + + let worldline = bindings.slot("worldline").expect("worldline"); + assert_eq!(worldline.binding().kind, "BufferWorldline"); + + let current_head = bindings.slot("currentHead").expect("currentHead"); + match current_head { + ResolvedSlotBinding::Relation(binding) => { + assert_eq!(binding.from_slot, "worldline"); + assert_eq!(binding.relation, "CANONICAL_HEAD"); + assert_eq!(binding.binding.kind, "RopeHead"); + } + ResolvedSlotBinding::Direct(_) => panic!("expected relation-derived currentHead"), + } + } + + #[test] + fn mock_runtime_reports_invalid_replace_range_bindings() { + let runtime = mock_runtime(); + + assert_eq!( + bind_replace_range_as_tick( + &runtime, + &ReplaceRangeAsTickBindingRequest { + worldline_id: "wl:missing".to_owned(), + base_head_id: "head:current".to_owned(), + start_byte: 0, + end_byte: 1, + }, + ), + Err(DynamicBindingRuntimeError::MissingDirectSlotTarget { + slot: "worldline".to_owned(), + reference: "wl:missing".to_owned(), + }) + ); + + assert_eq!( + bind_replace_range_as_tick( + &runtime, + &ReplaceRangeAsTickBindingRequest { + worldline_id: "wl:buf-1".to_owned(), + base_head_id: "head:stale".to_owned(), + start_byte: 0, + end_byte: 1, + }, + ), + Err(DynamicBindingRuntimeError::BasisHeadMismatch { + worldline: "wl:buf-1".to_owned(), + head: "head:stale".to_owned(), + }) + ); + + assert_eq!( + bind_replace_range_as_tick( + &runtime, + &ReplaceRangeAsTickBindingRequest { + worldline_id: "wl:buf-1".to_owned(), + base_head_id: "head:current".to_owned(), + start_byte: 12, + end_byte: 21, + }, + ), + Err(DynamicBindingRuntimeError::InvalidClosureRange { + slot: "touchedRope".to_owned(), + start: 12, + end: 21, + limit: 20, + }) + ); + } + + #[test] + fn mock_runtime_reports_missing_relation_derived_checkpoint_head() { + let mut runtime = mock_runtime(); + runtime + .worldlines + .get_mut("wl:buf-1") + .expect("worldline") + .canonical_head_id = None; + + assert_eq!( + bind_create_checkpoint( + &runtime, + &CreateCheckpointBindingRequest { + worldline_id: "wl:buf-1".to_owned(), + }, + ), + Err(DynamicBindingRuntimeError::MissingRelationTarget { + slot: "currentHead".to_owned(), + from_slot: "worldline".to_owned(), + relation: "CANONICAL_HEAD".to_owned(), + }) + ); + } +} diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 7c748dba..bd64042f 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -423,6 +423,7 @@ impl EngineBuilder { pub struct Engine { state: WarpState, rules: HashMap<&'static str, RewriteRule>, + #[cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] rules_by_id: HashMap, compact_rule_ids: HashMap, rules_by_compact: HashMap, @@ -1060,13 +1061,8 @@ impl Engine { }) } - /// Registers a rewrite rule so it can be referenced by name. - /// - /// # Errors - /// Returns [`EngineError::DuplicateRuleName`] if a rule with the same - /// name has already been registered, or [`EngineError::DuplicateRuleId`] - /// if a rule with the same id was previously registered. - pub fn register_rule(&mut self, rule: RewriteRule) -> Result<(), EngineError> { + #[cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] + fn register_rule_impl(&mut self, rule: RewriteRule) -> Result<(), EngineError> { if self.rules.contains_key(rule.name) { return Err(EngineError::DuplicateRuleName(rule.name)); } @@ -1100,6 +1096,29 @@ impl Engine { Ok(()) } + /// Registers a native rewrite rule so it can be referenced by name. + /// + /// This bootstrap surface exists only for Echo's internal fixtures, tests, + /// and transitional engine code. It is intentionally hidden behind the + /// `native_rule_bootstrap` feature so external Rust consumers do not gain a + /// supported handwritten rewrite-authoring API. + /// + /// # Errors + /// Returns [`EngineError::DuplicateRuleName`] if a rule with the same + /// name has already been registered, or [`EngineError::DuplicateRuleId`] + /// if a rule with the same id was previously registered. + #[cfg(feature = "native_rule_bootstrap")] + #[doc(hidden)] + pub fn register_rule(&mut self, rule: RewriteRule) -> Result<(), EngineError> { + self.register_rule_impl(rule) + } + + #[cfg(not(feature = "native_rule_bootstrap"))] + #[cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] + pub(crate) fn register_rule(&mut self, rule: RewriteRule) -> Result<(), EngineError> { + self.register_rule_impl(rule) + } + /// Begins a new transaction and returns its identifier. #[must_use] pub fn begin(&mut self) -> TxId { diff --git a/crates/warp-core/src/footprint.rs b/crates/warp-core/src/footprint.rs index 6b7e7e5e..9c6d8f77 100644 --- a/crates/warp-core/src/footprint.rs +++ b/crates/warp-core/src/footprint.rs @@ -81,7 +81,7 @@ fn intersects_btree(a: &BTreeSet, b: &BTreeSet) -> bool { /// /// Each entry is a `NodeKey` containing both `warp_id` and `local_id`, ensuring /// nodes in different warps don't cause false conflicts during scheduling. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct NodeSet(BTreeSet); impl NodeSet { @@ -123,7 +123,7 @@ impl NodeSet { /// /// Each entry is an `EdgeKey` containing both `warp_id` and `local_id`, ensuring /// edges in different warps don't cause false conflicts during scheduling. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct EdgeSet(BTreeSet); impl EdgeSet { @@ -165,7 +165,7 @@ impl EdgeSet { /// /// Each entry is a `(WarpId, PortKey)` tuple ensuring ports in different warps /// don't cause false conflicts during scheduling. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PortSet(BTreeSet); impl PortSet { @@ -212,7 +212,7 @@ impl PortSet { /// Ordered set of attachment slots. /// /// [`AttachmentKey`] is already warp-scoped (contains [`NodeKey`] or [`EdgeKey`]). -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct AttachmentSet(BTreeSet); impl AttachmentSet { @@ -251,7 +251,7 @@ impl AttachmentSet { /// All resource sets are warp-scoped to prevent false conflicts between /// rewrites in different warps that happen to touch resources with the /// same local identifier. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Footprint { /// Nodes read by the rewrite (warp-scoped). pub n_read: NodeSet, @@ -274,6 +274,16 @@ pub struct Footprint { } impl Footprint { + /// Derives the admission-side bounded site for this footprint. + /// + /// The first cut preserves the full declared claim and derives the site's + /// affected region and reintegration boundary directly from the existing + /// footprint model rather than introducing a competing geometry. + #[must_use] + pub fn bounded_site(&self) -> crate::admission::BoundedSite { + crate::admission::BoundedSite::from(self) + } + /// Returns `true` when this footprint is independent of `other`. /// /// Fast path checks the factor mask; then boundary ports; then edges and diff --git a/crates/warp-core/src/inbox.rs b/crates/warp-core/src/inbox.rs index 67d49d71..7d68efd9 100644 --- a/crates/warp-core/src/inbox.rs +++ b/crates/warp-core/src/inbox.rs @@ -18,6 +18,7 @@ //! This module provides: //! - `sys/dispatch_inbox`: drains all pending edges from the inbox (legacy helper) //! - `sys/ack_pending`: consumes exactly one pending edge for an event scope +#![cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] use blake3::Hasher; @@ -65,7 +66,19 @@ const PENDING_EDGE_HASH_DOMAIN: &[u8] = b"sim/inbox/pending:"; /// - Deletes all `edge:pending` edges from the inbox node. /// - Does NOT delete the event nodes (ledger entries remain). #[must_use] +#[cfg(feature = "native_rule_bootstrap")] +#[doc(hidden)] pub fn dispatch_inbox_rule() -> RewriteRule { + dispatch_inbox_rule_impl() +} + +#[must_use] +#[cfg(not(feature = "native_rule_bootstrap"))] +pub(crate) fn dispatch_inbox_rule() -> RewriteRule { + dispatch_inbox_rule_impl() +} + +fn dispatch_inbox_rule_impl() -> RewriteRule { let id: Hash = make_type_id("rule:sys/dispatch_inbox").0; RewriteRule { id, @@ -85,7 +98,19 @@ pub fn dispatch_inbox_rule() -> RewriteRule { /// This rule deletes the `edge:pending` edge corresponding to the provided /// `scope` event node, treating edge deletion as queue maintenance (not ledger deletion). #[must_use] +#[cfg(feature = "native_rule_bootstrap")] +#[doc(hidden)] pub fn ack_pending_rule() -> RewriteRule { + ack_pending_rule_impl() +} + +#[must_use] +#[cfg(not(feature = "native_rule_bootstrap"))] +pub(crate) fn ack_pending_rule() -> RewriteRule { + ack_pending_rule_impl() +} + +fn ack_pending_rule_impl() -> RewriteRule { let id: Hash = make_type_id("rule:sys/ack_pending").0; RewriteRule { id, diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 611e2507..83820c12 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -32,12 +32,14 @@ pub mod math; /// WSC (Write-Streaming Columnar) snapshot format for deterministic serialization. pub mod wsc; +mod admission; mod attachment; mod clock; mod cmd; mod constants; /// Domain separation prefixes for hashing. pub mod domain; +mod dynamic_binding; mod engine_impl; mod footprint; /// Footprint enforcement guard for parallel execution. @@ -154,6 +156,10 @@ mod worldline_registry; mod worldline_state; // Re-exports for stable public API +pub use admission::{ + AdmissionOutcome, AdmissionOutcomeKind, AdmissionPolicyRef, AffectedRegion, BoundedSite, + PluralArtifact, ReintegrationBoundary, +}; pub use attachment::{ AtomPayload, AttachmentKey, AttachmentOwner, AttachmentPlane, AttachmentValue, Codec, CodecRegistry, DecodeError, ErasedCodec, RegistryError, @@ -165,6 +171,12 @@ pub use cmd::{ IMPORT_SUFFIX_RESULT_EDGE_TYPE, IMPORT_SUFFIX_RESULT_NODE_TYPE, }; pub use constants::{blake3_empty, digest_len0_u64, POLICY_ID_NO_POLICY_V0}; +pub use dynamic_binding::{ + BoundNodeRef, ClosureMemberBinding, DirectSlotBinding, DynamicBindingError, + DynamicBindingRuntimeError, RangeClosureBindingRequest, RelationSlotBinding, + ResolvedClosureBinding, ResolvedSlotBinding, StructuredBindingResolver, + StructuredBindingRuntime, StructuredRuntimeBindings, +}; pub use engine_impl::{ scope_hash, ApplyResult, CommitOutcome, DispatchDisposition, Engine, EngineBuilder, EngineError, ExistingState, FreshStore, IngestDisposition, @@ -208,7 +220,8 @@ pub use playback::{ pub use playback::{SessionId, ViewSession}; // --- Truth delivery --- pub use neighborhood::{ - NeighborhoodError, NeighborhoodSite, NeighborhoodSiteId, NeighborhoodSiteService, + NeighborhoodCore, NeighborhoodError, NeighborhoodParticipant, NeighborhoodParticipantRole, + NeighborhoodPlurality, NeighborhoodSite, NeighborhoodSiteId, NeighborhoodSiteService, ParticipantRole, SiteParticipant, SitePlurality, }; pub use observation::{ @@ -241,8 +254,11 @@ pub use provenance_store::{ }; pub use receipt::{TickReceipt, TickReceiptDisposition, TickReceiptEntry, TickReceiptRejection}; pub use record::{EdgeRecord, NodeRecord}; +#[cfg(feature = "native_rule_bootstrap")] pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; -pub use sandbox::{build_engine, run_pair_determinism, DeterminismError, EchoConfig}; +pub use sandbox::DeterminismError; +#[cfg(feature = "native_rule_bootstrap")] +pub use sandbox::{build_engine, run_pair_determinism, EchoConfig}; pub use scheduler::SchedulerKind; #[cfg(feature = "serde")] pub use serializable::{ @@ -257,7 +273,7 @@ pub use snapshot::{ compute_state_root_for_warp_store, compute_tick_commit_hash_v2, OpEmissionEntry, Snapshot, }; pub use strand::{ - make_strand_id, BaseRef, DropReceipt, ParentMovementFootprint, Strand, StrandBasisReport, + make_strand_id, DropReceipt, ForkBasisRef, ParentMovementFootprint, Strand, StrandBasisReport, StrandDivergenceFootprint, StrandError, StrandId, StrandOverlapRevalidation, StrandRegistry, StrandRevalidationState, SupportPin, }; @@ -287,7 +303,8 @@ pub use worldline::{ /// /// Prefer this coordinator/runtime API for new stepping and routing code. pub use coordinator::{ - IngressDisposition, RuntimeError, SchedulerCoordinator, StepRecord, WorldlineRuntime, + ForkStrandReceipt, ForkStrandRequest, IngressDisposition, RuntimeError, SchedulerCoordinator, + StepRecord, WorldlineRuntime, }; /// Writer-head registry and routing primitives used by the runtime-owned ingress path. pub use head::{ diff --git a/crates/warp-core/src/neighborhood.rs b/crates/warp-core/src/neighborhood.rs index 4e4df329..a06315d5 100644 --- a/crates/warp-core/src/neighborhood.rs +++ b/crates/warp-core/src/neighborhood.rs @@ -6,11 +6,17 @@ //! observed local site. It is intentionally narrower than a full global braid //! model: it says which lanes participate in the current observed site, not //! every nearby alternative in the universe. +//! +//! This is a derived publication surface, not the authoritative admission-side +//! site noun. Admission in Echo is judged over a +//! [`BoundedSite`](crate::admission::BoundedSite); neighborhood publication is +//! a later observer-facing projection over nearby lane truth. use blake3::Hasher; use echo_wasm_abi::kernel_port as abi; use thiserror::Error; +use crate::admission::AdmissionOutcomeKind; use crate::clock::{GlobalTick, WorldlineTick}; use crate::coordinator::WorldlineRuntime; use crate::engine_impl::Engine; @@ -58,6 +64,15 @@ pub enum SitePlurality { } impl SitePlurality { + /// Maps the published site plurality into Echo's shared lawful outcome family. + #[must_use] + pub fn admission_outcome_kind(self) -> AdmissionOutcomeKind { + match self { + Self::Singleton => AdmissionOutcomeKind::Derived, + Self::Braided => AdmissionOutcomeKind::Plural, + } + } + fn to_abi(self) -> abi::SitePlurality { match self { Self::Singleton => abi::SitePlurality::Singleton, @@ -79,7 +94,7 @@ pub enum ParticipantRole { /// The lane being directly observed. Primary, /// The base coordinate from which the primary strand forked. - BaseAnchor, + BasisAnchor, /// A read-only support-pinned lane. Support, } @@ -88,18 +103,34 @@ impl ParticipantRole { fn to_abi(self) -> abi::ParticipantRole { match self { Self::Primary => abi::ParticipantRole::Primary, - Self::BaseAnchor => abi::ParticipantRole::BaseAnchor, + Self::BasisAnchor => abi::ParticipantRole::BaseAnchor, Self::Support => abi::ParticipantRole::Support, } } + fn to_core(self) -> NeighborhoodParticipantRole { + match self { + Self::Primary => NeighborhoodParticipantRole::Primary, + Self::BasisAnchor => NeighborhoodParticipantRole::BasisAnchor, + Self::Support => NeighborhoodParticipantRole::Support, + } + } + fn code(self) -> u8 { match self { Self::Primary => 1, - Self::BaseAnchor => 2, + Self::BasisAnchor => 2, Self::Support => 3, } } + + fn label(self) -> &'static str { + match self { + Self::Primary => "primary", + Self::BasisAnchor => "basis-anchor", + Self::Support => "support", + } + } } /// One lane participating in a published local site. @@ -131,6 +162,154 @@ impl SiteParticipant { } } +/// Shared observer/debugger plurality for one published neighborhood core. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NeighborhoodPlurality { + /// Only the primary lane participates. + Singleton, + /// Multiple lanes participate in the published local site. + Plural, +} + +impl SitePlurality { + fn to_core(self) -> NeighborhoodPlurality { + match self { + Self::Singleton => NeighborhoodPlurality::Singleton, + Self::Braided => NeighborhoodPlurality::Plural, + } + } +} + +impl NeighborhoodPlurality { + fn to_abi(self) -> abi::NeighborhoodPlurality { + match self { + Self::Singleton => abi::NeighborhoodPlurality::Singleton, + Self::Plural => abi::NeighborhoodPlurality::Plural, + } + } +} + +/// Shared observer/debugger role for one published neighborhood participant. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NeighborhoodParticipantRole { + /// The directly observed lane. + Primary, + /// The fork/source lane anchoring the primary strand. + BasisAnchor, + /// A read-only support lane. + Support, +} + +impl NeighborhoodParticipantRole { + fn to_abi(self) -> abi::NeighborhoodParticipantRole { + match self { + Self::Primary => abi::NeighborhoodParticipantRole::Primary, + Self::BasisAnchor => abi::NeighborhoodParticipantRole::BasisAnchor, + Self::Support => abi::NeighborhoodParticipantRole::Support, + } + } +} + +/// Shared observer/debugger participant for one published neighborhood core. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct NeighborhoodParticipant { + /// Stable participant identity within the published site. + pub participant_id: String, + /// Stable lane identity for the participant's worldline carrier. + pub lane_id: String, + /// Optional strand identity when the participant is a strand-backed lane. + pub strand_id: Option, + /// Participant role in the published site. + pub role: NeighborhoodParticipantRole, + /// Exact participant frame index. + pub frame_index: u64, + /// Canonical state hash for the participant at that frame. + pub state_hash: String, +} + +impl SiteParticipant { + fn to_core(&self) -> NeighborhoodParticipant { + NeighborhoodParticipant { + participant_id: participant_id(self), + lane_id: worldline_lane_id(self.worldline_id), + strand_id: self.strand_id.map(strand_id_label), + role: self.role.to_core(), + frame_index: self.tick.as_u64(), + state_hash: hash_label(self.state_hash), + } + } +} + +impl NeighborhoodParticipant { + fn to_abi(&self) -> abi::NeighborhoodParticipant { + abi::NeighborhoodParticipant { + participant_id: self.participant_id.clone(), + lane_id: self.lane_id.clone(), + strand_id: self.strand_id.clone(), + role: self.role.to_abi(), + frame_index: self.frame_index, + state_hash: self.state_hash.clone(), + } + } +} + +/// Shared observer/debugger projection for one published local site. +/// +/// This is the first Echo-side grounding for the shared Continuum +/// `NeighborhoodCore` family shape. It is intentionally narrow and does not +/// carry reintegration detail or receipt shell enrichment. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct NeighborhoodCore { + /// Stable published site identity. + pub site_id: String, + /// Anchor lane identity derived from the observed worldline. + pub anchor_lane_id: String, + /// Exact resolved anchor frame index. + pub anchor_frame_index: u64, + /// Optional anchor head identity. Echo does not yet publish this natively. + pub anchor_head_id: Option, + /// Top-level lawful outcome kind for this published site. + pub outcome_kind: AdmissionOutcomeKind, + /// Shared singleton-vs-plural truth. + pub plurality: NeighborhoodPlurality, + /// Participating lanes for the site. + pub participants: Vec, + /// Narrow human-readable summary for debugger surfaces. + pub summary: String, +} + +impl AdmissionOutcomeKind { + fn to_core_abi(self) -> abi::AdmissionOutcomeKind { + match self { + Self::Derived => abi::AdmissionOutcomeKind::Derived, + Self::Plural => abi::AdmissionOutcomeKind::Plural, + Self::Conflict => abi::AdmissionOutcomeKind::Conflict, + Self::Obstruction => abi::AdmissionOutcomeKind::Obstruction, + } + } +} + +impl NeighborhoodCore { + /// Converts the neighborhood core into its shared ABI DTO. + #[must_use] + pub fn to_abi(&self) -> abi::NeighborhoodCore { + abi::NeighborhoodCore { + site_id: self.site_id.clone(), + anchor_lane_id: self.anchor_lane_id.clone(), + anchor_frame_index: self.anchor_frame_index, + anchor_head_id: self.anchor_head_id.clone(), + outcome_kind: self.outcome_kind.to_core_abi(), + plurality: self.plurality.to_abi(), + participants: self + .participants + .iter() + .map(NeighborhoodParticipant::to_abi) + .collect(), + summary: self.summary.clone(), + } + } +} + /// Kernel-backed publication object for one observed local site. #[derive(Clone, PartialEq, Eq, Debug)] pub struct NeighborhoodSite { @@ -145,6 +324,16 @@ pub struct NeighborhoodSite { } impl NeighborhoodSite { + /// Returns the top-level lawful outcome kind for the published local site. + /// + /// Neighborhood publication remains a derived observer-facing surface, but + /// it should still use the same shared outcome algebra as the rest of + /// Echo's admission/publication stack. + #[must_use] + pub fn admission_outcome_kind(&self) -> AdmissionOutcomeKind { + self.plurality.admission_outcome_kind() + } + /// Converts the site into its ABI DTO. #[must_use] pub fn to_abi(&self) -> abi::NeighborhoodSite { @@ -159,6 +348,26 @@ impl NeighborhoodSite { .collect(), } } + + /// Projects the kernel-local site into the shared neighborhood-core family + /// shape consumed by observer/debugger surfaces. + #[must_use] + pub fn to_core(&self) -> NeighborhoodCore { + NeighborhoodCore { + site_id: site_id_label(self.site_id), + anchor_lane_id: worldline_lane_id(self.anchor.worldline_id), + anchor_frame_index: self.anchor.resolved_worldline_tick.as_u64(), + anchor_head_id: None, + outcome_kind: self.admission_outcome_kind(), + plurality: self.plurality.to_core(), + participants: self + .participants + .iter() + .map(SiteParticipant::to_core) + .collect(), + summary: neighborhood_summary(self), + } + } } /// Errors that can occur while publishing a local neighborhood site. @@ -237,11 +446,11 @@ impl NeighborhoodSiteService { if let Some(strand) = primary_strand { participants.push(SiteParticipant { - worldline_id: strand.base_ref.source_worldline_id, + worldline_id: strand.fork_basis_ref.source_lane_id, strand_id: None, - role: ParticipantRole::BaseAnchor, - tick: strand.base_ref.fork_tick, - state_hash: strand.base_ref.boundary_hash, + role: ParticipantRole::BasisAnchor, + tick: strand.fork_basis_ref.fork_tick, + state_hash: strand.fork_basis_ref.boundary_hash, }); for support_pin in &strand.support_pins { @@ -335,6 +544,52 @@ fn compute_site_id( NeighborhoodSiteId(hasher.finalize().into()) } +fn site_id_label(site_id: NeighborhoodSiteId) -> String { + format!("site:{}", hex::encode(site_id.as_bytes())) +} + +fn worldline_lane_id(worldline_id: WorldlineId) -> String { + format!("wl:{}", hex::encode(worldline_id.as_bytes())) +} + +fn strand_id_label(strand_id: StrandId) -> String { + format!("strand:{}", hex::encode(strand_id.as_bytes())) +} + +fn hash_label(hash: Hash) -> String { + hex::encode(hash) +} + +fn participant_id(participant: &SiteParticipant) -> String { + let carrier = participant.strand_id.map_or_else( + || worldline_lane_id(participant.worldline_id), + strand_id_label, + ); + format!( + "participant:{}:{}:{}", + carrier, + participant.role.label(), + participant.tick.as_u64() + ) +} + +fn neighborhood_summary(site: &NeighborhoodSite) -> String { + match site.plurality { + SitePlurality::Singleton => format!( + "Echo published a singleton local site with {} participant at {}@{}.", + site.participants.len(), + worldline_lane_id(site.anchor.worldline_id), + site.anchor.resolved_worldline_tick.as_u64() + ), + SitePlurality::Braided => format!( + "Echo published a plural local site with {} participants at {}@{}.", + site.participants.len(), + worldline_lane_id(site.anchor.worldline_id), + site.anchor.resolved_worldline_tick.as_u64() + ), + } +} + fn write_optional_global_tick(hasher: &mut Hasher, tick: Option) { match tick { Some(value) => { @@ -358,7 +613,7 @@ mod tests { use crate::receipt::TickReceipt; use crate::record::NodeRecord; use crate::snapshot::Snapshot; - use crate::strand::{make_strand_id, BaseRef, Strand}; + use crate::strand::{make_strand_id, ForkBasisRef, Strand}; use crate::tick_patch::{TickCommitStatus, WarpTickPatchV1}; use crate::worldline::{HashTriplet, WorldlineTickHeaderV1, WorldlineTickPatchV1}; use crate::{ @@ -485,10 +740,41 @@ mod tests { .unwrap(); assert_eq!(site.plurality, SitePlurality::Singleton); + assert_eq!(site.admission_outcome_kind(), AdmissionOutcomeKind::Derived); assert_eq!(site.participants.len(), 1); assert_eq!(site.participants[0].role, ParticipantRole::Primary); assert_eq!(site.participants[0].worldline_id, worldline_id); assert_eq!(site.participants[0].state_hash, snapshot.state_root); + + let core = site.to_core(); + assert_eq!( + core.site_id, + format!("site:{}", hex::encode(site.site_id.as_bytes())) + ); + assert_eq!( + core.anchor_lane_id, + format!("wl:{}", hex::encode(worldline_id.as_bytes())) + ); + assert_eq!(core.anchor_frame_index, 0); + assert_eq!(core.anchor_head_id, None); + assert_eq!(core.outcome_kind, AdmissionOutcomeKind::Derived); + assert_eq!(core.plurality, NeighborhoodPlurality::Singleton); + assert_eq!(core.participants.len(), 1); + assert_eq!( + core.participants[0].lane_id, + format!("wl:{}", hex::encode(worldline_id.as_bytes())) + ); + assert_eq!(core.participants[0].strand_id, None); + assert_eq!( + core.participants[0].role, + NeighborhoodParticipantRole::Primary + ); + assert_eq!(core.participants[0].frame_index, 0); + assert_eq!( + core.participants[0].state_hash, + hex::encode(snapshot.state_root) + ); + assert!(core.summary.contains("singleton local site")); } #[test] @@ -560,8 +846,8 @@ mod tests { runtime .register_strand(Strand { strand_id: support_strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_snapshot.hash, boundary_hash: base_snapshot.state_root, @@ -581,8 +867,8 @@ mod tests { runtime .register_strand(Strand { strand_id: primary_strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_snapshot.hash, boundary_hash: base_snapshot.state_root, @@ -618,16 +904,67 @@ mod tests { .unwrap(); assert_eq!(site.plurality, SitePlurality::Braided); + assert_eq!(site.admission_outcome_kind(), AdmissionOutcomeKind::Plural); assert_eq!(site.participants.len(), 3); assert_eq!(site.participants[0].role, ParticipantRole::Primary); assert_eq!(site.participants[0].strand_id, Some(primary_strand_id)); - assert_eq!(site.participants[1].role, ParticipantRole::BaseAnchor); + assert_eq!(site.participants[1].role, ParticipantRole::BasisAnchor); assert_eq!(site.participants[1].worldline_id, base_worldline); assert_eq!(site.participants[1].state_hash, base_snapshot.state_root); assert_eq!(site.participants[2].role, ParticipantRole::Support); assert_eq!(site.participants[2].worldline_id, support_worldline); assert_eq!(site.participants[2].strand_id, Some(support_strand_id)); assert_eq!(site.participants[2].state_hash, support_snapshot.state_root); + + let core = site.to_core(); + assert_eq!( + core.anchor_lane_id, + format!("wl:{}", hex::encode(primary_worldline.as_bytes())) + ); + assert_eq!(core.anchor_frame_index, 0); + assert_eq!(core.outcome_kind, AdmissionOutcomeKind::Plural); + assert_eq!(core.plurality, NeighborhoodPlurality::Plural); + assert_eq!(core.participants.len(), 3); + assert_eq!( + core.participants[0].role, + NeighborhoodParticipantRole::Primary + ); + assert_eq!( + core.participants[0].lane_id, + format!("wl:{}", hex::encode(primary_worldline.as_bytes())) + ); + assert_eq!( + core.participants[0].strand_id, + Some(format!( + "strand:{}", + hex::encode(primary_strand_id.as_bytes()) + )) + ); + assert_eq!( + core.participants[1].role, + NeighborhoodParticipantRole::BasisAnchor + ); + assert_eq!( + core.participants[1].lane_id, + format!("wl:{}", hex::encode(base_worldline.as_bytes())) + ); + assert_eq!(core.participants[1].strand_id, None); + assert_eq!( + core.participants[2].role, + NeighborhoodParticipantRole::Support + ); + assert_eq!( + core.participants[2].lane_id, + format!("wl:{}", hex::encode(support_worldline.as_bytes())) + ); + assert_eq!( + core.participants[2].strand_id, + Some(format!( + "strand:{}", + hex::encode(support_strand_id.as_bytes()) + )) + ); + assert!(core.summary.contains("plural local site")); } #[test] @@ -696,8 +1033,8 @@ mod tests { runtime .register_strand(Strand { strand_id: support_strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_snapshot.hash, boundary_hash: base_snapshot.state_root, @@ -716,8 +1053,8 @@ mod tests { runtime .register_strand(Strand { strand_id: primary_strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_snapshot.hash, boundary_hash: base_snapshot.state_root, diff --git a/crates/warp-core/src/observation.rs b/crates/warp-core/src/observation.rs index 087e3a4b..e5a4434d 100644 --- a/crates/warp-core/src/observation.rs +++ b/crates/warp-core/src/observation.rs @@ -1997,7 +1997,7 @@ mod tests { use crate::receipt::TickReceipt; use crate::record::{EdgeRecord, NodeRecord}; use crate::snapshot::compute_commit_hash_v2; - use crate::strand::{make_strand_id, BaseRef, Strand}; + use crate::strand::{make_strand_id, ForkBasisRef, Strand}; use crate::tick_patch::{SlotId, TickCommitStatus, WarpOp, WarpTickPatchV1}; use crate::worldline::{HashTriplet, WorldlineTickHeaderV1, WorldlineTickPatchV1}; use crate::{ @@ -2338,8 +2338,8 @@ mod tests { runtime .register_strand(Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, diff --git a/crates/warp-core/src/receipt.rs b/crates/warp-core/src/receipt.rs index d897aeba..a4b2adee 100644 --- a/crates/warp-core/src/receipt.rs +++ b/crates/warp-core/src/receipt.rs @@ -13,9 +13,14 @@ //! //! Today (engine spike), the only rejection reason is footprint conflict with //! previously accepted rewrites in the same tick. +//! +//! A [`TickReceipt`] is one shell family in Echo's broader admission +//! architecture. It should not be mistaken for the universal carrier of every +//! witness-bearing publication the runtime may emit. use blake3::Hasher; +use crate::admission::AdmissionOutcomeKind; use crate::ident::{Hash, NodeKey}; use crate::tx::TxId; @@ -141,6 +146,20 @@ pub enum TickReceiptDisposition { Rejected(TickReceiptRejection), } +impl TickReceiptDisposition { + /// Maps the tick-local disposition into Echo's shared lawful outcome family. + /// + /// A rejected candidate in the current tick kernel is obstructed rather than + /// transformed into explicit conflict residue. + #[must_use] + pub fn admission_outcome_kind(self) -> AdmissionOutcomeKind { + match self { + Self::Applied => AdmissionOutcomeKind::Derived, + Self::Rejected(_) => AdmissionOutcomeKind::Obstruction, + } + } +} + /// Why a tick candidate was rejected. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -238,4 +257,17 @@ mod tests { let digest_b = compute_tick_receipt_digest(&entries_b); assert_ne!(digest_a, digest_b); } + + #[test] + fn tick_receipt_disposition_maps_to_shared_admission_outcome_kind() { + assert_eq!( + TickReceiptDisposition::Applied.admission_outcome_kind(), + AdmissionOutcomeKind::Derived + ); + assert_eq!( + TickReceiptDisposition::Rejected(TickReceiptRejection::FootprintConflict) + .admission_outcome_kind(), + AdmissionOutcomeKind::Obstruction + ); + } } diff --git a/crates/warp-core/src/rule.rs b/crates/warp-core/src/rule.rs index db4fbd3b..0c433282 100644 --- a/crates/warp-core/src/rule.rs +++ b/crates/warp-core/src/rule.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS //! Rewrite rule definitions. +#![cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] + use crate::footprint::Footprint; use crate::graph_view::GraphView; use crate::ident::{Hash, NodeId, TypeId}; @@ -38,7 +40,9 @@ pub type ExecuteFn = for<'a> fn(GraphView<'a>, &NodeId, &mut TickDelta); /// Function pointer that computes a rewrite footprint at the provided scope. /// /// Phase 5 signature: footprint computation reads from an immutable -/// [`GraphView`] to declare the read/write sets without mutation. +/// [`GraphView`] to declare the read/write sets without mutation. The declared +/// [`Footprint`] is also the first-cut substrate from which Echo derives a +/// [`BoundedSite`](crate::admission::BoundedSite) for admission. /// /// Parameters: /// - `GraphView`: Read-only view over the graph state (Copy type, 8 bytes) diff --git a/crates/warp-core/src/sandbox.rs b/crates/warp-core/src/sandbox.rs index 3e13e508..20b4400b 100644 --- a/crates/warp-core/src/sandbox.rs +++ b/crates/warp-core/src/sandbox.rs @@ -3,6 +3,7 @@ //! Lightweight sandbox utilities for spinning up isolated Echo instances (Engine + `GraphStore`) //! with configurable scheduler and seeds for determinism tests and A/B comparisons. +#![cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] use std::sync::Arc; diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index d87d7815..222686cd 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -6,6 +6,7 @@ use blake3::Hasher; use echo_wasm_abi::kernel_port as abi; use thiserror::Error; +use crate::admission::AdmissionOutcomeKind; use crate::attachment::{AttachmentOwner, AttachmentPlane, AttachmentValue}; use crate::clock::{GlobalTick, WorldlineTick}; use crate::coordinator::{RuntimeError, WorldlineRuntime}; @@ -72,10 +73,10 @@ impl ConflictReason { pub struct SettlementDelta { /// Strand being compared. pub strand_id: StrandId, - /// Recorded base coordinate for the strand. - pub base_ref: crate::strand::BaseRef, + /// Recorded fork basis for the strand. + pub fork_basis_ref: crate::strand::ForkBasisRef, /// Child worldline carrying speculative suffix history. - pub source_worldline_id: WorldlineId, + pub source_lane_id: WorldlineId, /// First suffix tick eligible for settlement consideration. pub source_suffix_start_tick: WorldlineTick, /// Last suffix tick currently present on the source worldline. @@ -92,8 +93,8 @@ impl SettlementDelta { pub fn to_abi(&self) -> abi::SettlementDelta { abi::SettlementDelta { strand_id: abi::StrandId::from_bytes(*self.strand_id.as_bytes()), - base_ref: base_ref_to_abi(self.base_ref), - source_worldline_id: abi::WorldlineId::from_bytes(*self.source_worldline_id.as_bytes()), + base_ref: fork_basis_ref_to_abi(self.fork_basis_ref), + source_worldline_id: abi::WorldlineId::from_bytes(*self.source_lane_id.as_bytes()), source_suffix_start_tick: abi::WorldlineTick(self.source_suffix_start_tick.as_u64()), source_suffix_end_tick: abi::WorldlineTick(self.source_suffix_end_tick.as_u64()), source_entries: self @@ -150,6 +151,15 @@ pub enum SettlementDecision { } impl SettlementDecision { + /// Maps this settlement publication into Echo's shared lawful outcome family. + #[must_use] + pub fn admission_outcome_kind(&self) -> AdmissionOutcomeKind { + match self { + Self::ImportCandidate(_) => AdmissionOutcomeKind::Derived, + Self::ConflictArtifact(_) => AdmissionOutcomeKind::Conflict, + } + } + fn to_abi(&self) -> abi::SettlementDecision { match self { Self::ImportCandidate(candidate) => abi::SettlementDecision::ImportCandidate { @@ -258,19 +268,21 @@ impl SettlementResult { } } -fn base_ref_to_abi(base_ref: crate::strand::BaseRef) -> abi::BaseRef { +fn fork_basis_ref_to_abi(fork_basis_ref: crate::strand::ForkBasisRef) -> abi::BaseRef { abi::BaseRef { - source_worldline_id: abi::WorldlineId::from_bytes(*base_ref.source_worldline_id.as_bytes()), - fork_tick: abi::WorldlineTick(base_ref.fork_tick.as_u64()), - commit_hash: base_ref.commit_hash.to_vec(), - boundary_hash: base_ref.boundary_hash.to_vec(), - provenance_ref: provenance_ref_to_abi(base_ref.provenance_ref), + source_worldline_id: abi::WorldlineId::from_bytes( + *fork_basis_ref.source_lane_id.as_bytes(), + ), + fork_tick: abi::WorldlineTick(fork_basis_ref.fork_tick.as_u64()), + commit_hash: fork_basis_ref.commit_hash.to_vec(), + boundary_hash: fork_basis_ref.boundary_hash.to_vec(), + provenance_ref: provenance_ref_to_abi(fork_basis_ref.provenance_ref), } } fn settlement_basis_report_to_abi(report: &StrandBasisReport) -> abi::SettlementBasisReport { abi::SettlementBasisReport { - parent_anchor: base_ref_to_abi(report.parent_anchor), + parent_anchor: fork_basis_ref_to_abi(report.parent_anchor), child_worldline_id: abi::WorldlineId::from_bytes(*report.child_worldline_id.as_bytes()), source_suffix_start_tick: abi::WorldlineTick(report.source_suffix_start_tick.as_u64()), source_suffix_end_tick: report @@ -448,7 +460,7 @@ pub enum SettlementError { }, /// Wrapped runtime error. #[error(transparent)] - Runtime(#[from] RuntimeError), + Runtime(#[from] Box), /// Wrapped provenance error. #[error(transparent)] History(#[from] HistoryError), @@ -460,9 +472,15 @@ pub enum SettlementError { StrandBasis(#[from] StrandError), } -/// Deterministic base-worldline settlement service. +/// Deterministic source-basis settlement service. pub struct SettlementService; +impl From for SettlementError { + fn from(source: RuntimeError) -> Self { + Self::Runtime(Box::new(source)) + } +} + struct RecordedEntryDraft { event_kind: ProvenanceEventKind, patch: WorldlineTickPatchV1, @@ -483,13 +501,13 @@ impl SettlementService { ensure_frontier_matches_provenance( runtime, provenance, - strand.base_ref.source_worldline_id, + strand.fork_basis_ref.source_lane_id, )?; let source_len = ensure_frontier_matches_provenance(runtime, provenance, strand.child_worldline_id)?; let basis_report = strand.live_basis_report(provenance)?; let source_suffix_start_tick = strand - .base_ref + .fork_basis_ref .fork_tick .checked_increment() .ok_or(SettlementError::ForkTickOverflow(strand_id))?; @@ -503,11 +521,13 @@ impl SettlementService { .collect::, _>>()?; let source_suffix_end_tick = source_entries .last() - .map_or(strand.base_ref.fork_tick, |entry| entry.worldline_tick); + .map_or(strand.fork_basis_ref.fork_tick, |entry| { + entry.worldline_tick + }); Ok(SettlementDelta { strand_id, - base_ref: strand.base_ref, - source_worldline_id: strand.child_worldline_id, + fork_basis_ref: strand.fork_basis_ref, + source_lane_id: strand.child_worldline_id, source_suffix_start_tick, source_suffix_end_tick, source_entries, @@ -523,11 +543,11 @@ impl SettlementService { ) -> Result { let strand = strand(runtime.strands(), strand_id)?; let delta = Self::compare(runtime, provenance, strand_id)?; - let target_worldline = strand.base_ref.source_worldline_id; + let target_worldline = strand.fork_basis_ref.source_lane_id; let target_frontier_tick = ensure_frontier_matches_provenance(runtime, provenance, target_worldline)?; let expected_target_tick = strand - .base_ref + .fork_basis_ref .fork_tick .checked_increment() .ok_or(SettlementError::ForkTickOverflow(strand_id))?; @@ -548,12 +568,11 @@ impl SettlementService { let mut blocked_reason = None; for source_ref in &delta.source_entries { - let source_entry = - provenance.entry(delta.source_worldline_id, source_ref.worldline_tick)?; + let source_entry = provenance.entry(delta.source_lane_id, source_ref.worldline_tick)?; let reason = blocked_reason.or_else(|| { if parent_at_anchor && (target_frontier_tick != expected_target_tick - || target_tip != Some(strand.base_ref.provenance_ref)) + || target_tip != Some(strand.fork_basis_ref.provenance_ref)) { Some(ConflictReason::BaseDivergence) } else if !matches!(source_entry.event_kind, ProvenanceEventKind::LocalCommit) { @@ -665,7 +684,7 @@ impl SettlementService { Ok(SettlementPlan { strand_id, target_worldline, - target_base_ref: strand.base_ref.provenance_ref, + target_base_ref: strand.fork_basis_ref.provenance_ref, basis_report: delta.basis_report, decisions, }) @@ -1095,7 +1114,7 @@ mod tests { use crate::ident::{make_edge_id, make_node_id, make_type_id}; use crate::playback::PlaybackMode; use crate::record::{EdgeRecord, NodeRecord}; - use crate::strand::{BaseRef, Strand}; + use crate::strand::{ForkBasisRef, Strand}; use crate::tick_patch::{SlotId, WarpOp}; use crate::{GraphStore, WorldlineState}; @@ -1394,8 +1413,8 @@ mod tests { let strand_id = crate::strand::make_strand_id("test-strand"); let strand = Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, @@ -1451,8 +1470,8 @@ mod tests { runtime .register_strand(Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, @@ -1497,8 +1516,8 @@ mod tests { runtime .register_strand(Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, @@ -1529,6 +1548,10 @@ mod tests { plan.decisions[0], SettlementDecision::ImportCandidate(_) )); + assert_eq!( + plan.decisions[0].admission_outcome_kind(), + AdmissionOutcomeKind::Derived + ); let result = SettlementService::settle(&mut runtime, &mut provenance, strand_id).unwrap(); assert_eq!(result.appended_imports.len(), 1); @@ -1655,6 +1678,10 @@ mod tests { overlapping_slots }) if !overlapping_slots.is_empty() )); + assert_eq!( + plan.decisions[0].admission_outcome_kind(), + AdmissionOutcomeKind::Derived + ); let result = SettlementService::settle(&mut runtime, &mut provenance, strand_id).unwrap(); assert_eq!(result.appended_imports.len(), 1); diff --git a/crates/warp-core/src/strand.rs b/crates/warp-core/src/strand.rs index 463b1802..98ee92ed 100644 --- a/crates/warp-core/src/strand.rs +++ b/crates/warp-core/src/strand.rs @@ -2,27 +2,27 @@ // © James Ross Ω FLYING•ROBOTS //! Strand contract for speculative execution lanes. //! -//! A strand is a named, ephemeral, speculative execution lane derived from a -//! base worldline at a specific tick. It is a relation over a child worldline, -//! not a separate substrate. +//! A strand is a named, speculative execution lane derived from a source +//! worldline at a specific tick. It is a relation over a child worldline, not +//! a separate substrate. //! //! # Lifecycle //! //! A strand either exists in the `StrandRegistry` (live) or does not -//! (dropped). There is no tombstone state. Operational control (paused, -//! admitted, ticking) is derived from the writer heads — the heads are the -//! single source of truth for control state. +//! (dropped). There is no tombstone state. Operational control comes from the +//! ordinary writer-head control plane; strands do not own a private tick path +//! or scheduler. //! //! # Invariants //! //! See `docs/invariants/STRAND-CONTRACT.md` for the full normative list. //! Key invariants enforced by this module: //! -//! - **INV-S1:** `base_ref` is immutable after creation. +//! - **INV-S1:** `fork_basis_ref` is immutable after creation. //! - **INV-S2:** Writer heads are created fresh for the child worldline. -//! - **INV-S4:** Writer heads are created Dormant (manual tick only). -//! - **INV-S5:** All `base_ref` fields are verified against provenance. -//! - **INV-S7:** `child_worldline_id != base_ref.source_worldline_id`. +//! - **INV-S4:** Strands advance only through ordinary ingress + `super_tick()`. +//! - **INV-S5:** All `fork_basis_ref` fields are verified against provenance. +//! - **INV-S7:** `child_worldline_id != fork_basis_ref.source_lane_id`. //! - **INV-S8:** Every writer head key belongs to `child_worldline_id`. //! - **INV-S9:** support pins must be validated, live, and read-only. @@ -82,9 +82,9 @@ pub fn make_strand_id(label: &str) -> StrandId { /// - `provenance_ref` carries the same coordinate as a [`ProvenanceRef`]. /// - All fields refer to the **same provenance coordinate**. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct BaseRef { - /// Source worldline this strand was forked from. - pub source_worldline_id: WorldlineId, +pub struct ForkBasisRef { + /// Source lane this strand was forked from in v1. + pub source_lane_id: WorldlineId, /// Last included tick in the copied prefix. pub fork_tick: WorldlineTick, /// Commit hash at `fork_tick`. @@ -136,7 +136,7 @@ pub struct Strand { /// Unique strand identity. pub strand_id: StrandId, /// Immutable fork coordinate. - pub base_ref: BaseRef, + pub fork_basis_ref: ForkBasisRef, /// Child worldline created by fork. pub child_worldline_id: WorldlineId, /// Writer heads for the child worldline (cardinality 1 in v1). @@ -163,12 +163,12 @@ impl Strand { provenance: &P, ) -> Result { let suffix_start = self - .base_ref + .fork_basis_ref .fork_tick .checked_increment() .ok_or(StrandError::ForkTickOverflow(self.strand_id))?; let child_len = history_len(provenance, self.child_worldline_id)?; - let parent_len = history_len(provenance, self.base_ref.source_worldline_id)?; + let parent_len = history_len(provenance, self.fork_basis_ref.source_lane_id)?; if child_len < suffix_start { return Err(StrandError::Provenance(format!( @@ -179,7 +179,7 @@ impl Strand { if parent_len < suffix_start { return Err(StrandError::Provenance(format!( "parent worldline {:?} is shorter than strand anchor successor {}", - self.base_ref.source_worldline_id, suffix_start + self.fork_basis_ref.source_lane_id, suffix_start ))); } @@ -191,13 +191,13 @@ impl Strand { )?; let parent_movement = collect_parent_movement( provenance, - self.base_ref.source_worldline_id, + self.fork_basis_ref.source_lane_id, suffix_start, parent_len, )?; let realized_parent_ref = - tip_ref_at_len(provenance, self.base_ref.source_worldline_id, parent_len)? - .unwrap_or(self.base_ref.provenance_ref); + tip_ref_at_len(provenance, self.fork_basis_ref.source_lane_id, parent_len)? + .unwrap_or(self.fork_basis_ref.provenance_ref); let source_suffix_end_tick = last_tick_before(child_len); let parent_revalidation = if parent_len == suffix_start { StrandRevalidationState::AtAnchor @@ -205,12 +205,12 @@ impl Strand { let overlapping_slots = owned_divergence.overlapping_parent_writes(&parent_movement); if overlapping_slots.is_empty() { StrandRevalidationState::ParentAdvancedDisjoint { - parent_from: self.base_ref.provenance_ref, + parent_from: self.fork_basis_ref.provenance_ref, parent_to: realized_parent_ref, } } else { StrandRevalidationState::RevalidationRequired { - parent_from: self.base_ref.provenance_ref, + parent_from: self.fork_basis_ref.provenance_ref, parent_to: realized_parent_ref, overlapping_slots, } @@ -219,7 +219,7 @@ impl Strand { Ok(StrandBasisReport { strand_id: self.strand_id, - parent_anchor: self.base_ref, + parent_anchor: self.fork_basis_ref, child_worldline_id: self.child_worldline_id, source_suffix_start_tick: suffix_start, source_suffix_end_tick, @@ -397,7 +397,7 @@ pub struct StrandBasisReport { /// Strand being reported. pub strand_id: StrandId, /// Immutable parent anchor recorded at strand creation. - pub parent_anchor: BaseRef, + pub parent_anchor: ForkBasisRef, /// Child worldline currently carrying local divergence. pub child_worldline_id: WorldlineId, /// First child tick after the anchor. @@ -582,7 +582,7 @@ impl StrandRegistry { /// Inserts a fully constructed strand into the registry. /// /// Validates contract invariants before insertion: - /// - INV-S7: `child_worldline_id != base_ref.source_worldline_id` + /// - INV-S7: `child_worldline_id != fork_basis_ref.source_lane_id` /// - INV-S8: every writer head belongs to `child_worldline_id` /// - support pins, when present, reference already-live strands and do not /// duplicate or self-target @@ -597,9 +597,9 @@ impl StrandRegistry { return Err(StrandError::AlreadyExists(strand.strand_id)); } // INV-S7: distinct worldlines. - if strand.child_worldline_id == strand.base_ref.source_worldline_id { + if strand.child_worldline_id == strand.fork_basis_ref.source_lane_id { return Err(StrandError::InvariantViolation( - "INV-S7: child_worldline_id must differ from base_ref.source_worldline_id", + "INV-S7: child_worldline_id must differ from fork_basis_ref.source_lane_id", )); } // INV-S8: head ownership. @@ -659,21 +659,21 @@ impl StrandRegistry { } /// Returns a zero-allocation iterator over live strands derived from the - /// given base worldline, ordered by [`StrandId`]. - pub fn iter_by_base<'a>( + /// given source lane, ordered by [`StrandId`]. + pub fn iter_by_source_lane<'a>( &'a self, - base_worldline_id: &'a WorldlineId, + source_lane_id: &'a WorldlineId, ) -> impl Iterator + 'a { self.strands .values() - .filter(move |s| &s.base_ref.source_worldline_id == base_worldline_id) + .filter(move |s| &s.fork_basis_ref.source_lane_id == source_lane_id) } - /// Returns all live strands derived from the given base worldline, - /// ordered by [`StrandId`]. Allocates; prefer [`iter_by_base`](Self::iter_by_base) - /// in hot paths. - pub fn list_by_base<'a>(&'a self, base_worldline_id: &'a WorldlineId) -> Vec<&'a Strand> { - self.iter_by_base(base_worldline_id).collect() + /// Returns all live strands derived from the given source lane, + /// ordered by [`StrandId`]. Allocates; prefer + /// [`iter_by_source_lane`](Self::iter_by_source_lane) in hot paths. + pub fn list_by_source_lane<'a>(&'a self, source_lane_id: &'a WorldlineId) -> Vec<&'a Strand> { + self.iter_by_source_lane(source_lane_id).collect() } /// Returns the support pins for one strand. diff --git a/crates/warp-core/src/witnessed_suffix.rs b/crates/warp-core/src/witnessed_suffix.rs index a811fe8d..edbfd667 100644 --- a/crates/warp-core/src/witnessed_suffix.rs +++ b/crates/warp-core/src/witnessed_suffix.rs @@ -17,7 +17,7 @@ use crate::ident::Hash; use crate::provenance_store::ProvenanceRef; use crate::settlement::ConflictReason; use crate::strand::{ - BaseRef, StrandBasisReport, StrandOverlapRevalidation, StrandRevalidationState, + ForkBasisRef, StrandBasisReport, StrandOverlapRevalidation, StrandRevalidationState, }; use crate::tick_patch::SlotId; use crate::worldline::WorldlineId; @@ -991,7 +991,7 @@ fn witnessed_suffix_outcome_to_abi( fn settlement_basis_report_to_abi(report: &StrandBasisReport) -> abi::SettlementBasisReport { abi::SettlementBasisReport { - parent_anchor: base_ref_to_abi(report.parent_anchor), + parent_anchor: fork_basis_ref_to_abi(report.parent_anchor), child_worldline_id: worldline_id_to_abi(report.child_worldline_id), source_suffix_start_tick: worldline_tick_to_abi(report.source_suffix_start_tick), source_suffix_end_tick: report.source_suffix_end_tick.map(worldline_tick_to_abi), @@ -1055,13 +1055,13 @@ fn overlap_revalidation_to_abi( } } -fn base_ref_to_abi(base_ref: BaseRef) -> abi::BaseRef { +fn fork_basis_ref_to_abi(fork_basis_ref: ForkBasisRef) -> abi::BaseRef { abi::BaseRef { - source_worldline_id: worldline_id_to_abi(base_ref.source_worldline_id), - fork_tick: worldline_tick_to_abi(base_ref.fork_tick), - commit_hash: base_ref.commit_hash.to_vec(), - boundary_hash: base_ref.boundary_hash.to_vec(), - provenance_ref: provenance_ref_to_abi(base_ref.provenance_ref), + source_worldline_id: worldline_id_to_abi(fork_basis_ref.source_lane_id), + fork_tick: worldline_tick_to_abi(fork_basis_ref.fork_tick), + commit_hash: fork_basis_ref.commit_hash.to_vec(), + boundary_hash: fork_basis_ref.boundary_hash.to_vec(), + provenance_ref: provenance_ref_to_abi(fork_basis_ref.provenance_ref), } } diff --git a/crates/warp-core/src/witnessed_suffix_tests.rs b/crates/warp-core/src/witnessed_suffix_tests.rs index ae976a8c..bdf8288e 100644 --- a/crates/warp-core/src/witnessed_suffix_tests.rs +++ b/crates/warp-core/src/witnessed_suffix_tests.rs @@ -5,8 +5,8 @@ use echo_wasm_abi::kernel_port as abi; use crate::{ derive_witnessed_suffix_shell_digest, evaluate_witnessed_suffix_admission, export_suffix, - import_suffix, make_node_id, make_strand_id, BaseRef, CausalSuffixBundle, ConflictReason, - ExportSuffixRequest, Hash, ImportSuffixRequest, ImportSuffixResult, NodeKey, + import_suffix, make_node_id, make_strand_id, CausalSuffixBundle, ConflictReason, + ExportSuffixRequest, ForkBasisRef, Hash, ImportSuffixRequest, ImportSuffixResult, NodeKey, ParentMovementFootprint, ProvenanceRef, ReadingResidualPosture, SlotId, StrandBasisReport, StrandDivergenceFootprint, StrandOverlapRevalidation, StrandRevalidationState, WarpId, WitnessedSuffixAdmissionContext, WitnessedSuffixAdmissionOutcome, @@ -43,8 +43,8 @@ fn basis_report(realized_parent_ref: ProvenanceRef) -> StrandBasisReport { StrandBasisReport { strand_id: make_strand_id("witnessed-suffix-test"), - parent_anchor: BaseRef { - source_worldline_id: parent_anchor.worldline_id, + parent_anchor: ForkBasisRef { + source_lane_id: parent_anchor.worldline_id, fork_tick: parent_anchor.worldline_tick, commit_hash: parent_anchor.commit_hash, boundary_hash: [21; 32], diff --git a/crates/warp-core/tests/strand_contract_tests.rs b/crates/warp-core/tests/strand_contract_tests.rs index 2b9bd2d4..7c30b150 100644 --- a/crates/warp-core/tests/strand_contract_tests.rs +++ b/crates/warp-core/tests/strand_contract_tests.rs @@ -8,7 +8,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] use warp_core::strand::{ - make_strand_id, BaseRef, DropReceipt, Strand, StrandError, StrandRegistry, + make_strand_id, DropReceipt, ForkBasisRef, Strand, StrandError, StrandRegistry, StrandRevalidationState, SupportPin, }; use warp_core::{ @@ -79,8 +79,8 @@ fn make_test_strand( Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick, commit_hash, boundary_hash, @@ -223,13 +223,13 @@ fn register_worldline_with_tick(provenance: &mut ProvenanceService, worldline_id append_committed_tick(provenance, worldline_id, GlobalTick::from_raw(1)); } -// ── INV-S7: child_worldline_id != base_ref.source_worldline_id ────────── +// ── INV-S7: child_worldline_id != fork_basis_ref.source_lane_id ────────── #[test] fn inv_s7_child_and_base_worldlines_are_distinct() { let strand = make_test_strand("s7-test", wl(1), wl(2), wt(5)); assert_ne!( - strand.child_worldline_id, strand.base_ref.source_worldline_id, + strand.child_worldline_id, strand.fork_basis_ref.source_lane_id, "INV-S7: child worldline must differ from base" ); } @@ -254,31 +254,42 @@ fn inv_s2_s8_strand_heads_belong_to_child_worldline() { } } -// ── INV-S4: strand heads are Dormant and Paused ──────────────────────── +// ── INV-S4: strand heads use ordinary writer-head control ────────────── #[test] -fn inv_s4_strand_head_created_dormant_and_paused() { +fn inv_s4_strand_head_uses_ordinary_writer_head_control() { let child = wl(2); let head_key = WriterHeadKey { worldline_id: child, - head_id: make_head_id("strand-head-dormant"), + head_id: make_head_id("strand-head-generic"), }; - let head = WriterHead::new(head_key, PlaybackMode::Paused); + let mut head = WriterHead::new(head_key, PlaybackMode::Play); - assert!(head.is_paused(), "strand head must be created paused"); - // Dormant must be set explicitly - let mut head = head; + assert!( + head.is_admitted(), + "strand head should use default admitted eligibility" + ); + assert!( + !head.is_paused(), + "strand head should follow ordinary writer-head playback defaults" + ); + + head.pause(); + assert!( + head.is_paused(), + "generic pause control should remain available" + ); head.set_eligibility(HeadEligibility::Dormant); assert!( !head.is_admitted(), - "strand head must not be admitted (Dormant)" + "generic eligibility control should remain available" ); } -// ── INV-S4 / INV-S10: strand heads excluded from live scheduler ──────── +// ── INV-S4 / INV-S10: strand heads follow ordinary runnable-set rules ─── #[test] -fn inv_s4_s10_dormant_strand_heads_excluded_from_runnable_set() { +fn inv_s4_s10_strand_heads_follow_ordinary_runnable_set_rules() { let base_wl = wl(1); let strand_wl = wl(2); @@ -291,13 +302,12 @@ fn inv_s4_s10_dormant_strand_heads_excluded_from_runnable_set() { }; head_registry.insert(WriterHead::new(live_key, PlaybackMode::Play)); - // Register a strand head on the child worldline (dormant, paused) + // Register a strand head on the child worldline using ordinary head control let strand_key = WriterHeadKey { worldline_id: strand_wl, head_id: make_head_id("strand-head"), }; - let mut strand_head = WriterHead::new(strand_key, PlaybackMode::Paused); - strand_head.set_eligibility(HeadEligibility::Dormant); + let strand_head = WriterHead::new(strand_key, PlaybackMode::Play); head_registry.insert(strand_head); // Build the runnable set @@ -310,10 +320,22 @@ fn inv_s4_s10_dormant_strand_heads_excluded_from_runnable_set() { "live head should be in runnable set" ); - // Strand head must NOT be runnable + // Strand head should participate like any other admitted, unpaused head + assert!( + runnable.iter().any(|k| *k == strand_key), + "INV-S4: strand head should appear in runnable set when admitted and unpaused" + ); + + let mut strand_head = head_registry + .remove(&strand_key) + .expect("strand head present"); + strand_head.pause(); + head_registry.insert(strand_head); + + runnable.rebuild(&head_registry); assert!( !runnable.iter().any(|k| *k == strand_key), - "INV-S4/S10: dormant strand head must not appear in runnable set" + "paused strand head should be excluded by ordinary runnable-set rules" ); } @@ -366,8 +388,8 @@ fn live_basis_report_allows_parent_advance_outside_owned_footprint() { let strand = Strand { strand_id: make_strand_id("live-basis-disjoint"), - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_ref.commit_hash, boundary_hash: provenance @@ -437,8 +459,8 @@ fn live_basis_report_requires_revalidation_when_parent_invades_owned_footprint() let strand = Strand { strand_id: make_strand_id("live-basis-overlap"), - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_ref.commit_hash, boundary_hash: provenance @@ -469,15 +491,15 @@ fn live_basis_report_requires_revalidation_when_parent_invades_owned_footprint() )); } -// ── INV-S5: base_ref fields agree ────────────────────────────────────── +// ── INV-S5: fork_basis_ref fields agree ────────────────────────────────────── #[test] fn inv_s5_base_ref_fields_consistent() { let strand = make_test_strand("s5-test", wl(1), wl(2), wt(5)); - let br = &strand.base_ref; + let br = &strand.fork_basis_ref; - // provenance_ref must agree with base_ref scalars - assert_eq!(br.provenance_ref.worldline_id, br.source_worldline_id); + // provenance_ref must agree with fork_basis_ref scalars + assert_eq!(br.provenance_ref.worldline_id, br.source_lane_id); assert_eq!(br.provenance_ref.worldline_tick, br.fork_tick); assert_eq!(br.provenance_ref.commit_hash, br.commit_hash); } @@ -542,8 +564,8 @@ fn registry_insert_rejects_inv_s8_wrong_head_worldline() { let strand_id = make_strand_id("s8-bad"); let strand = Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: wl(1), + fork_basis_ref: ForkBasisRef { + source_lane_id: wl(1), fork_tick: wt(5), commit_hash: [0xAA; 32], boundary_hash: [0xBB; 32], @@ -643,8 +665,8 @@ fn registry_insert_rejects_duplicate_support_target() { let owner_id = make_strand_id("duplicate-owner"); let owner = Strand { strand_id: owner_id, - base_ref: BaseRef { - source_worldline_id: wl(1), + fork_basis_ref: ForkBasisRef { + source_lane_id: wl(1), fork_tick: wt(5), commit_hash: [0xAA; 32], boundary_hash: [0xBB; 32], @@ -799,7 +821,7 @@ fn registry_remove_nonexistent_returns_error() { } #[test] -fn registry_list_by_base_filters_correctly() { +fn registry_list_by_source_lane_filters_correctly() { let mut registry = StrandRegistry::new(); let base_a = wl(1); let base_b = wl(10); @@ -814,20 +836,20 @@ fn registry_list_by_base_filters_correctly() { .insert(make_test_strand("b1", base_b, wl(4), wt(5))) .unwrap(); - let from_a = registry.list_by_base(&base_a); - assert_eq!(from_a.len(), 2, "should find 2 strands from base_a"); + let from_a = registry.list_by_source_lane(&base_a); + assert_eq!(from_a.len(), 2, "should find 2 strands from source lane a"); for s in &from_a { - assert_eq!(s.base_ref.source_worldline_id, base_a); + assert_eq!(s.fork_basis_ref.source_lane_id, base_a); } - let from_b = registry.list_by_base(&base_b); - assert_eq!(from_b.len(), 1, "should find 1 strand from base_b"); + let from_b = registry.list_by_source_lane(&base_b); + assert_eq!(from_b.len(), 1, "should find 1 strand from source lane b"); let unknown = wl(99); - let from_none = registry.list_by_base(&unknown); + let from_none = registry.list_by_source_lane(&unknown); assert!( from_none.is_empty(), - "should find no strands from unknown base" + "should find no strands from unknown source lane" ); } @@ -942,9 +964,9 @@ fn provenance_fork_happy_path_child_has_correct_prefix() { // Verify the child's worldline ID was rewritten. assert_eq!(child_entry.worldline_id, child_id); - // Build base_ref from the SOURCE entry, not the child copy. - let base_ref = BaseRef { - source_worldline_id: base_id, + // Build fork_basis_ref from the SOURCE entry, not the child copy. + let fork_basis_ref = ForkBasisRef { + source_lane_id: base_id, fork_tick: wt(1), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, @@ -957,12 +979,18 @@ fn provenance_fork_happy_path_child_has_correct_prefix() { // INV-S5: all fields agree with source coordinate. assert_eq!( - base_ref.provenance_ref.worldline_id, - base_ref.source_worldline_id + fork_basis_ref.provenance_ref.worldline_id, + fork_basis_ref.source_lane_id + ); + assert_eq!( + fork_basis_ref.provenance_ref.worldline_tick, + fork_basis_ref.fork_tick + ); + assert_eq!( + fork_basis_ref.provenance_ref.commit_hash, + fork_basis_ref.commit_hash ); - assert_eq!(base_ref.provenance_ref.worldline_tick, base_ref.fork_tick); - assert_eq!(base_ref.provenance_ref.commit_hash, base_ref.commit_hash); - assert_eq!(base_ref.boundary_hash, base_entry.expected.state_root); + assert_eq!(fork_basis_ref.boundary_hash, base_entry.expected.state_root); } // ── Drop receipt carries correct fields ───────────────────────────────── diff --git a/crates/warp-wasm/src/lib.rs b/crates/warp-wasm/src/lib.rs index 98fee385..afe63051 100644 --- a/crates/warp-wasm/src/lib.rs +++ b/crates/warp-wasm/src/lib.rs @@ -366,6 +366,23 @@ pub fn observe_neighborhood_site(request_bytes: &[u8]) -> Uint8Array { encode_result(with_kernel_ref(|k| k.observe_neighborhood_site(request))) } +/// Publish the shared neighborhood-core projection for an explicit observation request. +/// +/// The request bytes must decode as canonical-CBOR `ObservationRequest`. +#[wasm_bindgen] +pub fn observe_neighborhood_core(request_bytes: &[u8]) -> Uint8Array { + let request = match echo_wasm_abi::decode_cbor::(request_bytes) { + Ok(request) => request, + Err(err) => { + return encode_err(&AbiError { + code: kernel_port::error_codes::INVALID_PAYLOAD, + message: format!("invalid observation request payload: {err}"), + }) + } + }; + encode_result(with_kernel_ref(|k| k.observe_neighborhood_core(request))) +} + /// Compare a strand suffix against its recorded base coordinate. /// /// The request bytes must decode as canonical-CBOR `SettlementRequest`. @@ -679,16 +696,18 @@ mod schema_validation_tests { mod init_tests { use super::*; use echo_wasm_abi::kernel_port::{ - BaseRef, BuiltinObserverPlan, ConflictArtifactDraft, ConflictReason, DispatchResponse, - GlobalTick, HeadInfo, HeadObservation, NeighborhoodSite, NeighborhoodSiteId, - ObservationArtifact, ObservationAt, ObservationBasisPosture, ObservationFrame, - ObservationPayload, ObservationProjection, ParticipantRole, ProvenanceRef, - ReadingBudgetPosture, ReadingEnvelope, ReadingObserverBasis, ReadingObserverPlan, - ReadingResidualPosture, ReadingRightsPosture, ReadingWitnessRef, RegistryInfo, - ResolvedObservationCoordinate, RunCompletion, RunId, SchedulerMode, SchedulerState, - SchedulerStatus, SettlementBasisReport, SettlementDecision, SettlementDelta, - SettlementParentRevalidation, SettlementPlan, SettlementRequest, SettlementResult, - SiteParticipant, SitePlurality, WorkState, WorldlineId, WorldlineTick, ABI_VERSION, + AdmissionOutcomeKind, BaseRef, BuiltinObserverPlan, ConflictArtifactDraft, ConflictReason, + DispatchResponse, GlobalTick, HeadInfo, HeadObservation, NeighborhoodCore, + NeighborhoodParticipant, NeighborhoodParticipantRole, NeighborhoodPlurality, + NeighborhoodSite, NeighborhoodSiteId, ObservationArtifact, ObservationAt, + ObservationBasisPosture, ObservationFrame, ObservationPayload, ObservationProjection, + ParticipantRole, ProvenanceRef, ReadingBudgetPosture, ReadingEnvelope, + ReadingObserverBasis, ReadingObserverPlan, ReadingResidualPosture, ReadingRightsPosture, + ReadingWitnessRef, RegistryInfo, ResolvedObservationCoordinate, RunCompletion, RunId, + SchedulerMode, SchedulerState, SchedulerStatus, SettlementBasisReport, SettlementDecision, + SettlementDelta, SettlementParentRevalidation, SettlementPlan, SettlementRequest, + SettlementResult, SiteParticipant, SitePlurality, WorkState, WorldlineId, WorldlineTick, + ABI_VERSION, }; struct StubKernel; @@ -794,6 +813,29 @@ mod init_tests { }) } + fn observe_neighborhood_core( + &self, + _request: ObservationRequest, + ) -> Result { + Ok(NeighborhoodCore { + site_id: "site:stub".into(), + anchor_lane_id: "wl:stub".into(), + anchor_frame_index: 0, + anchor_head_id: None, + outcome_kind: AdmissionOutcomeKind::Derived, + plurality: NeighborhoodPlurality::Singleton, + participants: vec![NeighborhoodParticipant { + participant_id: "participant:stub:primary:0".into(), + lane_id: "wl:stub".into(), + strand_id: None, + role: NeighborhoodParticipantRole::Primary, + frame_index: 0, + state_hash: "02".repeat(32), + }], + summary: "Stub neighborhood core".into(), + }) + } + fn compare_settlement( &self, request: SettlementRequest, @@ -971,6 +1013,28 @@ mod init_tests { assert_eq!(site.participants[0].role, ParticipantRole::Primary); } + #[test] + fn neighborhood_core_publication_uses_installed_kernel() { + clear_kernel(); + install_kernel(Box::new(StubKernel)); + let request = ObservationRequest { + coordinate: kernel_port::ObservationCoordinate { + worldline_id: WorldlineId::from_bytes([9; 32]), + at: ObservationAt::Frontier, + }, + frame: ObservationFrame::CommitBoundary, + projection: ObservationProjection::Head, + }; + let core = with_kernel_ref(|k| k.observe_neighborhood_core(request)).unwrap(); + assert_eq!(core.outcome_kind, AdmissionOutcomeKind::Derived); + assert_eq!(core.plurality, NeighborhoodPlurality::Singleton); + assert_eq!(core.participants.len(), 1); + assert_eq!( + core.participants[0].role, + NeighborhoodParticipantRole::Primary + ); + } + #[test] fn settlement_publication_uses_installed_kernel() { clear_kernel(); diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs index 761b76bd..a9afd565 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -16,8 +16,9 @@ use echo_wasm_abi::kernel_port::{ BraidId as AbiBraidId, ControlIntentV1, CoordinateAt as AbiCoordinateAt, DispatchResponse, EchoCoordinate as AbiEchoCoordinate, GlobalTick as AbiGlobalTick, HeadEligibility as AbiHeadEligibility, HeadId as AbiHeadId, HeadInfo, KernelPort, - NeighborhoodSite as AbiNeighborhoodSite, ObservationArtifact as AbiObservationArtifact, - ObservationFrame as AbiObservationFrame, ObservationProjection as AbiObservationProjection, + NeighborhoodCore as AbiNeighborhoodCore, NeighborhoodSite as AbiNeighborhoodSite, + ObservationArtifact as AbiObservationArtifact, ObservationFrame as AbiObservationFrame, + ObservationProjection as AbiObservationProjection, ObservationReadBudget as AbiObservationReadBudget, ObservationRequest as AbiObservationRequest, ObservationRights as AbiObservationRights, ObserveOpticRequest as AbiObserveOpticRequest, ObserveOpticResult as AbiObserveOpticResult, ObserverInstanceRef as AbiObserverInstanceRef, @@ -955,6 +956,16 @@ impl KernelPort for WarpKernel { .map_err(Self::map_neighborhood_error) } + fn observe_neighborhood_core( + &self, + request: AbiObservationRequest, + ) -> Result { + let request = Self::to_core_request(request)?; + NeighborhoodSiteService::observe(&self.runtime, &self.provenance, &self.engine, request) + .map(|site| site.to_core().to_abi()) + .map_err(Self::map_neighborhood_error) + } + fn compare_settlement( &self, request: AbiSettlementRequest, @@ -1030,8 +1041,8 @@ mod tests { }; use warp_core::{ compute_commit_hash_v2, make_edge_id, make_head_id, make_node_id, make_strand_id, - make_type_id, make_warp_id, materialization::make_channel_id, AdmissionLawId, BaseRef, - CoordinateAt, EchoCoordinate, EdgeRecord, GlobalTick, GraphStore, HashTriplet, InboxPolicy, + make_type_id, make_warp_id, materialization::make_channel_id, AdmissionLawId, CoordinateAt, + EchoCoordinate, EdgeRecord, ForkBasisRef, GlobalTick, GraphStore, HashTriplet, InboxPolicy, IntentFamilyId, NodeId, NodeKey, NodeRecord, OpticActorId, OpticCapabilityId, OpticCause, OpticReadBudget, PlaybackMode, ProvenanceEntry, ProvenanceService, ProvenanceStore, SlotId, Strand, StrandId, TickCommitStatus, WarpOp, WarpTickPatchV1, WorldlineHeadOptic, @@ -1312,8 +1323,8 @@ mod tests { runtime .register_strand(Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, @@ -1373,8 +1384,8 @@ mod tests { runtime .register_strand(Strand { strand_id, - base_ref: BaseRef { - source_worldline_id: base_worldline, + fork_basis_ref: ForkBasisRef { + source_lane_id: base_worldline, fork_tick: wt(0), commit_hash: base_entry.expected.commit_hash, boundary_hash: base_entry.expected.state_root, @@ -2005,6 +2016,39 @@ mod tests { ); } + #[test] + fn observe_neighborhood_core_returns_shared_projection_for_default_worldline() { + let kernel = WarpKernel::new().unwrap(); + let core = kernel + .observe_neighborhood_core(AbiObservationRequest { + coordinate: AbiObservationCoordinate { + worldline_id: abi_worldline_id(kernel.default_worldline), + at: AbiObservationAt::Frontier, + }, + frame: AbiObservationFrame::CommitBoundary, + projection: AbiObservationProjection::Head, + }) + .unwrap(); + + assert_eq!( + core.outcome_kind, + echo_wasm_abi::kernel_port::AdmissionOutcomeKind::Derived + ); + assert_eq!( + core.plurality, + echo_wasm_abi::kernel_port::NeighborhoodPlurality::Singleton + ); + assert_eq!(core.anchor_frame_index, 0); + assert_eq!(core.anchor_head_id, None); + assert_eq!(core.participants.len(), 1); + assert_eq!( + core.participants[0].role, + echo_wasm_abi::kernel_port::NeighborhoodParticipantRole::Primary + ); + assert!(core.anchor_lane_id.starts_with("wl:")); + assert!(core.site_id.starts_with("site:")); + } + #[test] fn settlement_publication_returns_import_plan_for_child_suffix() { let mut kernel = WarpKernel::new().unwrap(); diff --git a/docs/audit/2026-04-11_code-quality.md b/docs/audit/2026-04-11_code-quality.md new file mode 100644 index 00000000..7adf664e --- /dev/null +++ b/docs/audit/2026-04-11_code-quality.md @@ -0,0 +1,81 @@ + + + +# AUDIT: CODE QUALITY (2026-04-11) + +## 0. 🏆 EXECUTIVE REPORT CARD (Strategic Lead View) + +| **Metric** | **Score (1-10)** | **Recommendation** | +| ----------------------------- | ---------------- | --------------------------------------------------------------------------------- | +| **Developer Experience (DX)** | 8.0 | **Best of:** High-integrity DIND determinism verification. | +| **Internal Quality (IQ)** | 9.5 | **Watch Out For:** Complex footprint-conflict matrix logic. | +| **Overall Recommendation** | **THUMBS UP** | **Justification:** A masterclass in structural determinism and systems integrity. | + +--- + +## 1. DX: ERGONOMICS & INTERFACE CLARITY (Advocate View) + +- **1.1. Time-to-Value (TTV) Score (1-10):** 7 + - **Answer:** High bar for entry. Understanding footprints and DPO-inspired rewriting is a steep learning curve. + - **Action Prompt (TTV Improvement):** `Create a 'cargo xtask scaffold-rule' command that generates a template rule with boilerplate footprint declarations and a passing determinism test.` + +- **1.2. Principle of Least Astonishment (POLA):** + - **Answer:** `materialize()` in `warp-core` requires explicit options for receipts vs. state, which can be verbose for simple investigations. + - **Action Prompt (Interface Refactoring):** `Introduce a 'WarpCore::inspect()' helper that returns a combined 'Snapshot' object containing both state and the latest tick receipts by default.` + +- **1.3. Error Usability:** + - **Answer:** Footprint violations poison the delta but the error message is often a raw coordinate mismatch. + - **Action Prompt (Error Handling Fix):** `Implement a 'FootprintViolationDiagnostic' that maps raw graph coordinates back to the source-code variable names or AST paths declared in the rule.` + +--- + +## 2. DX: DOCUMENTATION & EXTENDABILITY (Advocate View) + +- **2.1. Documentation Gap:** + - **Answer:** The relationship between `kairos` (possibility) and `chronos` (sequence) is theoretically sound but lacks a practical guide for building "Multiverse Puzzles." + - **Action Prompt (Documentation Creation):** `Create 'docs/guide/temporal-mechanics.md' detailing how to useKairos branches to implement déjà vu, Mandela artifacts, and other causal gameplay effects.` + +- **2.2. Customization Score (1-10):** 9 + - **Answer:** Hexagonal ports for `ProvenanceStore` and `ScenePort` are world-class. Weakest point is the hardcoded `ReduceOp` set in the materialization bus. + - **Action Prompt (Extension Improvement):** `Allow custom 'ReduceOp' registration in the MaterializationBus, enabling domain-specific deterministic reduction logic (e.g. spatial grid accumulation).` + +--- + +## 3. INTERNAL QUALITY: ARCHITECTURE & MAINTAINABILITY (Architect View) + +- **3.1. Technical Debt Hotspot:** + - **Answer:** `crates/warp-core/src/engine_impl.rs`. It is the central coordinator for the entire rewrite lifecycle. + - **Action Prompt (Debt Reduction):** `Extract the 'Canonical Merge' logic from 'engine_impl.rs' into a dedicated 'MergeEngine' module, clarifying the boundary between parallel rule execution and state commitment.` + +- **3.2. Abstraction Violation:** + - **Answer:** Some TTD-browser surfaces contain hand-written TypeScript mirrors of Rust structs that should be Wesley-generated. + - **Action Prompt (SoC Refactoring):** `Port all browser-TTD data structures to the Wesley schema and replace handwritten mirrors with generated code to prevent cross-language contract drift.` + +- **3.3. Testability Barrier:** + - **Answer:** High. The project has extreme determinism testing. The only barrier is the lack of a "Fast Replay" mode for giant worldlines (>1M ticks). + - **Action Prompt (Testability Improvement):** `Implement 'Checkpoint-Based Replay' in the DIND harness, allowing tests to resume from the nearest Merkle snapshot rather than replaying from Genesis.` + +--- + +## 4. INTERNAL QUALITY: RISK & EFFICIENCY (Auditor View) + +- **4.1. The Critical Flaw:** + - **Answer:** Floating-point drift risk. While Echo bans standard floats, the opt-in `F32Scalar` feature is optimistic. + - **Action Prompt (Risk Mitigation):** `Enforce 'det_fixed' (DFix64) as the default build profile for all published artifacts, making floating-point math opt-in only for non-consensus paths.` + +- **4.2. Efficiency Sink:** + - **Answer:** `SnapshotAccumulator` performs redundant hashing of unchanged sub-graphs in very deep hierarchies. + - **Action Prompt (Optimization):** `Implement 'Merkle-Tree Memoization' in the snapshot accumulator, caching the hash of unchanged descended WARP instances across ticks.` + +- **4.3. Dependency Health:** + - **Answer:** Excellent. Strictly pinned versions and a clean `Cargo.lock`. + - **Action Prompt (Dependency Update):** `Run 'cargo deny check' weekly to ensure no unsafe or vulnerable dependencies are introduced via the @git-stunts peer stack.` + +--- + +## 5. STRATEGIC SYNTHESIS & ACTION PLAN (Strategist View) + +- **5.1. Combined Health Score (1-10):** 9.2 +- **5.2. Strategic Fix:** **Wesley Convergence**. Moving all cross-boundary contracts to the Wesley schema compiler is the highest leverage point for long-term systems integrity. +- **5.3. Mitigation Prompt:** + - **Action Prompt (Strategic Priority):** `Refactor the TTD and ScenePort boundaries to use 100% Wesley-generated contracts. Remove all handwritten JSON/TS mirrors and replace them with bit-exact generated adapters. This locks in the 'Inevitability' tenet across the hot/cold boundary.` diff --git a/docs/audit/2026-04-11_documentation-quality.md b/docs/audit/2026-04-11_documentation-quality.md new file mode 100644 index 00000000..61966400 --- /dev/null +++ b/docs/audit/2026-04-11_documentation-quality.md @@ -0,0 +1,42 @@ + + + +# AUDIT: DOCUMENTATION QUALITY (2026-04-11) + +## 1. ACCURACY & EFFECTIVENESS ASSESSMENT + +- **1.1. Core Mismatch:** + - **Answer:** The root `README.md` previously described Echo as "early" with "sharp edges" but then made strong claims about "Stable" core determinism. I have refined this to lead with its industrial-grade simulation identity while acknowledging the evolving API. + +- **1.2. Audience & Goal Alignment:** + - **Answer:** + - **Target Audience:** Systems engineers, game developers, and researchers. + - **Top 3 Questions addressed?** + 1. **"How is it parallel AND deterministic?"**: Yes (The Trick section). + 2. **"How do I prove it?"**: Yes (DIND section). + 3. **"What is WARP?"**: Yes (Algebra section). + +- **1.3. Time-to-Value (TTV) Barrier:** + - **Answer:** The "Theoretical Foundations" are dense. A developer might spend an hour reading papers before they understand how to write a single rule. + +## 2. REQUIRED UPDATES & COMPLETENESS CHECK + +- **2.1. README.md Priority Fixes:** + 1. **Stack Clarity**: Explicitly define the role of `warp-core` vs `echo-app-core`. + 2. **Continuum Role**: Elevate its role as the "hot runtime" in the larger platform. + 3. **Actionable CLI**: Add a "Quick Start" section for determinism verification. + +- **2.2. Missing Standard Documentation:** + 1. **`METHOD.md`**: Created at root to align work doctrine across the monorepo. + 2. **`ADVANCED_GUIDE.md`**: Created at root to house theoretical doctrine and specs. + +- **2.3. Supplementary Documentation (Docs):** + - **Answer:** **Footprint Design Patterns**. A guide explaining common strategies for declaring read/write sets to maximize parallel throughput without causing conflict-storms. + +## 3. FINAL ACTION PLAN + +- **3.1. Recommendation Type:** **A. Incremental updates to the existing README and documentation.** (The core manifolds are now authoritative; they need pattern-level detail). + +- **3.2. Deliverable (Prompt Generation):** `Align VitePress docs with the new root manifests. Create 'docs/guide/footprint-patterns.md' explaining parallelization strategies. Document the 'Mr. Clean' panic-free doctrine in ADVANCED_GUIDE.md.` + +- **3.3. Mitigation Prompt:** `Update 'docs/index.md' to mirror the high-signal wording of the new root README. Create 'docs/guide/footprint-patterns.md' detailing the trade-offs between broad and narrow graph declarations. Add a 'Panic-Free Integrity' section to ADVANCED_GUIDE.md explaining the lints and patterns used to prevent runtime crashes in the kernel.` diff --git a/docs/audit/2026-04-11_ship-readiness.md b/docs/audit/2026-04-11_ship-readiness.md new file mode 100644 index 00000000..6743b886 --- /dev/null +++ b/docs/audit/2026-04-11_ship-readiness.md @@ -0,0 +1,26 @@ + + + +# AUDIT: READY-TO-SHIP ASSESSMENT (2026-04-11) + +## 1. QUALITY & MAINTAINABILITY ASSESSMENT (EXHAUSTIVE) + +1.1. **Technical Debt Score (1-10):** 2 - **Justification:** 1. **Complex Footprint Overlap Logic**: The spatial conflict detection in the scheduler is highly optimized but has high cognitive complexity. 2. **Manual Wesley Sync**: The generated code path still requires manual oversight rather than being a "set-and-forget" build step. 3. **Dual Float Strategy**: Maintaining both IEEE and Fixed-point paths creates a large testing surface for determinism. + +1.2. **Readability & Consistency:** - **Issue 1:** `warp-core` uses Lamport ticks but refers to them as "Ticks" while the renderer refers to them as "Frames." - **Mitigation Prompt 1:** `Standardize on 'Tick' for all causal coordinates and 'Frame' only for the final visual projection in the ScenePort.` - **Issue 2:** The "Mr. Clean" (panic-free) discipline is enforced via lints but not always explicitly documented in function signatures. - **Mitigation Prompt 2:** `Add '# Errors' sections to all public CasService/WarpCore methods detailing the stable error variants instead of relying on Result.` - **Issue 3:** The `MaterializationBus` reduction logic is spread across 8 built-in variants with minimal domain-level explanation. - **Mitigation Prompt 3:** `Create 'docs/spec/materialization-reduction.md' explaining the deterministic algebra behind Sum, Min, Max, and Consensus reduce operations.` + +1.3. **Code Quality Violation:** - **Violation 1: SRP (`SnapshotAccumulator`)**: It handles both state hashing and the Merkle-tree diff generation. - **Violation 2: SoC (`warp-cli`)**: The CLI handles both local testing and remote TTD host bridging. - **Violation 3: SRP (`ProvenanceStore`)**: The local implementation handles both file I/O and hash-locked indexing. + +## 2. PRODUCTION READINESS & RISK ASSESSMENT (EXHAUSTIVE) + +2.1. **Top 3 Immediate Ship-Stopping Risks (The "Hard No"):** - **Risk 1: Floating-Point Poisoning (High)**: If an external library uses standard `f32` in a core path, it will break determinism across platforms. - **Mitigation Prompt 7:** `Add a lint to scripts/ban-nondeterminism.sh that detects any direct usage of std::f32/f64 in non-WVP crates.` - **Risk 2: Merkle Hash Collision (Medium)**: BLAKE3 is strong, but the way sub-graphs are keyed in the snapshot accumulator needs a formal collision-avoidance audit. - **Mitigation Prompt 8:** `Conduct a 'Hash Collision Audit' of the Merkle Commit implementation, ensuring that type IDs and node IDs are salted before hashing to prevent cross-graph collisions.` - **Risk 3: OOM during Merkle Finalization (Low)**: Giant graphs could cause memory exhaustion during the recursive hash calculation. - **Mitigation Prompt 9:** `Implement a 'Streaming Snapshot Accumulator' that yields hashes incrementally rather than building the entire Merkle tree in memory.` + +2.2. **Security Posture:** - **Vulnerability 1: Side-Channel Data Leak**: The replay history contains every rejected counterfactual. If those patches contain PII, the history is a target. - **Mitigation Prompt 10:** `Implement 'Privacy Redaction' in the ProvenanceStore, allowing sensitive properties to be zeroed out after a worldline is sealed.` - **Vulnerability 2: Host Fingerprinting**: Deterministic math LUTs might theoretically leak CPU architecture details if not strictly bounds-checked. - **Mitigation Prompt 11:** `Audit all math LUT implementations for constant-time access and ensure no branching occurs based on input values.` + +2.3. **Operational Gaps:** - **Gap 1: Profiling Integration**: No built-in way to export Flamegraph data for rule-level bottleneck detection. - **Gap 2: Remote TTD Stability**: The bridge between Echo WASM and `warp-ttd` is still under active churn. - **Gap 3: CI Throughput**: DIND tests are slow and may block the release pipeline as seed counts increase. + +## 3. FINAL RECOMMENDATIONS & NEXT STEP + +3.1. **Final Ship Recommendation:** **YES, BUT...** (Harden the floating-point ban and audit Merkle collision safety). + +3.2. **Prioritized Action Plan:** - **Action 1 (High Urgency):** Enforce the `det_fixed` build profile by default. - **Action 2 (Medium Urgency):** Standardize on Wesley-generated contracts for TTD. - **Action 3 (Low Urgency):** Implement Merkle-tree memoization for deep graphs. diff --git a/docs/design/0003-dt-policy.md b/docs/design/0003-dt-policy.md new file mode 100644 index 00000000..8277b3f5 --- /dev/null +++ b/docs/design/0003-dt-policy.md @@ -0,0 +1,177 @@ + + + +# 0003 — Lock the dt policy + +_Ratify fixed timestep as a history-plane invariant: dt is fixed per +worldline, no committed tick carries its own dt, and wall-clock time +never enters semantic history._ + +Legend: KERNEL + +Depends on: + +- nothing + +## Why this cycle exists + +Echo's hardest open problem is canonical cross-worldline settlement. +The settlement backlog says Echo needs "one deterministic result, not +eventual convergence." If ticks can carry different durations, then +equal tick counts stop meaning equal simulated time, and +compare/braid/settle gets uglier fast. Issue #243 blocks older +time-travel inspector planning, so this is exactly the kind of foundational +invariant worth locking early instead of letting it leak everywhere +later. + +The code already leans this way. `warp_geom::Tick` documents "the +engine advances in integer ticks with a fixed `dt` per branch." The +planning docs say fixed timestep is simpler and more deterministic, +while variable dt introduces a new divergence class. The TT1 task +ties the dt decision to catch-up and wormhole behavior. This cycle +slams the door. + +The invariant is: dt is fixed per worldline. No committed tick carries +its own dt. Wall-clock time may exist for telemetry, rendering, pacing, +and I/O, but it does not enter semantic history as per-tick dt. + +This is a spec-only cycle. No runtime code. The deliverable is an +invariant document and normative cross-references from the existing +spec corpus. + +## Normative text + +The invariant document will contain these rulings: + +1. Every worldline has an immutable `tick_quantum` chosen at genesis. +2. Each committed tick advances simulation by exactly one + `tick_quantum`. +3. `dt` is not an admitted stream fact and is never stored per tick. +4. Catch-up means running 0, 1, or N fixed ticks in one host frame, + not "one larger tick." +5. All TTL, deadline, retry, and expiry semantics are + tick-denominated. +6. HostTime may influence simulation semantics only through a + recorded canonical decision (per TT0 HistoryTime/HostTime + classification). +7. Cross-worldline compare and settlement require identical + `tick_quantum`; otherwise reject in v1. + +## Human users / jobs / hills + +### Primary human users + +- Engine contributors implementing time-aware systems +- Game designers choosing time models for their simulations +- Debugger developers building fork/compare workflows + +### Human jobs + +1. Know that tick numbers are always comparable across worldlines + with the same `tick_quantum`. +2. Know that no code path needs to account for per-tick variable dt. +3. Know the boundary: wall-clock time is telemetry, not history. + +### Human hill + +A contributor can trust that tick N advances simulation by exactly +one `tick_quantum` on every worldline, without checking per-tick +metadata or consulting a compatibility matrix. + +## Agent users / jobs / hills + +### Primary agent users + +- Agents writing time-aware adapters or session protocol code +- Agents implementing strand/settlement specs downstream + +### Agent jobs + +1. Assume fixed dt when generating time-dependent code. +2. Never emit per-tick dt fields in provenance entries. +3. Gate cross-worldline operations on `tick_quantum` equality. + +### Agent hill + +An agent can assume tick-comparability across worldlines by comparing +a single genesis parameter (`tick_quantum`), without inspecting +individual ticks. + +## Human playback + +1. The human opens `docs/invariants/FIXED-TIMESTEP.md`. +2. The document states seven normative rulings (listed above). +3. The human asks: "are tick numbers comparable across these two + worldlines?" Answer: yes, if their `tick_quantum` is identical + (which it must be for compare/settle in v1). +4. The human asks: "can my adapter use wall-clock time?" Answer: yes, + for pacing and telemetry, but it must emit a canonical decision + record before the simulation consumes the result. +5. The human asks: "what does catch-up mean?" Answer: run N fixed + ticks, not one big tick. + +## Agent playback + +1. The agent reads the invariant document. +2. The agent confirms: no per-tick dt field exists in + `ProvenanceEntry` or `WorldlineTickPatchV1`. +3. The agent confirms: `tick_quantum` is a worldline-genesis + parameter, not a per-tick value. + +## Implementation outline + +1. Create `docs/invariants/` directory. +2. Write `docs/invariants/FIXED-TIMESTEP.md` with the seven normative + rulings, rationale, and consequences. +3. Cross-reference from SPEC-0004 (worldlines) — add a one-line + normative reference to the invariant. +4. Cross-reference from the current runtime/invariant docs — note that the hot + runtime's time model is fixed-quantum. +5. Verify that `warp_geom::Tick` doc comment is consistent with the + invariant (it already says "fixed `dt` per branch"). +6. Update the strand-contract and strand-settlement backlog items + to note that cross-worldline operations require identical + `tick_quantum`. + +## Tests to write first + +- Shell assertion: `docs/invariants/FIXED-TIMESTEP.md` exists. +- Shell assertion: the invariant document contains "MUST" and + "tick_quantum". +- Shell assertion: the invariant document contains all seven rulings + (grep for key phrases: "immutable tick_quantum", "not an admitted + stream fact", "never stored per tick", "tick-denominated", + "canonical decision", "identical tick_quantum"). +- Shell assertion: SPEC-0004 contains a reference to the invariant. +- Shell assertion: no file in `crates/` contains "variable.dt" or + "variable_dt" or "dt_stream" (negative test — the concept does not + exist in code). + +## Risks / unknowns + +- **Risk: future use case needs variable dt.** If a real consumer + appears, the invariant can be relaxed in a future cycle with + explicit design work. But we do not design for hypothetical + requirements. Fixed until proven otherwise. +- **Risk: tick_quantum choice is premature.** The invariant declares + that `tick_quantum` exists and is immutable, not what its value is. + Actual quantum values are a runtime configuration concern for each + application. + +## Postures + +- **Accessibility:** Not applicable — spec-only, no UI. +- **Localization:** Not applicable — internal invariant document. +- **Agent inspectability:** The invariant is a set of normative + statements parseable by grep. + +## Non-goals + +- Choosing a specific `tick_quantum` value (application concern). +- Runtime enforcement of the invariant (structural — no variable-dt + mechanism exists to enforce against). +- HistoryTime/HostTime field classification table (TT0 scope, not + this cycle — but this cycle establishes the boundary that TT0 + classifies against). +- The strand contract itself (next cycle). +- Settlement semantics (future cycle after strand contract). diff --git a/docs/design/0004-strand-contract.md b/docs/design/0004-strand-contract.md new file mode 100644 index 00000000..3c85de64 --- /dev/null +++ b/docs/design/0004-strand-contract.md @@ -0,0 +1,548 @@ + + + +# 0004 — Strand contract + +_Define the strand as a first-class relation in Echo with exact fields, +invariants, fork-basis semantics, and TTD mapping._ + +Legend: KERNEL + +Depends on: + +- [0003 — dt-policy](./0003-dt-policy.md) + +## Why this cycle exists + +Echo can fork worldlines but has no concept of the relationship +between them. `ProvenanceStore::fork()` creates a prefix-copy and +rewrites parent refs, but once forked, the child worldline is just +another worldline — there is no way to ask "what was this forked +from?", "is this a speculative lane?", or "what strands exist for +this source lane?" + +git-warp has a full strand implementation. warp-ttd Cycle D already +builds strand topology into the TUI (`LaneKind::STRAND`, +`LaneRef.parentId`, create/inspect/compare/drop). Echo needs to surface +strands through the TTD adapter, and it needs the strand contract to +do so honestly. + +The strand contract does not require settlement in the same cycle. It +does need to stop lying about scope. + +`git-warp` is currently ahead on speculative-lane richness. It already +has durable strands, braid-capable reads, comparison workflows, and a +clearer path toward settlement. Echo does not need to copy that engine +internally, but if parity is the real target then Echo's strand plan +must be read as a **bootstrap slice**, not the endpoint. + +This cycle therefore has two jobs: + +1. define the exact bootstrap contract Echo can land now +2. make explicit which additional capabilities are required before Echo + can claim conceptual parity with `git-warp` + +Settlement remains a separate spec, but the strand contract must leave +an honest path to settlement, braid geometry, and shared debugger +publication instead of freezing a too-small model as if it were final. + +## Scope posture + +This packet defines the **bootstrap strand contract** under the runtime +ontology now governed by +[0009 — Strand Runtime Graph Ontology](./0009-strand-runtime-graph-ontology.md). + +That bootstrap is sufficient for: + +- fork +- speculative authoring through ordinary ingress + `super_tick()` +- precise source-lane provenance +- TTD lane typing and parentage +- basic compare workflows + +It is **not** sufficient for full parity with `git-warp`. + +Parity requires follow-on work in at least four areas: + +1. **Braid geometry** + - support pins or equivalent multi-lane read composition + - participating-lane publication + - honest local plurality for debugger surfaces +2. **Settlement** + - compare + - plan + - import + - conflict artifact publication +3. **Retention policy** + - session-scoped ephemerality is acceptable for bootstrap + - it is not the only valid long-term model +4. **Shared observer/debugger publication** + - neighborhood core + - reintegration detail + - receipt shell + +Nothing in the bootstrap contract should block those later capabilities. + +## Bootstrap normative definitions + +### Strand + +A strand is a named speculative execution lane rooted at one exact +admissible source-lane coordinate. It is a relation over a child +worldline, not a separate substrate. + +```text +Strand { + strand_id: StrandId, + fork_basis_ref: ForkBasisRef, + child_worldline_id: WorldlineId, + writer_heads: Vec, + support_pins: Vec, +} +``` + +There is no `StrandLifecycle` field. A strand either exists in the +registry (live) or does not (dropped). Its materialised state is +obtained solely from the child worldline frontier. Operational control +comes from ordinary writer-head eligibility and playback posture under +Echo's single `super_tick()` law, not from a strand-specific ticking +subsystem. + +### StrandId + +Domain-separated hash newtype (prefix `b"strand:"`), following the +`HeadId`/`NodeId` pattern. + +### ForkBasisRef + +The exact admissible coordinate the strand was forked from. Immutable +after creation. + +```text +ForkBasisRef { + source_lane_id: LaneId, + fork_tick: WorldlineTick, + commit_hash: Hash, + boundary_hash: Hash, + provenance_ref: ProvenanceRef, +} +``` + +**Coordinate semantics (exact):** + +- `source_lane_id` names the precise source lane. In the first runtime + cut this may point to a canonical worldline or to a speculative lane + resolved at one exact coordinate. +- `fork_tick` is the **last included tick** in the copied prefix. The + child worldline contains entries `0..=fork_tick`. The child's next + appendable tick is `fork_tick + 1`. +- `commit_hash` is the commit hash **at `fork_tick`** for the source + coordinate. +- `boundary_hash` is the **output boundary hash** at `fork_tick` — + the state root after applying the patch at `fork_tick`. This is the + hash of the state the child worldline begins diverging from. +- `provenance_ref` carries the same coordinate as a `ProvenanceRef` + for substrate-native lookups. +- All five fields refer to the **same admissible coordinate**. If any + field disagrees with the provenance store, construction MUST fail. + +### SupportPin + +A read-only reference to another strand's materialized state at a +specific tick. This is braid geometry — the strand can read from +pinned support strands without merging them. + +```text +SupportPin { + strand_id: StrandId, + worldline_id: WorldlineId, + pinned_tick: WorldlineTick, + state_hash: Hash, +} +``` + +**Bootstrap note:** the first strand cut left `support_pins` empty to avoid +premature braid semantics. That restriction is superseded by +[0007 — Braid geometry and neighborhood publication](./0007-braid-geometry-and-neighborhood-publication.md), +which permits validated non-empty support pins as read geometry and +publication inputs. Under `0009`, however, support pins remain derived / +cache truth in the first cut rather than authoritative graph ontology. + +If Echo stopped at the empty-only posture, it would still lag `git-warp` on +braid-capable speculative work. + +### Registry ordering + +`StrandRegistry` is a `BTreeMap`. Iteration order +is by `StrandId` (lexicographic over the hash bytes). This is +deterministic but not semantically meaningful. + +`list_strands_by_source_lane(source_lane_id)` returns results filtered by +`fork_basis_ref.source_lane_id`, ordered by `StrandId`. + +## Invariants + +- **INV-S1 (Immutable fork basis):** A strand's `fork_basis_ref` MUST NOT + change after creation. +- **INV-S2 (Own heads):** A strand's child worldline MUST NOT share + writer heads with its source lane. Head keys are created fresh for + the child worldline. +- **INV-S3 (Bootstrap session scope):** A strand MUST NOT outlive the + session that created it in the bootstrap landing. Long-term retention + policy remains an explicit follow-on design axis, not a semantic truth + about what a strand is. +- **INV-S4 (Single tick law):** A strand advances only through ordinary + intent admission under Echo's global `super_tick()` path. No + strand-specific tick path is authoritative. +- **INV-S5 (Complete fork basis):** `fork_basis_ref` MUST pin source lane + ID, fork tick, commit hash, boundary hash, and provenance ref. All + fields MUST agree with the provenance store at construction time. +- **INV-S6 (Inherited quantum):** A strand inherits its parent's + `tick_quantum` at fork time (per FIXED-TIMESTEP invariant). No + strand can change its quantum. +- **INV-S7 (Distinct worldlines):** `child_worldline_id` MUST NOT + equal the source-basis carrier. A strand is always represented by a + distinct child worldline, even when its source lane was itself + speculative. +- **INV-S8 (Head ownership):** Every key in `writer_heads` MUST + belong to `child_worldline_id`. +- **INV-S9 (Validated support pins):** support pins, when present, MUST be + live, correctly resolved, non-duplicated, and read-only. +- **INV-S10 (Clean drop):** After `drop_strand`, no runnable heads + for the child worldline MUST remain in the `PlaybackHeadRegistry`. + +## Bootstrap drop semantics + +The bootstrap landing uses **hard-delete**: + +- `drop_strand(strand_id)` removes the strand's writer heads from + `PlaybackHeadRegistry`, removes the child worldline from + `WorldlineRegistry`, removes the child worldline's history from + the provenance store, and removes the strand from + `StrandRegistry`. +- There is no Dropped tombstone state. After drop, `get(strand_id)` + returns `None`. +- `drop_strand` returns a `DropReceipt` containing the `strand_id`, + `child_worldline_id`, and the tick the child had reached at drop + time. This is the only record that the strand existed. +- TTD can log the `DropReceipt` if it needs to show "this strand + existed and was dropped" during the session. + +## Bootstrap create/drop atomicity + +### create_strand + +Construction follows a fixed order. If any step fails, all prior +steps are rolled back: + +1. Validate that the requested source lane / tick coordinate is admissible + and capture the `ForkBasisRef` fields. +2. Call `ProvenanceStore::fork()` to create the child worldline. +3. Create one or more new `WriterHead`s for the child worldline under the + ordinary writer-head control model. +4. Register the head in `PlaybackHeadRegistry`. +5. Register the strand in `StrandRegistry`. + +Rollback on failure at step N: + +- Step 2 fails: nothing to roll back (validation only in step 1). +- Step 3 fails: remove the forked worldline from provenance. +- Step 4 fails: remove the forked worldline from provenance. +- Step 5 fails: remove head from registry, remove forked worldline + from provenance. + +### drop_strand + +Drop follows a fixed order. Each step is independent (no rollback): + +1. Remove writer heads from `PlaybackHeadRegistry`. +2. Remove child worldline from `WorldlineRegistry`. +3. Remove child worldline history from provenance store. +4. Remove strand from `StrandRegistry`. +5. Return `DropReceipt`. + +If the strand does not exist, return an error. If intermediate +removal fails (e.g., worldline already removed), log a warning and +continue — drop is best-effort cleanup of an ephemeral resource. + +## Bootstrap writer-head cardinality + +v1 creates exactly one writer head per strand. `writer_heads` is a +`Vec` to support future multi-head strands, but v1 +always produces a vec of length 1. + +Again, this is a bootstrap constraint, not a statement that a strand is +inherently single-head forever. + +## Parity target that bootstrap must not block + +### 1. Strands must grow beyond singleton publication + +The bootstrap contract is good enough to say: + +- this is a speculative lane +- it came from this source basis coordinate +- it has its own writer head + +That is not enough for mature debugger or comparison work. + +For parity, Echo eventually needs a first-class local-site publication +path that can say: + +- which lanes participate in the current local site +- whether the site is singleton or plural +- what the nearby alternatives are + +The current strand contract should therefore be read as the minimal lane +identity foundation, not the whole neighborhood story. + +### 2. Braid geometry is required for parity + +`git-warp` already treats braid as a real composite read presentation. + +Echo does not need to implement the full final braid model in this +cycle, but it does need to keep the door open for: + +- read-only support overlays +- explicit support-pin mutation APIs +- participating-lane publication +- observer/debugger surfaces that can inspect more than one lane at a + local site + +The old bootstrap posture of keeping `support_pins` empty was acceptable only +because the next cycle made braid geometry real. That follow-on now exists as +[0007 — Braid geometry and neighborhood publication](./0007-braid-geometry-and-neighborhood-publication.md). + +### 3. Settlement must remain separate from braid + +The current split is correct: + +- braid is geometry +- settlement is history/import/conflict law + +But parity requires both, not just one. + +Echo needs: + +- braid-capable speculative reads +- compare / plan / import / conflict artifacts + +That is why `KERNEL_strand-settlement` remains required even after this +packet lands. + +### 4. Retention must become policy, not essence + +Session-scoped strands are a sensible bootstrap safety posture. + +They should not harden into the theory as if "strands are ephemeral" +were an essential truth. For parity with `git-warp`, Echo needs a +clearer retention policy axis: + +- session-scoped +- lease-scoped +- or durable + +The current bootstrap may choose the first. The type/theory should not +pretend the others are impossible. + +The same distinction applies to debugger work. A TTD observation does +not itself create a strand. An explicit "continue from here" or "what +if?" action does. In Echo v1, that debugger-created strand should be +understood as session-scoped scratch by default. Durable author-only +retention is a later policy extension, not something observation gets +for free. + +### 5. Shared debugger publication must become explicit + +Even after bootstrap strands land, Echo will still not be aligned if the +host adapter has to invent: + +- neighborhood core +- reintegration detail +- receipt shell + +Strands therefore need to feed the later Continuum-aligned publication +boundaries, not remain only an Echo-local kernel feature. + +## Human users / jobs / hills + +### Primary human users + +- Debugger users exploring "what if?" scenarios +- Engine contributors implementing time travel features +- Game designers testing alternative simulation paths + +### Human jobs + +1. Fork a strand from any admissible lane/tick coordinate. +2. Author speculative intents against the child worldline. +3. Let `super_tick()` advance the strand through the ordinary scheduler. +4. Compare strand state against its source basis. +5. Drop the strand when done. + +### Human hill + +A debugger user can fork a speculative lane from any admissible point in +simulation history and explore it without affecting the source lane. + +## Agent users / jobs / hills + +### Primary agent users + +- TTD host adapter surfacing strand state to warp-ttd +- Agents implementing settlement or time travel downstream + +### Agent jobs + +1. Create a strand with a well-defined `fork_basis_ref`. +2. Register strand heads in the head registry. +3. Report strand type and parentage to the TTD adapter + (`LaneKind::STRAND`, `LaneRef.parentId`). +4. Enumerate strands derived from a common source lane. + +### Agent hill + +An agent can create, inspect, and drop strands through a typed API, +author intents through ordinary ingress, and programmatically surface +strand topology to TTD. + +## Human playback + +1. The human calls `create_strand(source_lane, fork_tick)`. +2. A new strand is returned with a `StrandId`, `fork_basis_ref` + pinning the exact fork coordinate (all five fields verified against + provenance), and a child worldline with its own writer head. +3. The human submits speculative intents for the child worldline. The + source lane is unaffected. +4. Echo advances the runtime through `super_tick()`. The strand moves + only because the ordinary scheduler admitted those intents. +5. The human inspects the strand's child worldline state at its + current tick and compares it to the source basis. +6. The human drops the strand. A `DropReceipt` is returned. The + child worldline, its heads, and its provenance are gone. + `get(strand_id)` returns `None`. + +## Agent playback + +1. The agent calls the strand creation API. +2. The returned `Strand` struct contains: `strand_id`, `fork_basis_ref` + (with `provenance_ref`), `child_worldline_id`, `writer_heads` + (length 1), `support_pins` (empty until explicitly pinned through braid + geometry APIs). +3. The agent maps `strand_id` to `LaneKind::STRAND` (type, not + lifecycle) and `fork_basis_ref.source_lane_id` to + `LaneRef.parentId`. +4. The agent calls `list_strands_by_source_lane(source_lane_id)` and + receives all live strands derived from that source lane, ordered by + `StrandId`. +5. The agent drops the strand. `get(strand_id)` returns `None`. + The `DropReceipt` carries the strand_id, child worldline, and + final tick. + +## Bootstrap implementation outline + +1. Define `StrandId` as a domain-separated hash newtype (prefix + `b"strand:"`), following the `HeadId`/`NodeId` pattern. +2. Define `ForkBasisRef`, `SupportPin`, `DropReceipt`, and `Strand` + structs in a new `crates/warp-core/src/strand.rs` module. +3. Define `StrandRegistry` — `BTreeMap` with + `create`, `get`, `contains`, `list_by_source_lane`, and `drop` + operations. Session-scoped, not persisted. +4. Implement `create_strand` with the five-step construction + sequence and rollback on failure. +5. Implement `drop_strand` with the five-step hard-delete sequence + returning a `DropReceipt`. +6. Implement `list_strands_by_source_lane(source_lane_id)` — filter by + `fork_basis_ref.source_lane_id`, ordered by `StrandId`. +7. Write `docs/invariants/STRAND-CONTRACT.md` with the ten + invariants (INV-S1 through INV-S10). + +## Required follow-on work for parity + +This packet is only correct if the next queue makes the missing +capabilities explicit. + +Required follow-ons: + +1. braid geometry and neighborhood publication +2. settlement / compare / import / conflict artifacts +3. retention and capability policy for timeline mutation +4. Continuum/Wesley publication of strand-facing shared observer nouns + +## Tests to write first + +- Unit test: `create_strand` returns a strand with correct + `fork_basis_ref` fields — all five fields match the source + coordinate at `fork_tick`. +- Unit test: strand's child worldline has its own `WriterHeadKey`, + distinct from any head on the source lane (INV-S2). +- Unit test: the only way to advance a strand is through ordinary + ingress + `super_tick()` (INV-S4). +- Unit test: strand-authored intents advance the child worldline + without affecting the source lane frontier. +- Unit test: `list_strands_by_source_lane` returns strands matching the + source lane and does not return strands from other source lanes. +- Unit test: `drop_strand` removes the child worldline, its heads, + and its provenance. `get(strand_id)` returns `None`. No heads for + the child worldline remain in `PlaybackHeadRegistry` (INV-S10). +- Unit test: `drop_strand` returns a `DropReceipt` with the correct + `strand_id`, `child_worldline_id`, and final tick. +- Unit test: `child_worldline_id` is distinct from the source lane's + live worldline frontier carrier (INV-S7). +- Unit test: new strands start with `support_pins` empty until explicitly + pinned. +- Unit test: support pins validate live targets, reject duplicates/self pins, + and remain read-only (INV-S9). +- Unit test: `create_strand` fails and rolls back if `fork_tick` + does not exist in the source worldline. +- Shell assertion: `docs/invariants/STRAND-CONTRACT.md` exists and + contains all ten invariant codes (INV-S1 through INV-S10). + +## Risks / unknowns + +- **Risk: provenance removal API.** `LocalProvenanceStore` has no + `remove_worldline` method. This cycle must add one, scoped to + ephemeral strand cleanup only. The removal MUST NOT affect other + worldlines that reference the dropped child through + `ProvenanceRef` parent links — those refs become dangling but are + structurally harmless (the coordinate they point to no longer + resolves, which is the correct behavior for a dropped strand). +- **Risk: head registry coupling.** `PlaybackHeadRegistry` is + engine-global, ordered canonically by `(worldline_id, head_id)`. + Strand heads are inserted into this global registry. The risk is no + longer accidental live scheduling through a strand-specific gate; it + is accidental over-capability. Tests must prove that strand heads can + author only for their child worldline and participate in scheduling + only through ordinary ingress + `super_tick()`. +- **Unknown: multi-head strands.** v1 creates one head per strand. + Future cycles may create multiple. The vec is correct but the + cardinality-1 assumption should be documented and tested. +- **Unknown: retention posture beyond bootstrap.** Session-scoped + deletion is simple, but parity with `git-warp` may eventually require + explicit durable or lease-scoped strands. That decision should be made + as policy, not smuggled in as type essence. +- **Unknown: braid publication shape.** `support_pins` is enough to + avoid a breaking struct rewrite, but not enough to define how plural + local sites should publish into shared debugger contracts. + +## Postures + +- **Accessibility:** Not applicable — internal API, no UI. +- **Localization:** Not applicable — internal types. +- **Agent inspectability:** All strand fields are public and + serializable. `StrandRegistry` supports enumeration with + documented ordering. The TTD mapping is type-to-type (`StrandId` + → `LaneKind::STRAND`, `fork_basis_ref.source_lane_id` → + `LaneRef.parentId`), not lifecycle-to-lifecycle. + +## Non-goals + +- Settlement semantics (KERNEL_strand-settlement, future cycle). +- Full braid geometry implementation in this cycle. Bootstrap alone left + `support_pins` empty, but that posture was not the endpoint. +- Strand persistence across sessions (v1 is ephemeral). +- Any strand-specific execution model separate from ordinary ingress + + `super_tick()`. +- TTD adapter implementation (this cycle defines the mapping; the + adapter is PLATFORM_echo-ttd-host-adapter). +- Multi-head strand creation (v1 creates exactly one head). diff --git a/docs/design/0005-echo-ttd-witness-surface.md b/docs/design/0005-echo-ttd-witness-surface.md new file mode 100644 index 00000000..d031540e --- /dev/null +++ b/docs/design/0005-echo-ttd-witness-surface.md @@ -0,0 +1,340 @@ + + + +# 0005 — Echo TTD witness surface + +_Define how Echo's current runtime objects map to `warp-ttd` neighborhood core, reintegration detail, and receipt shell._ + +Legend: [PLATFORM](../method/legends/PLATFORM.md) + +Depends on: + +- [0004 — Strand contract](./0004-strand-contract.md) +- external `warp-ttd` design packets: + - `0016-local-neighborhood-browser` + - `0017-neighborhood-protocol-shapes` + +## Why this cycle exists + +Echo already has most of the raw runtime truth a `warp-ttd` host adapter +needs: + +- stable identity and routing via `WorldlineId`, `WriterHeadKey`, + ingress targets, playback mode, and scheduler status +- explicit read contracts via `ObservationRequest` and + `ObservationArtifact` +- committed history via `ProvenanceEntry` +- per-candidate tick outcomes via `TickReceipt` +- recorded truth outputs via `FinalizedChannel` + +What Echo does **not** yet have is one honest object for the local site a +debugger wants to inspect. The older backlog note +`PLATFORM_echo-ttd-host-adapter.md` assumed Echo could map directly into +`PlaybackFrame`, `ReceiptSummary`, and `EffectEmissionSummary`. That is too +flat now. + +`warp-ttd` has moved to a cleaner model: + +- neighborhood core = law-bearing site truth +- reintegration detail = seam anchors, obligations, and evidence +- receipt shell = runtime/provenance explanation around the site + +This cycle defines how Echo maps onto that ladder and names the missing +pieces. Without this, the eventual host adapter will either lie by flattening +everything into receipt-shaped sludge or block forever waiting for perfect +runtime objects. + +## Human users / jobs / hills + +### Primary human users + +- runtime contributors implementing the Echo host adapter +- debugger contributors making `warp-ttd` speak honestly across hosts +- future app authors depending on replay-safe debugger semantics + +### Human jobs + +1. Inspect one Echo coordinate and know what counts as core site truth versus + runtime explanation. +2. Build a host adapter without guessing which Echo object should populate + which `warp-ttd` layer. + +### Human hill + +A human can point at an Echo runtime object and say whether it belongs in +neighborhood core, reintegration detail, or receipt shell without inventing +new folklore. + +## Agent users / jobs / hills + +### Primary agent users + +- agents generating or auditing the Echo host adapter +- agents comparing Echo and `git-warp` debugger surfaces + +### Agent jobs + +1. Programmatically classify Echo runtime objects into core, reintegration, or + shell layers. +2. Detect where the adapter must synthesize shape versus where Echo must grow a + first-class runtime object. + +### Agent hill + +An agent can inspect Echo's runtime types and determine which `warp-ttd` +neighborhood fields are already grounded in repo truth and which are still +missing. + +## Human playback + +1. The human reads this cycle while looking at Echo runtime code. +2. The document names concrete source objects for each debugger layer. +3. The human can decide what to synthesize now and what must be promoted into + runtime truth before the adapter ships. + +## Agent playback + +1. The agent reads `ObservationArtifact`, `ProvenanceEntry`, `TickReceipt`, + `FinalizedChannel`, `CursorReceipt`, and `SchedulerStatus`. +2. The document classifies them against the neighborhood ladder. +3. The agent can implement the adapter without flattening core truth into + receipt shell. + +## Design decision + +Echo should map its existing runtime surfaces onto the `warp-ttd` neighborhood +ladder as follows: + +- `NeighborhoodCoreSummary` is derived primarily from Echo's native + `NeighborhoodCore` publication, with `ObservationArtifact` retained as + revelation / shell context rather than the source of local-site truth. +- `ReintegrationDetailSummary` is derived primarily from `TickReceipt`, + provenance parentage, and materialization conflict structure. +- `ReceiptShellSummary` is derived from `ProvenanceEntry`, finalized channel + outputs, cursor/session receipts, scheduler metadata, and other explanatory + runtime context. + +Echo now does expose one first-class local-site object: + +- `NeighborhoodSite` as Echo-native publication truth +- `NeighborhoodCore` as the shared neighborhood-core family projection + +What Echo still does **not** yet expose is a native reintegration-core object +or a full nearby-alternative neighborhood browser. Those remain explicit host +or follow-on runtime gaps and should not be guessed silently. + +## TTD observation versus counterfactual creation + +This packet needs one explicit separation because `warp-ttd` will +eventually expose both read-side inspection and write-side speculative +forks. + +Observation alone does not create new Echo history. A debugger session, +cursor move, or read against `ObservationArtifact` remains revelation +over existing runtime truth. + +If a user explicitly asks to continue from an earlier coordinate or +explore a counterfactual, that is a separate act: create a strand with +an exact `fork_basis_ref` and route speculative intents through the +ordinary scheduler. + +For Echo v1, the honest posture is: + +- debugger-created strands are session-scoped scratch or minimally + retained speculative lanes +- they are not silently promoted into shared admitted history +- any future durable author-only retention must record creator, tool or + session origin, exact fork basis, and retention or revelation posture + +This keeps the adapter honest about the difference between a reading +surface and a fork surface while still matching the three-tier thinking +room doctrine from Paper VII. + +## Runtime mapping + +### 1. Neighborhood core + +Neighborhood core is the minimum law-bearing site object. In Echo today, the +best grounding is: + +- `ObservationArtifact.resolved` + - stable observed coordinate + - worldline identity + - resolved tick + - commit/global tick stamps + - commit/state root commitments +- `HeadObservation` / `SnapshotObservation` + - head and snapshot truth at the chosen coordinate +- `WriterHeadKey` + - exact head identity when the observed site is head-relative +- strand metadata from `0004` + - when the observed worldline is a strand, this gives parentage and lane kind + +This is enough to ground: + +- `siteId` as an adapter-defined stable identifier over observed coordinate plus + host/lane scope +- `coordinate` +- `primaryWorldlineId` +- `primaryLaneId` +- `headId` when applicable +- `frameIndex` / debugger-local coordinate mapping + +This is **not** enough to ground the full neighborhood core shape by itself. + +Missing today: + +- explicit alternative set near the current site +- richer nearby-neighborhood enumeration beyond the current local site + +Echo now closes the first neighborhood-core gap natively through +`NeighborhoodSite` and its shared `NeighborhoodCore` projection: + +- explicit participating lane set +- explicit singleton-vs-plural local outcome at one observed site + +So the initial Echo adapter no longer needs to invent neighborhood core from +raw `ObservationArtifact` alone. It should consume the native +`NeighborhoodCore` projection first, then add reintegration and shell detail +without redefining the core. + +### 2. Reintegration detail + +Reintegration detail is the first protocol cash-out of the seam-bearing core. +Echo does not have `R_core` as one object, but several current objects already +carry parts of it. + +Best current grounding: + +- `TickReceipt.entries` + - per-candidate applied/rejected outcomes +- `TickReceipt.blocked_by(idx)` + - explicit blocking causality for rejected candidates +- `TickReceiptRejection` + - current seam-law failure reason (`FootprintConflict`) +- `ProvenanceEntry.parents` + - explicit parent lineage at the committed coordinate +- `ProvenanceEntry.head_key` + - producing writer head for local commits +- `FinalizeReport.errors` + - deterministic materialization conflict structure when channel finalization + fails + +This means Echo already has partial grounding for: + +- seam anchors (`J`) via parent refs, producing head, and observed coordinate +- compatibility obligations (`K`) via receipt dispositions and finalize law +- compatibility evidence (`V`) via blocker indices and deterministic lineage + +What is still missing is a first-class **local reintegration object**: + +- no single seam-anchor record +- no normalized obligation/evidence surface +- no explicit distinction between reintegration core and explanatory shell + +So the adapter should publish reintegration detail as a synthesized summary +layer over receipts/provenance, not pretend Echo already has a native +`R_core` DTO. + +### 3. Receipt shell + +Receipt shell is where Echo is strongest today. + +Best current grounding: + +- `ProvenanceEntry` + - `commit_global_tick` + - `head_key` + - `parents` + - `event_kind` + - `expected` + - `patch` + - `outputs` + - `atom_writes` +- `FinalizedChannel` + - recorded truth payloads that survived deterministic materialization +- `CursorReceipt` + - session/cursor/worldline/tick/commit context +- `TruthFrame` + - cursor receipt plus channel payload and hash +- `SchedulerStatus` + - current runtime work/scheduler state + +This is explanatory/runtime shell. It is critical for inspection, but it +should not be allowed to redefine neighborhood core or seam detail. + +## Adapter doctrine + +The Echo host adapter should follow these rules: + +1. Do not infer local neighborhood plurality from shell objects. +2. Do not treat `ProvenanceEntry` as if it were already a neighborhood core. +3. Do not treat `TickReceipt` as if it were already a full reintegration + object. +4. Publish narrow, truthful neighborhood core first. +5. Publish synthesized reintegration detail second. +6. Attach provenance/materialization/runtime context as receipt shell only. + +## Immediate implications + +### What Echo can support now + +- coordinate-grounded site inspection +- head/worldline identity +- replay-safe commit/snapshot observation +- per-tick candidate outcome inspection +- deterministic blocking-causality inspection +- finalized recorded-truth payload inspection + +### What Echo cannot support honestly yet + +- full participating-lane neighborhood enumeration +- explicit local alternative set +- native reintegration-core DTO + +## Implementation outline + +1. Reconcile Echo's TTD protocol consumption with canonical `warp-ttd` + protocol ownership. +2. In the eventual Echo host adapter, consume Echo's native + `NeighborhoodCore` publication first, with `ObservationArtifact` retained as + shell/revelation context rather than as the source of core site truth. +3. Synthesize `ReintegrationDetailSummary` from `TickReceipt`, + `ProvenanceEntry.parents`, and materialization conflict data. +4. Keep `ProvenanceEntry`, `FinalizedChannel`, `CursorReceipt`, `TruthFrame`, + and `SchedulerStatus` in receipt shell lanes. +5. File or implement follow-on runtime work when the adapter needs explicit + nearby alternatives or richer reintegration structure. + +## Tests to write first + +- adapter test proving Echo's native `NeighborhoodCore` publication reaches the + host boundary without fake alternatives +- adapter test proving `TickReceipt.blocked_by()` becomes reintegration detail, + not receipt shell-only text +- adapter test proving finalized channels and scheduler status stay in shell and + do not mutate core outcome + +## Risks / unknowns + +- Echo may need a first-class local-site runtime object sooner than desired if + synthesized reintegration detail becomes too lossy. +- Materialization conflict data may belong partly in reintegration detail and + partly in shell depending on channel law class. +- Strand support may be required before Echo can publish honest participating + lane sets. + +## Postures + +- **Accessibility:** not applicable; this cycle defines data boundaries, not a + rendered interface. +- **Localization:** not applicable; protocol/runtime naming only. +- **Agent inspectability:** explicit goal; this cycle exists to separate + law-bearing core from explanatory shell for machine consumers. + +## Non-goals + +- Implement the Echo `warp-ttd` host adapter in this cycle. +- Unify every Echo runtime DTO with `warp-ttd` in one pass. +- Introduce a new local-site runtime object in Echo before the adapter proves + one is necessary. diff --git a/docs/design/0006-echo-continuum-alignment.md b/docs/design/0006-echo-continuum-alignment.md new file mode 100644 index 00000000..1bef9907 --- /dev/null +++ b/docs/design/0006-echo-continuum-alignment.md @@ -0,0 +1,371 @@ + + + +# 0006 — Echo Continuum alignment + +_Decide what Echo must change so Continuum tools can consume one honest shared +observer/debugger noun stack without flattening Echo’s runtime-specific truth._ + +Legend: [PLATFORM](../method/legends/PLATFORM.md) + +Depends on: + +- [0004 — Strand contract](./0004-strand-contract.md) +- [0005 — Echo TTD witness surface](./0005-echo-ttd-witness-surface.md) +- external Continuum packets: + - `0001` through `0015` + +## Why this cycle exists + +Echo is older than the current Continuum theory stack. + +That history shows up in the repo: + +- some Echo runtime nouns pre-date the later lane / braid / witness language +- the runtime schema freeze in `schemas/runtime/` explicitly defers Wesley-owned + generation +- the browser/TTD bridge path was prototyped locally before `warp-ttd` became + the explicit observer/control plane + +Those are not mistakes. They are historical truth. + +The problem is interoperability pressure. If Continuum tools must work against +Echo and `git-warp`, then Echo cannot rely on "adapter folklore" forever. It +has to publish the shared observer/debugger nouns honestly enough that Wesley +and `warp-ttd` can consume them without reinterpreting Echo from the outside. + +This cycle answers one narrow question: + +**What must change in Echo to align with Continuum, and what should stay +Echo-local?** + +## Design decision + +Echo should **not** be flattened into Continuum theory terms everywhere. + +Instead, Echo should align by making a cleaner split between: + +1. **Echo-local engine/runtime nouns** +2. **shared Continuum observer/debugger contract nouns** +3. **adapter-only temporary synthesis** + +The goal is: + +- keep Echo’s hot-runtime truths where they belong +- stop using host adapters as permanent normalization swamps +- publish the same top-level observer/debugger nouns that Continuum tools will + also expect from `git-warp` + +## What should stay Echo-local + +These are real Echo-specific runtime truths and should not be promoted just +because they matter: + +- `GlobalTick` as a scheduler-global cycle stamp +- `SchedulerStatus` +- ingress routing and admission policy (`IngressTarget`, `InboxPolicy`) +- `WriterHeadKey` +- head eligibility / disposition +- materialization/finalization law details +- runtime-local control-plane types that only Echo needs + +These are part of the Echo engine. They may be surfaced to tools, but they are +not the minimal shared Continuum contract. + +## What Echo should publish into the shared Continuum surface + +Echo should converge on the same observer/debugger-facing noun stack that +Continuum tools will want from both engines. + +The important shared targets are: + +- lane / worldline / strand identity at the observer boundary +- coordinate / frame truth +- admission outcome kind +- neighborhood core +- reintegration detail +- receipt shell +- effect emission +- delivery observation +- eventually observer trace + +The key idea is: + +- the internal engine may stay Echo-shaped +- the published observer/debugger contract should not feel like a different + religion from `git-warp` + +That includes one shared lawful outcome family at the publication boundary: + +- `Derived` +- `Plural` +- `Conflict` +- `Obstruction` + +Echo does not need to make every local subsystem emit all four outcomes +immediately. It does need to publish the same top-level outcome kind whenever a +shared observer/debugger surface depends on that distinction. + +## Current Echo misalignment + +### 1. No first-class local site object + +`0005` already established that Echo can ground a narrow site from: + +- `ObservationArtifact` +- `HeadObservation` / `SnapshotObservation` +- `WriterHeadKey` +- strand metadata + +But Echo still lacks a first-class local-site object carrying: + +- participating lanes +- local outcome +- nearby alternatives + +That means the current adapter path can publish only a singleton or narrow site +summary. It cannot honestly support the fuller neighborhood model Continuum now +expects. + +### 2. Reintegration truth is present but still scattered + +Echo has strong raw ingredients: + +- `TickReceipt.entries` +- `TickReceipt.blocked_by(idx)` +- `TickReceiptRejection` +- `ProvenanceEntry.parents` +- `ProvenanceEntry.head_key` +- `FinalizeReport.errors` + +But those pieces are not yet normalized into a first-class reintegration core. + +Continuum now distinguishes: + +- seam anchors +- compatibility obligations +- compatibility evidence + +Echo still makes consumers reconstruct that split from multiple runtime objects. + +### 3. Receipt shell is strong, but too easy to overuse + +Echo is already rich in shell/explanation objects: + +- `ProvenanceEntry` +- `FinalizedChannel` +- `CursorReceipt` +- `TruthFrame` +- `SchedulerStatus` + +That is useful, but dangerous. Without a stricter boundary, these shell objects +can quietly masquerade as neighborhood or reintegration truth. + +### 4. Runtime schema generation is still deferred + +`schemas/runtime/README.md` says the right thing honestly: Echo’s runtime +fragments are authored locally today, and Wesley generation is deferred. + +That was a reasonable posture while the runtime shape was being frozen. It is +not a stable end state if Echo is supposed to align with Continuum and Wesley. + +### 5. Compile-time footprint safety does not exist yet + +Echo has real runtime footprint enforcement and compliance reporting. That is +good and should stay. + +But Continuum’s current proof target now requires a stronger claim for at least +one slice: + +- rewrite implementations should fail to compile when they exceed the declared + footprint + +Echo cannot satisfy that today because the generated capability surface does +not exist yet. + +### 6. Browser host bridge still carries too much compatibility residue + +`ttd-browser` and `echo-session-proto` are already narrower than before, but +they still carry more compatibility-era surface than the final architecture +wants. + +That is not just cleanup debt. It affects alignment, because the more Echo’s +bridge surface remains locally defined and compatibility-shaped, the harder it +is to pin one shared observer contract. + +## Required changes + +### A. Add a native local-site publication boundary + +Echo needs a first-class, runtime-backed publication shape for the debugger’s +local site. + +This does **not** require a perfect "all alternatives" object on day one. It +does require one honest native surface that stops making every consumer derive +site truth ad hoc from observations and receipts. + +Minimum target: + +- stable site identity +- primary lane/worldline/head coordinate +- explicit statement of whether the site is singleton/narrow or plural +- top-level lawful outcome kind +- enough lane participation data to support shared neighborhood nouns later + +### B. Add a native reintegration detail boundary + +Echo should stop forcing consumers to reconstruct seam truth from scattered +receipt/provenance/finalization objects. + +It needs a first-class reintegration publication surface that normalizes: + +- seam anchors +- compatibility obligations +- compatibility evidence + +This can still be synthesized internally at first, but it should be published +as one runtime-backed boundary object rather than as adapter folklore. + +### C. Keep receipt shell explicit and subordinate + +Echo should continue to expose rich shell objects, but make the layering +explicit: + +- neighborhood core does not come from shell +- reintegration detail does not come from shell +- shell refines those layers; it does not define them + +### D. Split Echo-local schema from shared contract composition + +Echo should keep owning Echo-specific engine-level GraphQL families such as: + +- runtime identifiers and counters +- routing/admission policy +- playback control richness +- scheduler status and engine-specific control-plane details + +But Echo should stop acting like those local families are the whole story for +cross-repo tooling. + +Instead: + +- Echo-local engine schema stays in Echo +- shared observer/debugger families should come from the Continuum/Wesley side +- Echo composes those generated/shared families with its own local runtime + families + +### E. Land one Wesley-generated proof slice in Echo + +Echo does not need to migrate wholesale first. + +It does need one narrow proof slice where: + +- the family is authored once through Wesley +- Rust-facing artifacts are generated +- Echo uses those generated artifacts in real runtime code +- a valid rewrite compiles and runs +- an invalid rewrite fails to compile because of footprint-shaped capability + boundaries + +This is the bridge from "alignment doctrine" to "real alignment." + +### F. Narrow the browser host bridge to the real contract boundary + +`ttd-browser` should become the narrow Echo browser host bridge, not the place +where debugger/product semantics continue to accumulate. + +`echo-session-proto` should keep only what the bridge path actually needs and +shed the legacy transport residue that no longer represents the future +architecture. + +### G. Publish the same top-level debugger nouns expected from `git-warp` + +Echo should not wait for `warp-ttd` adapters to invent the shared nouns on its +behalf forever. + +At the observer/debugger boundary, Echo should aim to publish the same top +categories that `git-warp` must also eventually publish: + +- frame / coordinate +- lane identity +- neighborhood core +- reintegration detail +- receipt shell +- effect emission +- delivery observation +- observer trace + +The exact internal engine structures can differ. The published categories +should not. + +## Sequencing + +### Phase 1: publish truthful local boundaries + +- native local-site boundary +- native reintegration-detail boundary +- explicit shell layering +- one shared lawful outcome mapping across tick, neighborhood, and settlement publication + +### Phase 2: prove Wesley ownership on one slice + +- one generated runtime family +- one compile-fail footprint proof +- one codec path + +### Phase 3: finish the bridge cut + +- narrow `ttd-browser` +- split `echo-session-proto` +- feed `warp-ttd` through the shared contract instead of local folklore + +## Current implementation note + +Echo now has the first truthful runtime mapping for the shared lawful outcome +family: + +- `TickReceiptDisposition::Applied` => `Derived` +- `TickReceiptDisposition::Rejected(FootprintConflict)` => `Obstruction` +- `NeighborhoodSite::Singleton` => `Derived` +- `NeighborhoodSite::Braided` => `Plural` +- `SettlementDecision::ImportCandidate` => `Derived` +- `SettlementDecision::ConflictArtifact` => `Conflict` + +That is enough to prove the top-level algebra in runtime truth. The remaining +Continuum cut is to carry this mapping into the shared generated family +boundary instead of leaving adapters to infer it. + +Echo also now has a native projection from `NeighborhoodSite` into an +Echo-side `NeighborhoodCore` DTO. That closes the first local-site publication +gap: the runtime can publish the shared neighborhood-core shape directly, +rather than requiring demo or adapter code to synthesize it by hand. + +## What does not need to change + +Echo does **not** need to: + +- stop being the hot runtime +- erase `GlobalTick` / scheduler-specific truths +- hide ingress/admission richness +- adopt every Continuum noun internally +- wait for perfect neighborhood plurality before publishing honest narrow site + truth + +Alignment does not mean losing engine specificity. It means publishing shared +observer/debugger truth at the right boundary. + +## Done looks like + +Echo is aligned with Continuum when: + +1. Echo-local engine families are clearly separated from shared contract + families. +2. Echo publishes neighborhood core, reintegration detail, and receipt shell as + explicit boundary layers instead of adapter synthesis folklore. +3. One Wesley-generated proof slice compiles into Echo runtime code and proves + compile-time footprint enforcement. +4. `warp-ttd` can consume Echo through the same top-level observer/debugger + nouns it will also expect from `git-warp`. + +At that point, Continuum tools can treat Echo as one engine over the shared +causal language instead of as a special case they must reinterpret every time. diff --git a/docs/design/0007-braid-geometry-and-neighborhood-publication.md b/docs/design/0007-braid-geometry-and-neighborhood-publication.md new file mode 100644 index 00000000..829c2517 --- /dev/null +++ b/docs/design/0007-braid-geometry-and-neighborhood-publication.md @@ -0,0 +1,405 @@ + + + +# 0007 — Braid geometry and neighborhood publication + +_Make Echo strands capable of read-only braid geometry and publish one honest +local site object for Continuum / `warp-ttd` consumption._ + +Legend: KERNEL + +Depends on: + +- [0004 — Strand contract](./0004-strand-contract.md) +- [0005 — Echo TTD witness surface](./0005-echo-ttd-witness-surface.md) +- [0006 — Echo Continuum alignment](./0006-echo-continuum-alignment.md) +- external Continuum packets: + - `0001` through `0015` + - `OVERVIEW` + +## Why this cycle exists + +The bootstrap strand contract in `0004` was intentionally narrow: + +- strands are first-class speculative lanes +- source-basis provenance is explicit +- advancement happens only through ordinary ingress + `super_tick()` +- support pins exist structurally, but MUST be empty in bootstrap + +That was the right first cut, but it is not enough for parity with +`git-warp`, and it is not enough for honest Continuum publication. + +Echo still lacks two things: + +1. **real braid geometry** + - a strand can name another lane as read-only support at an exact + coordinate +2. **native neighborhood publication** + - the kernel can publish which lanes actually participate in the local + site being observed, without making adapters reconstruct that shape from + scattered runtime objects + +This cycle therefore defines the first non-empty support-pin contract and the +first honest local-site publication boundary. + +Settlement remains separate. This packet is about local plural read geometry, +not compare/import/conflict law. That follow-on is now +[0008 — Strand settlement and conflict artifacts](./0008-strand-settlement.md). + +## Design decision + +Echo should align with Continuum braid theory by making two publication +surfaces first-class, while keeping the authoritative runtime-control +ontology anchored in +[0009 — Strand Runtime Graph Ontology](./0009-strand-runtime-graph-ontology.md): + +1. **Support pins become real, read-only braid geometry inputs on strands.** +2. **Observation publishes a first-class local site object whose participants + come from the observed lane, its source-basis relation, and its declared support + pins.** + +The resulting publication must stay intentionally narrow: + +- it need not enumerate every nearby speculative alternative in the whole + runtime +- it does need to publish the actual participating lanes for the observed site +- it must distinguish singleton from plural sites honestly + +This is the missing leg between bootstrap strands and a Continuum-aligned +`NeighborhoodCore`. + +## Human users / jobs / hills + +### Primary human users + +- kernel contributors implementing strand and observation runtime truth +- debugger contributors making `warp-ttd` speak the same neighborhood nouns + across Echo and `git-warp` +- advanced users exploring plural local sites without flattening them into one + fake line + +### Human jobs + +1. Declare a strand that reads through support overlays without mutating those + support lanes. +2. Observe one coordinate and know which lanes actually participate in the + local site. +3. Publish that site to a debugger without adapter folklore. + +### Human hill + +A human can inspect one Echo coordinate and see whether it is a singleton or a +plural site, which lanes participate, and why those lanes are in scope. + +## Agent users / jobs / hills + +### Primary agent users + +- agents implementing Echo host bridges and Continuum/Wesley proof slices +- agents auditing Echo for parity with `git-warp` + +### Agent jobs + +1. Determine whether an observed site is singleton or braided. +2. Read the participating lane set without reconstructing it from multiple + unrelated runtime objects. +3. Map Echo publication into Continuum `NeighborhoodCore` without guessing. + +### Agent hill + +An agent can consume one Echo neighborhood publication object and derive the +same top-level debugger nouns that `warp-ttd` expects from any host. + +## Core distinction + +Echo must keep three things separate: + +- **strand** + - a speculative lane relation over a child worldline +- **braid geometry** + - read-only local plurality declared by support pins +- **settlement** + - compare/import/conflict law over lane history + +Braid is geometry. Settlement is history law. Neither should be collapsed into +the other. + +## First non-empty support-pin contract + +Support pins should graduate from placeholder to first-class read geometry with +the following contract. + +### Support pin semantics + +A support pin is a read-only, exact-coordinate reference from one live strand +to another live strand's child worldline. + +It means: + +- "when reading this strand's local site, also include that support lane at + this exact pinned coordinate" + +It does **not** mean: + +- copy or import support history +- share writer heads +- make the support lane runnable +- settle or merge anything +- create a new worldline + +### Support-pin invariants + +In addition to the existing `SupportPin` fields in `strand.rs`, the first +non-empty contract should enforce: + +- **BG-1 (Live target):** the pinned `strand_id` must exist in + `StrandRegistry`. +- **BG-2 (Worldline agreement):** `worldline_id` must equal the target + strand's `child_worldline_id`. +- **BG-3 (Pinned coordinate exists):** `pinned_tick` must exist in the target + worldline provenance. +- **BG-4 (Pinned state agreement):** `state_hash` must match the target + worldline's state root at `pinned_tick`. +- **BG-5 (No self-pin):** a strand must not support-pin itself. +- **BG-6 (Quantum agreement):** the primary strand and support strand must + share the same `tick_quantum`. +- **BG-7 (No duplicate support target):** one strand must not carry multiple + support pins to the same target strand in the same publication slice. +- **BG-8 (Read-only support):** support pins do not authorize writes through + the support lane. + +### Support-pin mutation surface + +The first useful mutation surface is explicit and small: + +- `pin_support(strand_id, support_strand_id, pinned_tick)` +- `unpin_support(strand_id, support_strand_id)` +- `list_support_pins(strand_id)` + +`pin_support(...)` resolves and stores the exact target worldline ID and state +hash at the pinned tick. Pinning is therefore deterministic and replayable. + +### Drop posture + +Bootstrap braid geometry should reject dropping a strand while it is pinned by +another live strand. + +That is stricter than automatic cleanup, but it preserves a simple truth: + +- a published plural site cannot silently lose one of its declared + participants because another operation hard-deleted it out from under the + geometry + +If a looser cleanup model is desired later, it should be an explicit follow-on +design, not incidental side effect. + +## Local site publication + +Echo needs a first-class publication object for the local site being observed. + +The exact type name is not yet fixed. In this packet it is called +`NeighborhoodSite`. + +`NeighborhoodSite` is not a new worldline and not a universal braid object. It +is a bounded publication object for one observed coordinate. + +### Minimum `NeighborhoodSite` contract + +```text +NeighborhoodSite { + site_id: NeighborhoodSiteId, + anchor: ResolvedObservationCoordinate, + plurality: SitePlurality, + participants: Vec, + outcome_kind: AdmissionOutcomeKind, // derived from plurality in the first cut +} + +SiteParticipant { + worldline_id: WorldlineId, + strand_id: Option, + role: ParticipantRole, + tick: WorldlineTick, + state_hash: Hash, +} +``` + +Where: + +- `ParticipantRole::Primary` + - the observed lane at the observed coordinate +- `ParticipantRole::BasisAnchor` + - the source lane/coordinate from `ForkBasisRef` when the primary lane is a strand +- `ParticipantRole::Support` + - a read-only support-pinned lane + +And: + +- `SitePlurality::Singleton` + - only the primary lane participates +- `SitePlurality::Braided` + - one or more additional participants are present + +At the shared lawful-outcome layer, the first cut maps: + +- `SitePlurality::Singleton` => `Derived` +- `SitePlurality::Braided` => `Plural` + +This should remain a derived relation in the first cut. Echo does not need a +separate stored field to tell the same story twice. + +### What counts as a participant + +The first publication must stay narrow and explicit. + +At an observed coordinate, participants are: + +1. the observed primary lane +2. the basis anchor, if the primary lane is a strand +3. every declared support pin on that strand, resolved to its pinned + coordinate + +This is enough to publish real local plurality without pretending Echo already +has a perfect global neighborhood search or full nearby-alternative catalog. + +### Site identity + +`site_id` should be stable over: + +- the primary lane identity +- the observed coordinate +- the declared participant set and their roles + +The adapter may still format a transport-specific string or opaque ID, but the +site identity must come from kernel-truth inputs, not UI heuristics. + +## Observation integration + +`ObservationArtifact` should remain the record of: + +- resolved coordinate +- frame +- projection +- payload +- deterministic artifact hash + +It should **not** be overloaded to carry every neighborhood concern inline. + +Instead, Echo should add a neighboring publication path: + +- observe frame/projection → `ObservationArtifact` +- observe local site at coordinate → `NeighborhoodSite` + +Those two objects are related, but not the same. + +This keeps the layering clean: + +- observation artifact = what was read +- neighborhood site = which lanes define the local site around that read + +## Kernel truth vs adapter summary + +### Kernel runtime truth + +Echo kernel/runtime should own: + +- strand -> child-worldline truth +- fork-basis truth +- authorised head truth +- support-pin declarations or cache records when enabled +- participant roles +- pinned coordinates and state hashes +- site plurality inputs +- stable site identity inputs + +`NeighborhoodSite` itself is a derived publication object in the first +cut. It is computed from authoritative runtime truth; it is not the +authoritative store of that truth. + +### Adapter / debugger summary + +Adapters such as Echo → `warp-ttd` may still own: + +- transport DTO layout +- UI ordering and labels +- debugger-local `frameIndex` mapping +- host-specific shell decoration + +But adapters must stop owning the core local-site reconstruction. + +If the adapter has to rediscover participants from raw `ObservationArtifact`, +`Strand`, and provenance state, Echo is still not aligned. + +## Continuum mapping + +This packet is the Echo-side bridge into Continuum's shared observer/debugger +nouns. + +The intended mapping is: + +- `NeighborhoodSite.anchor` → shared coordinate/frame truth +- `NeighborhoodSite.participants` → shared lane participation +- `NeighborhoodSite.plurality` → shared singleton-vs-plural site truth +- `NeighborhoodSite.outcome_kind` → shared lawful outcome family (`Derived` / `Plural`) +- support/basis roles → Continuum neighborhood semantics + +This is the first real Echo-side grounding for a shared `NeighborhoodCore`. + +The current kernel cut now also provides a native projection boundary from +`NeighborhoodSite` into an Echo-side `NeighborhoodCore` DTO, so host projects +no longer need to reconstruct that shape from scattered runtime objects just +to consume the first shared family slice. + +It does **not** yet solve: + +- reintegration detail +- settlement +- observer trace +- global nearby-alternative enumeration + +Those remain separate layers. + +## What this cycle does not do + +- define settlement/import/conflict law +- define a full global braid graph +- define all nearby alternatives around a site +- make support lanes writable +- flatten plural sites into synthetic worldlines + +## Immediate implementation consequences + +1. `StrandRegistry` and strand runtime APIs must allow non-empty + `support_pins` under the new invariants, while keeping them + explicitly non-authoritative in the first cut. +2. Echo must track reverse pin usage well enough to reject dropping a pinned + support strand. +3. Observation/publication code must expose a native `NeighborhoodSite` + boundary. +4. Echo's `warp-ttd` host bridge should map `NeighborhoodSite` directly into + shared neighborhood publication instead of reconstructing participants + indirectly. +5. `KERNEL_strand-settlement` should build on this geometry, not redefine it. + +## Open questions + +1. Should support pins remain strand-to-strand only, or eventually admit + canonical-worldline support references too? +2. Does `NeighborhoodSite` belong beside `ObservationArtifact`, or should both + be grouped under a higher observation/publication envelope? +3. Is `BasisAnchor` always a participant in the published site, or can some + observers request a reduced publication that omits provenance-only lanes? + +## Decision + +Echo should not wait for a perfect global braid model before publishing plural +local sites. + +It should: + +- make support pins real as read-only braid geometry inputs +- publish one native `NeighborhoodSite` object for the observed local site +- keep settlement separate +- let adapters consume that derived publication instead of inventing it + +That is the smallest honest step from bootstrap strands toward parity with +`git-warp` and toward shared Continuum debugger nouns. diff --git a/docs/design/0008-strand-settlement.md b/docs/design/0008-strand-settlement.md new file mode 100644 index 00000000..2351999a --- /dev/null +++ b/docs/design/0008-strand-settlement.md @@ -0,0 +1,478 @@ + + + +# 0008 — Strand settlement and conflict artifacts + +_Define Echo's first deterministic settlement runway for strands: +compare -> plan -> import -> conflict artifact._ + +Legend: KERNEL + +Depends on: + +- [0004 — Strand contract](/docs/design/0004-strand-contract.md) +- [0007 — Braid geometry and neighborhood publication](/docs/design/0007-braid-geometry-and-neighborhood-publication.md) +- [0006 — Echo Continuum alignment](/docs/design/0006-echo-continuum-alignment.md) +- [0010 — Bounded site and admission policy](/docs/design/0010-bounded-site-and-admission-policy.md) + +## Why this cycle exists + +After `0007`, Echo has an honest story for: + +- speculative lanes +- fork-basis provenance +- braid geometry +- local plurality publication + +What it still does not have is a lawful way to bring speculative lane history +back into canonical history. + +That is the settlement problem. + +Settlement is not: + +- "merge two worldlines" as if they were symmetrical peers +- "copy the published truth frames back in" +- "last writer wins" +- "compose braid geometry until it becomes history" + +Settlement is a separate history law: + +- compare the strand's suffix against its fork-basis coordinate +- plan what may be imported under deterministic policy +- import accepted cause-level history +- record unresolved residue as first-class conflict artifacts + +This is the missing leg between plural speculative work and real parity with +`git-warp`'s compare/transfer-plan workflow. + +## Design decision + +Echo should define v1 strand settlement as a **canonical-source settlement +runway**: + +1. compare one strand's suffix against the worldline and coordinate recorded in + `ForkBasisRef` +2. produce a deterministic settlement plan +3. append accepted imports to the canonical target worldline as `MergeImport` + provenance entries +4. append unresolved residue as `ConflictArtifact` provenance entries + +v1 does **not** generalize to arbitrary target lanes yet, and it does +not yet support settlement when the strand's source basis is itself a +speculative lane. + +`0009` governs the runtime-control ontology and the generalized +`ForkBasisRef` naming. This packet narrows only the settlement runway. + +That is a deliberate narrowing, not a theoretical claim. It gives Echo one +honest settlement path without pretending every cross-worldline import problem +is solved at once. + +## Human users / jobs / hills + +### Primary human users + +- engine contributors implementing strand compare/import behavior +- debugger contributors who need conflict artifacts instead of silent drops +- advanced users exploring speculative lanes and then deciding what can come + back into canonical history + +### Human jobs + +1. Compare a strand against its fork-basis coordinate and see the resulting delta. +2. Produce a deterministic import plan instead of ad hoc merge behavior. +3. See explicit conflict artifacts for what could not be imported. + +### Human hill + +A human can run settlement on a strand and get one stable answer: +what imports cleanly, what conflicts, and why. + +## Agent users / jobs / hills + +### Primary agent users + +- agents implementing or auditing settlement/runtime publication +- agents building Continuum/Wesley proof slices over shared debugger nouns + +### Agent jobs + +1. Compute a settlement plan deterministically from runtime truth. +2. Distinguish accepted imports from recorded conflicts. +3. Map settlement outputs into reintegration detail and receipt shell without + inventing new folklore. + +### Agent hill + +An agent can observe one strand, compute the same settlement runway every time, +and publish accepted imports versus conflict residue through explicit runtime +artifacts. + +## Core principle + +**Merge the causes, not the published truth.** + +`TruthFrame` and session playback surfaces are replay artifacts. They are +authoritative for observation and delivery, but they are not merge inputs. + +Settlement operates on recorded causal/runtime truth such as: + +- `ProvenanceEntry` +- replay patches +- atom writes +- recorded channel outputs +- channel policies + +It does not settle by replaying UI frames back into history. + +## Scope of v1 settlement + +### Allowed source + +- one live strand +- `fork_basis_ref.source_lane_id` must name a canonical worldline in v1 +- source history is the child worldline suffix strictly after + `fork_basis_ref.fork_tick` + +### Allowed target + +- only the canonical worldline named by `fork_basis_ref.source_lane_id` +- only against the exact fork-basis coordinate recorded in `fork_basis_ref` + +### Explicit exclusions + +v1 does not define: + +- settlement into arbitrary non-target worldlines +- settlement between two sibling strands +- settlement for strands whose `fork_basis_ref.source_lane_id` is + itself speculative +- automatic conflict resolution +- support-pin history import +- synthetic merged worldlines + +Support pins remain geometry only. They inform reads and neighborhood +publication, but they are not additional import sources in v1 settlement. + +## Settlement objects + +The exact Rust type names can still change, but the runtime publication ladder +should look like this. + +### 1. `SettlementDelta` + +The compare result for one strand relative to its fork-basis coordinate. + +Minimum contents: + +```text +SettlementDelta { + strand_id: StrandId, + fork_basis_ref: ForkBasisRef, + source_child_worldline_id: WorldlineId, + source_suffix_start_tick: WorldlineTick, + source_suffix_end_tick: WorldlineTick, + source_entries: Vec, +} +``` + +This is intentionally narrow. It identifies the source settlement window and +the authoritative recorded entries that the planner will inspect. + +### 2. `SettlementPlan` + +The deterministic result of evaluating the delta under channel and import law. + +Minimum contents: + +```text +SettlementPlan { + strand_id: StrandId, + target_worldline: WorldlineId, + target_basis_ref: ProvenanceRef, + basis_report: StrandBasisReport, + decisions: Vec, +} +``` + +Where each `SettlementDecision` is one of: + +- `ImportCandidate` +- `ConflictArtifactDraft` + +At the shared admission-family layer, v1 settlement uses the narrow truthful +subset: + +- `ImportCandidate` => `Derived` +- `ConflictArtifactDraft` => `Conflict` + +v1 settlement does **not** yet publish `Plural` or `Obstruction` as settlement +surface outcomes. Those remain future braid/collapse and wider admission-law +work, not something this packet should fake early. + +### 3. `ImportCandidate` + +One accepted unit of source history eligible to become target provenance. + +Minimum contents: + +```text +ImportCandidate { + source_ref: ProvenanceRef, + source_head_key: Option, + imported_op_id: Hash, + target_expected_state_root: Hash, + overlap_revalidation: Option, +} +``` + +The imported unit is source provenance, not playback shell. The target expected +state root is target-local replay truth; it may differ from the source state +root when the parent basis advanced. + +### 4. `ConflictArtifactDraft` + +One unresolved unit of settlement residue. + +Minimum contents: + +```text +ConflictArtifactDraft { + artifact_id: Hash, + source_ref: ProvenanceRef, + channel_ids: Vec, + reason: ConflictReason, + overlap_revalidation: Option, +} +``` + +`ConflictReason` should begin narrow and deterministic: + +- `ChannelPolicyConflict` +- `UnsupportedImport` +- `BaseDivergence` +- `ParentFootprintOverlap` +- `QuantumMismatch` + +The exact reason set can grow later, but v1 must not collapse every failure +into one anonymous "could not merge" blob. + +Disjoint parent advance is not a conflict reason. When the parent advanced +outside the strand-owned footprint, settlement must compute a target-local +expected root and plan an `ImportCandidate` over the current parent basis. + +`ParentFootprintOverlap` means explicit revalidation of a parent write inside +the strand-owned closed footprint failed, was obstructed, or proved conflicting. +It is not the default result for all overlap: an overlapped source patch that is +already satisfied on the current parent basis can still settle as a clean +target-local import with an inspectable `Clean` revalidation outcome. + +## Compare phase + +The compare phase answers: + +- what exact suffix exists on the strand after the fork point +- what the planner will evaluate + +Compare walks the strand child worldline after `fork_basis_ref.fork_tick` and +collects authoritative `ProvenanceRef`s / entries in append order. + +Compare does **not** decide eligibility. It only defines the candidate runway. + +## Plan phase + +The plan phase evaluates each source entry under deterministic import law. + +### Planning inputs + +- source `ProvenanceEntry` +- source replay patch / atom writes / outputs +- target fork-basis coordinate +- target worldline policy state +- channel policy for all affected channels + +### Planning rules + +1. **Quantum agreement is mandatory.** + Cross-worldline settlement requires identical `tick_quantum`. +2. **Support pins are not import sources.** + Only the strand's own post-fork suffix is planned. +3. **Channel policy gates import eligibility.** +4. **Unsettled residue becomes explicit conflict artifacts.** +5. **Live-basis drift must be classified before import.** + Parent movement outside the owned footprint is eligible for target-local + import. Parent movement inside the owned footprint requires explicit + revalidation before it can import. +6. **Overlap revalidation must be inspectable.** + If a source patch intersects live-basis overlap slots, apply failure becomes + `Obstructed` residue, no target state-root change becomes `Clean` import, + and target state-root change becomes `Conflict` residue. + +### Live-basis import root law + +When the parent has not advanced beyond the strand anchor, a source suffix +entry's expected root can match the target-local import root. + +When the parent has advanced outside the strand-owned footprint, those roots are +not the same object: + +```text +source root = state_root(anchor_parent + child_suffix_patch) +target root = state_root(current_parent_tip + child_suffix_patch) +``` + +The target root is the only root that may be committed on the target worldline. +Settlement now computes this root during planning and carries it on the internal +`ImportCandidate`, while the ABI plan continues to expose the source-coordinate +candidate shape. + +### Channel policy law + +Echo already has real channel policies: + +- `StrictSingle` +- `Reduce(op)` +- `Log` + +Settlement should treat them as import eligibility law: + +- `StrictSingle` + - if the source contribution disagrees with the target's admissible single + value, plan a conflict artifact +- `Reduce(op)` + - import is eligible only through the deterministic reducer +- `Log` + - import is eligible only when the channel explicitly opts into historical + multiplicity; otherwise the planner must still be allowed to surface a + conflict instead of smearing events together + +The planner's job is not to hide disagreement. It is to classify it +deterministically. + +## Import phase + +The import phase appends accepted imports to the canonical target worldline as +`ProvenanceEventKind::MergeImport` entries. + +Those entries should: + +- point back to the imported source coordinate +- preserve deterministic parentage on the target worldline +- carry imported patch/causal truth as replayable provenance +- remain distinguishable from ordinary local commits + +v1 does not need a universal import algebra. It does need one honest import +recording path that can be replayed and inspected. + +## Conflict artifact phase + +The conflict phase appends unresolved residue as +`ProvenanceEventKind::ConflictArtifact` entries on the target worldline. + +The provenance event kind is already present in repo truth. This cycle gives it +real semantics: + +- it is not a warning log +- it is not shell-only metadata +- it is first-class recorded history saying "this source residue existed and + could not be deterministically settled under current law" + +The artifact payload may still be represented through auxiliary shell data or a +future richer record, but the provenance entry itself must exist as kernel +truth. + +## Provenance event semantics + +This cycle sharpens three existing placeholders in +`ProvenanceEventKind`: + +1. `CrossWorldlineMessage` + - remains pre-settlement coordination / future runway + - not required for v1 settlement execution +2. `MergeImport` + - becomes the authoritative target-worldline record for accepted imports +3. `ConflictArtifact` + - becomes the authoritative target-worldline record for unresolved + settlement residue + +That is enough to move these event kinds out of placeholder limbo without +forcing all future cross-worldline behavior into this one cycle. + +## Relationship to braid geometry + +`0007` defined support pins and local plural site publication. + +This cycle depends on that geometry, but does not redefine it. + +The law is: + +- braid geometry answers "which lanes define the local site?" +- settlement answers "which part of one strand's history can lawfully become + target history?" + +If those two concerns are collapsed, Echo will either overfit settlement into +the observation model or quietly treat support overlays as if they were imports. + +## Relationship to Continuum and `warp-ttd` + +Settlement must eventually feed: + +- reintegration detail +- receipt shell +- conflict inspection in `warp-ttd` + +But `warp-ttd` should consume the published settlement nouns, not invent them. + +The important top-level debugger truth is: + +- accepted import +- explicit conflict artifact +- admission outcome kind +- stable source/target coordinate + +That gives Continuum tools one honest cross-host story instead of "Echo +settlement looks nothing like `git-warp` transfer planning." + +## What this cycle does not do + +- implement automatic conflict resolution +- define arbitrary target selection beyond the canonical target worldline +- define a full conflict artifact schema family +- settle support-pin participants as if they were source history +- replace reintegration detail with settlement shell + +## Immediate implementation consequences + +1. Echo needs a native settlement compare surface over strand suffixes. +2. Echo needs a deterministic planner that reads channel policies as import law. +3. Echo must give `MergeImport` and `ConflictArtifact` real runtime semantics. +4. Echo should publish settlement outputs so adapters can surface them without + re-deriving them from raw provenance. +5. Later Wesley/Continuum proof slices should target these settlement outputs + as shared observer/debugger nouns, with Echo-local shell allowed around + them. +6. Settlement must grow target-local import candidates before disjoint parent + drift can be cleanly imported. + +## Open questions + +1. Should `ImportCandidate` operate at provenance-entry granularity only, or + should v1 plan at finer op/channel granularity inside an entry? +2. Does `ConflictArtifactDraft` need a first-class payload type in kernel truth + immediately, or can v1 begin with provenance entry plus shell data? +3. When Echo later permits durable strands, does settlement remain + canonical-source-only, or become target-parameterized? + +## Decision + +Echo should add one honest, deterministic settlement runway now: + +- compare one strand's suffix to its fork-basis coordinate +- plan imports under channel policy +- record accepted imports as `MergeImport` +- record unresolved residue as `ConflictArtifact` + +That is enough to move Echo from speculative-lane experimentation toward real +conceptual parity with `git-warp`, without pretending it already owns a +universal merge oracle. diff --git a/docs/design/0009-strand-runtime-graph-ontology.md b/docs/design/0009-strand-runtime-graph-ontology.md new file mode 100644 index 00000000..26f52a72 --- /dev/null +++ b/docs/design/0009-strand-runtime-graph-ontology.md @@ -0,0 +1,521 @@ + + + +# 0009 - Strand Runtime Graph Ontology + +Cycle: `0009-strand-runtime-graph-ontology` +Legend: `KERNEL` +Source backlog item: `docs/method/backlog/up-next/KERNEL_strand-runtime-graph-ontology.md` + +## Sponsors + +- Human: Runtime / ontology architect +- Agent: Kernel implementation agent + +## Hill + +Echo gets one explicit graph-native control ontology for strands. A +strand becomes a relation object naming a child worldline, an exact +fork basis, and authorised writer heads. Its current state is obtained +only from the child worldline frontier, and future braid/support +geometry has an honest place to live without reintroducing a second +execution model. + +## Playback Questions + +### Human + +- [ ] If we inspect the runtime graph, can we point to explicit nodes + and edges that answer what a strand is, what it was forked from, + which worldline currently carries its state, and which heads may + write it? +- [ ] Does the graph make it impossible to mistake a strand for a + private mutable store or frozen snapshot? + +### Agent + +- [ ] Can I derive the current strand state by following + `strand -> child_worldline -> current_portal -> Descend(warp)` + with no strand-local shadow store or side channel? +- [ ] Are support-pin and braid publication objects explicitly marked + as derived / non-authoritative in the first cut? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: the packet should answer + three questions in order: where strand state lives, what graph nouns + are authoritative, and what remains derived. +- Non-visual or alternate-reading expectations: each node and edge name + should carry a single stable meaning so a reader can reconstruct the + ontology from text alone. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: type labels remain + ASCII-first, domain-separated, and stable enough to appear in code, + docs, tests, and generated debugger output. +- Logical direction / layout assumptions: the packet describes graph + structure and rewrite posture only; no UI layout or visual debugger + presentation is in scope. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: the authoritative + node types, authoritative edge types, payload shapes, and the exact + traversal that yields a strand's current state. +- What must be attributable, evidenced, or governed: any support-pin or + braid publication object must be identifiable as cache / derived + publication rather than canonical truth. + +## Why this cycle exists + +The bootstrap strand contract was useful because it named a relation +between a parent provenance coordinate and a child worldline. It also +smuggled in a bogus second execution model: strands were described as +manual, dormant, strand-specific lanes instead of ordinary participants +in Echo's single `SuperTick` law. + +That drift matters because the runtime already has the correct kernel +shape: + +- one mutable frontier per worldline +- immutable graph truth beneath that frontier +- content-addressed ingress +- deterministic scheduler admission +- one `super_tick()` path for state change + +Until the runtime graph says explicitly where strand state lives and +which objects are authoritative, `0007` and `0008` remain partly +folkloric. We need one graph ontology that tells the truth. + +Where older packets conflict on runtime-control ontology, naming, or +first-cut authority claims, this packet governs. + +## Design decision + +Echo should materialise strand runtime ontology in a dedicated control +warp and make the following objects authoritative in the first cut: + +1. worldline control nodes +2. one current-portal node per live worldline +3. strand relation nodes +4. fork-basis nodes +5. writer-head control nodes + +Support pins and braid views may also be represented in the graph, but +in the first cut they remain derived / cache publication objects rather +than canonical truth. + +## Core laws + +### 1. State locality law + +A strand's current materialised state lives only on the child +worldline's current frontier. + +The strand node itself does not carry mutable current state, overlay +truth, or a descended current graph. + +### 2. Relation law + +A strand is a relation object. + +It names: + +- one child worldline +- one precise fork basis +- one or more authorised writer heads + +It is not a second scheduler, a private mutable store, or a snapshot +container. + +### 3. Fork basis law + +A strand must be rooted at one exact parent coordinate, not at an +ambient parent tip clone. The source coordinate may come from any +admissible causal lane and any admissible tick within that lane, not +only from the current frontier. + +That basis must remain explicitly recoverable from graph truth. + +Examples that must remain lawful include: + +- forking from a canonical worldline frontier +- forking from a historical coordinate in that worldline +- forking from a speculative lane at one exact earlier coordinate + +What matters is not frontier-ness. What matters is that the basis is one +precise admissible coordinate. + +### 4. Head capability law + +Writer heads attached to a strand remain ordinary writer heads under the +same control law as every other head. + +They may author work only for the child worldline they are authorised to +serve unless a future cross-lane mechanism is introduced explicitly. + +### 5. Control warp law + +Runtime control ontology should live in a dedicated control warp rather +than polluting arbitrary application graphs. + +### 6. Derived braid law + +Support-pin geometry and braid publication may be represented in the +runtime graph, but in the first cut they are not authoritative stores of +semantic truth. + +## Runtime control warp + +This packet assumes a dedicated runtime-control warp instance, for +example: + +```text +warp:sys/runtime +``` + +The runtime-control warp contains lane, strand, fork-basis, and +writer-head ontology. Application / domain graphs remain in their own +materialised warp instances and are reached through explicit portal / +descend structure. + +## Authoritative node types + +### `sys/runtime` + +The root control node for the runtime-control warp. + +This node anchors the live control ontology and owns edges to currently +live worldlines, strands, and heads. + +### `sys/worldline` + +A live worldline control noun. + +Alpha attachment type: + +```text +payload:sys/worldline_meta/v1 +``` + +Minimum payload: + +```text +WorldlineMeta { + worldline_id: WorldlineId, + frontier_tick: WorldlineTick, + tick_quantum: WorldlineTick, +} +``` + +This node is authoritative for the identity of the worldline and its +current frontier coordinate in the runtime. + +### `sys/worldline/current_portal` + +A dedicated portal node whose alpha attachment is: + +```text +Descend(WarpId) +``` + +This node exists because one attachment slot cannot simultaneously carry +both structured atom payload and a descended warp target. Splitting the +portal into its own node keeps the graph honest. + +### `sys/strand` + +A strand relation node. + +Alpha attachment type: + +```text +payload:sys/strand_meta/v1 +``` + +Minimum payload: + +```text +StrandMeta { + strand_id: StrandId, +} +``` + +The strand node is intentionally small. Its semantics come primarily +from edges to the child worldline, fork basis, and authorised heads. + +### `sys/fork_basis` + +A first-class basis object for strand origin. + +Alpha attachment type: + +```text +payload:sys/fork_basis/v1 +``` + +Minimum payload: + +```text +ForkBasisMeta { + basis_ref: ForkBasisRef, +} +``` + +`ForkBasisRef` names one exact admissible coordinate in any causal +lane. It supersedes the older worldline-only fork-basis naming. + +This node reifies the exact source lane relation, fork tick, commit +hash, boundary hash, and provenance reference used to create the +strand. The basis object must be able to refer to a historical interior +coordinate in either a canonical worldline or a speculative strand, not +only a live frontier. + +### `sys/head/writer` + +A writer-head control node. + +Alpha attachment type: + +```text +payload:sys/writer_head_meta/v1 +``` + +Minimum payload: + +```text +WriterHeadMeta { + head_key: WriterHeadKey, + eligibility: HeadEligibility, + playback_mode: PlaybackMode, + public_inbox: Option, + is_default_writer: bool, +} +``` + +This node makes head control state graph-visible instead of hiding it in +scattered registries. + +## Authoritative edge types + +### `edge:runtime/worldline` + +```text +sys/runtime -> sys/worldline +``` + +Meaning: this worldline is live in the current runtime. + +Cardinality: zero or more from the runtime root. + +### `edge:runtime/strand` + +```text +sys/runtime -> sys/strand +``` + +Meaning: this strand relation object is live in the current runtime. + +Cardinality: zero or more from the runtime root. + +### `edge:runtime/head` + +```text +sys/runtime -> sys/head/writer +``` + +Meaning: this writer head is live in the current runtime. + +Cardinality: zero or more from the runtime root. + +### `edge:worldline/current` + +```text +sys/worldline -> sys/worldline/current_portal +``` + +Meaning: this edge identifies the worldline's current materialised state. + +Cardinality: exactly one outgoing edge from each live `sys/worldline` +node. + +### `edge:strand/child_worldline` + +```text +sys/strand -> sys/worldline +``` + +Meaning: this strand's live state is carried by that child worldline. + +Cardinality: exactly one outgoing edge from each `sys/strand` node. + +### `edge:strand/fork_basis` + +```text +sys/strand -> sys/fork_basis +``` + +Meaning: this strand is rooted at that exact fork basis. + +Cardinality: exactly one outgoing edge from each `sys/strand` node. + +### `edge:fork_basis/source_lane` + +```text +sys/fork_basis -> (sys/worldline | sys/strand) +``` + +Meaning: this fork basis refers to the exact source causal lane from +which the strand was created. The target may be a canonical worldline or +a speculative strand. + +Cardinality: exactly one outgoing edge from each `sys/fork_basis` node. + +### `edge:strand/authorized_head` + +```text +sys/strand -> sys/head/writer +``` + +Meaning: the target writer head is authorised to submit work for this +strand's child worldline. + +Cardinality: one or more outgoing edges from each `sys/strand` node. + +Validation rule: every authorised head must target the same child +worldline named by `edge:strand/child_worldline`. + +## Authoritative traversal contracts + +### Current strand state + +The current strand materialisation is obtained by following: + +```text +sys/strand + -> edge:strand/child_worldline +sys/worldline + -> edge:worldline/current +sys/worldline/current_portal + -> Descend(WarpId) +``` + +That descended warp is the current graph state for the strand's child +worldline. + +### Source basis + +The source basis of a strand is obtained by following: + +```text +sys/strand + -> edge:strand/fork_basis +sys/fork_basis + -> edge:fork_basis/source_lane +``` + +### Authorised heads + +The writer heads allowed to author child-lane work are obtained by +following: + +```text +sys/strand + -> edge:strand/authorized_head +``` + +## Derived / cache graph objects + +The following objects may exist in the runtime graph, but in the first +cut they are not authoritative truth. + +### Support pins + +Support pins may be represented as: + +```text +edge:strand/support_pin +``` + +from one `sys/strand` node to another, with beta attachment type: + +```text +payload:sys/support_pin/v1 +``` + +Minimum payload: + +```text +SupportPinMeta { + pinned_tick: WorldlineTick, + state_hash: Hash, +} +``` + +First-cut status: + +- recomputable +- versioned +- safe to drop and rebuild +- not the sole authority for braid semantics + +### Braid publication + +Braid publication may later use derived nodes such as: + +- `sys/braid_view` +- `sys/braid_cell` + +with edges such as: + +- `edge:braid/basis` +- `edge:braid/participant` +- `edge:braid/cell` + +But this packet does not make those objects authoritative. + +## Rewrite and mutation posture + +This packet describes ontology, not a second execution engine. + +The intended operational posture is: + +- strand creation may remain a bootstrap helper in this cycle so long as + it produces truthful graph objects and an exact `ForkBasis` +- the creation surface must accept any admissible lane/tick coordinate as + fork input, not just current frontier coordinates +- steady-state lane evolution still occurs only through ordinary intent + admission under `super_tick()` +- no `tick_strand()` API or equivalent side channel should exist after + this cycle is implemented +- support-pin and braid publication objects may be rebuilt from + authoritative runtime truth + +## Non-goals + +- [ ] Make support pins authoritative semantic truth in this cycle. +- [ ] Introduce a large persisted braid ontology before we have honest + strand state locality. +- [ ] Make fork creation itself a tick-admitted causal event in this + cycle. +- [ ] Solve settlement recursion here; `0008` remains the history-law + follow-on. +- [ ] Replace application / domain graph ontology with runtime-control + nodes. + +## Backlog Context + +Echo already has the right kernel law for worldlines: one frontier, one +scheduler, one `super_tick()` path. The missing piece is an explicit +runtime graph ontology that says where strand state lives and what the +strand object actually is. + +Without that ontology: + +- strand docs drift toward manual ticking folklore +- support pins risk becoming accidental truth stores +- braid and settlement have no stable graph nouns to attach to + +This packet creates the authoritative runtime nouns first so later work +can recurse honestly under the same causal law. diff --git a/docs/design/0010-bounded-site-and-admission-policy.md b/docs/design/0010-bounded-site-and-admission-policy.md new file mode 100644 index 00000000..9a775376 --- /dev/null +++ b/docs/design/0010-bounded-site-and-admission-policy.md @@ -0,0 +1,246 @@ + + + +# 0010 - Bounded Site and Admission Policy + +Cycle: `0010-bounded-site-and-admission-policy` +Legend: `KERNEL` +Source backlog item: `docs/method/backlog/up-next/KERNEL_bounded-site-and-admission-policy.md` + +## Sponsors + +- Human: Runtime / ontology architect +- Agent: Kernel implementation agent + +## Hill + +Echo gets one explicit admission-side noun for the site over which claims are +judged, and one explicit policy story for how `super_tick()` decides lawful +results. The runtime keeps its single-tick law, but stops scattering policy +truth across unrelated structs and comments. + +## Playback Questions + +### Human + +- [ ] Can we point to one explicit site noun and explain what part of local + graph truth is being judged during a tick, compare, or settlement step? +- [ ] Can a host choose policy without being allowed to inject bespoke causal + law into the engine? + +### Agent + +- [ ] Can I explain `super_tick()` as admission over ingress claims at bounded + sites under explicit policy? +- [ ] Can I map existing Echo policy fragments into one admission-law family + without pretending the runtime is a reducer-only machine? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: the packet should answer four + questions in order: what a bounded site is, what policy governs admission, + what lawful outcomes exist, and what shell/witness layering follows. +- Non-visual or alternate-reading expectations: the runtime noun stack must be + reconstructible from text without assuming diagrams or debugger UI. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: the terms remain ASCII-first and + stable enough for code, logs, tests, and generated debug output. +- Logical direction / layout assumptions: this packet defines runtime law, not + screen layout or observer-product presentation. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: the admission site, the + governing policy identity, the lawful outcome family, and the distinction + between witness core and shell. +- What must be attributable, evidenced, or governed: when policy changes causal + meaning, the engine must be able to name that policy; observer-side + summarisation must not masquerade as canonical admission. + +## Why this cycle exists + +Echo already has most of the raw pieces: + +- content-addressed ingress +- one `super_tick()` path +- `policy_id` in engine/patch identity +- `ChannelPolicy`, `ConflictPolicy`, `HeadEligibility`, `PlaybackMode`, and + `RetentionPolicy` +- settlement reason classes and neighborhood publication + +What Echo lacks is one explicit admission-law object model that ties those +pieces together. + +Without that model, the runtime risks staying correct in implementation while +remaining blurry in doctrine: + +- policy exists, but as fragments +- sites exist, but under several overlapping names +- outcomes exist, but not yet as one explicit admission algebra +- shells exist, but are too easily mistaken for the witness itself + +## Design decision + +Echo should state its central runtime step as: + +**`super_tick()` performs admission over ingress claims at bounded sites under +explicit engine-defined policy, yielding lawful outcomes plus witness-bearing +shells.** + +## Core laws + +### 1. Admission site law + +Echo needs one explicit admission-side noun: + +- `BoundedSite` + +`BoundedSite` is Echo's formalisation of focal closure on the admission side. +It names the local site over which coexistence or obstruction is judged. + +This is not a new competing geometry. It is the place where Echo cashes out the +existing ideas already present in: + +- footprint +- affect / affected region +- reintegration boundary +- optional derived neighborhood publication + +### 2. Site structure law + +`BoundedSite` should be rich enough to explain lawful judgement and small enough +to avoid becoming a second ontology of the whole graph. + +At minimum it should carry or derive: + +- the direct claim footprint +- the locally affected region needed for admission +- the reintegration boundary needed for later comparison or settlement + +Neighborhood publication may be derived from the same underlying truth, but it +is not the authoritative site noun for admission. + +### 3. Policy law + +Admission policy in Echo is engine-defined law. + +The allowed model is: + +- Echo defines deterministic policy families +- hosts select, parameterise, or reference them +- policy identity is explicit when it changes published causal meaning + +The forbidden model is: + +- hosts injecting bespoke executable admission law and still claiming that the + result is ordinary Echo truth + +### 4. Outcome law + +Echo should standardise one admission outcome family that spans local tick work +and the later braid/settlement story: + +- `Derived` +- `Plural` +- `Conflict` +- `Obstruction` + +This does not require every subsystem to emit the same struct today. It does +require the runtime and docs to stop speaking as if every unresolved situation +were merely failure or residue. + +### 5. Shell law + +Echo should preserve the distinction between: + +- the lawful result +- the witness of lawfulness +- the shell or hologram used to carry that result onward + +`TickReceipt` is one shell family. +Settlement publications and later transport artefacts may be other shell +families. + +The runtime should not pretend that every witness-bearing publication is +secretly the same receipt type. + +### 6. Single-tick admission law + +This packet does not introduce a second execution model. + +All state change still happens under the existing single-tick law: + +- claims enter through ingress +- Echo routes and orders them deterministically +- `super_tick()` performs the admission step +- new graph truth and shell artefacts are emitted + +This packet only makes the law explicit and names the missing site/policy +objects honestly. + +## Existing policy surfaces to unify + +The raw policy substrate already exists in Echo. The work is to align and name +it coherently: + +- `policy_id` in engine/patch identity +- `ChannelPolicy` +- `ConflictPolicy` +- `HeadEligibility` +- `PlaybackMode` +- `RetentionPolicy` +- settlement reason classes + +## First implementation cut + +The first cut does not need to rewrite the whole engine. + +It should: + +1. introduce an explicit `BoundedSite` noun +2. restate `super_tick()` in docs and code comments as admission over claims at + bounded sites under explicit policy +3. introduce or stabilise one admission outcome family +4. keep existing policy fragments, but route them through one clearer + admission-law story + +### Current implementation note + +The first runtime cut lands this as a thin wrapper over existing footprint +truth: + +- `crates/warp-core/src/admission.rs` defines `BoundedSite` +- `claim_footprint` preserves the full declared footprint +- `affected_region` is currently the direct write wake +- `reintegration_boundary` is currently the declared boundary-port surface + +This is intentionally conservative. It makes the admission-site noun explicit +without inventing a second geometry or pretending that braid publication is +already the authoritative site model. + +The next runtime cut should thread the shared lawful outcome family through the +existing publication surfaces: + +- `TickReceiptDisposition::Applied` => `Derived` +- `TickReceiptDisposition::Rejected(FootprintConflict)` => `Obstruction` +- `SettlementDecision::ImportCandidate` => `Derived` +- `SettlementDecision::ConflictArtifact` => `Conflict` + +That keeps Echo honest while braid-local plurality remains a later cut. + +## Non-goals + +- replacing Echo's scheduler with a different execution model +- freezing one cross-engine site struct +- making settlement or braid publication authoritative in the same cut +- inventing host-authored policy plugins + +## Backlog Context + +- [0006 - Echo Continuum alignment](./0006-echo-continuum-alignment.md) +- [0007 - Braid geometry and neighborhood publication](./0007-braid-geometry-and-neighborhood-publication.md) +- [0008 - Strand settlement](./0008-strand-settlement.md) +- [0009 - Strand runtime graph ontology](./0009-strand-runtime-graph-ontology.md) +- External reference: Continuum `0020 - Shared Admission and Policy Publication` diff --git a/docs/design/0011-neighborhood-publication-stack.md b/docs/design/0011-neighborhood-publication-stack.md new file mode 100644 index 00000000..e4f4b578 --- /dev/null +++ b/docs/design/0011-neighborhood-publication-stack.md @@ -0,0 +1,329 @@ + + + +# 0011 - Neighborhood publication stack + +Cycle: `0011-neighborhood-publication-stack` +Legend: `PLATFORM` +Source backlog item: `docs/method/backlog/up-next/PLATFORM_neighborhood-publication-stack.md` + +## Sponsors + +- Human: Runtime / host-boundary architect +- Agent: Host-adapter and debugger integration agent + +## Hill + +Echo has one explicit, inspectable explanation of how local-site truth moves +from runtime state to host-visible publication, including: + +- what is authoritative admission truth +- what is commit-time emitted runtime truth +- what is read-time publication +- how `NeighborhoodCore` reaches the wasm/host boundary + +The packet should answer this without requiring the reader to reconstruct the +stack from code comments, tests, and chat logs. + +## Playback Questions + +### Human + +- [ ] Can I explain why `NeighborhoodCore` is published on demand rather than + emitted automatically during every tick? +- [ ] Can I point to the exact difference between `ObservationArtifact`, + `NeighborhoodSite`, and `NeighborhoodCore`? +- [ ] Can I explain how bytes reach the host without implying that + neighborhood core is part of commit-time materialization? + +### Agent + +- [ ] Can I tell which API to call when I want raw observation, Echo-native + local site truth, or the shared Continuum family shape? +- [ ] Can I explain the exact runtime path from `ObservationRequest` to + CBOR-encoded `NeighborhoodCore` without guessing? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: this packet should be readable as + one left-to-right ladder: admission truth, observation truth, Echo-native + site publication, shared family projection, ABI bytes. +- Non-visual expectations: all distinctions must be explicit in text. The + packet should not depend on diagrams. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: use stable ASCII-first nouns so + the stack is reproducible in logs, tests, and host adapter docs. +- Logical direction / layout assumptions: this packet defines causal layering + and boundary shape, not screen layout. + +## Agent Inspectability and Explainability + +- What must be explicit for agents: which objects are authoritative, which are + derived, and which are boundary DTOs. +- What must remain attributable: if the host sees `NeighborhoodCore`, the + system must name the kernel object and export path from which it came. + +## Why this cycle exists + +Echo now has enough real publication truth that the old “adapter will +eventually synthesize a narrow neighborhood core” story is no longer +accurate. + +Repo truth now includes: + +- admission-side `BoundedSite` +- raw observation via `ObservationArtifact` +- Echo-native local-site publication via `NeighborhoodSite` +- shared family projection via `NeighborhoodCore` +- wasm/ABI entrypoints for both `observe_neighborhood_site(...)` and + `observe_neighborhood_core(...)` + +Without one explicit packet for that stack, the system becomes hard to reason +about: + +- hosts may assume `NeighborhoodCore` is a commit-time materialization channel +- maintainers may assume `ObservationArtifact` and `NeighborhoodSite` are just + two names for the same thing +- debugger work may keep reintroducing adapter folklore + +## Core distinction + +Echo must keep three classes of truth separate. + +### 1. Admission truth + +This is where the engine decides lawful coexistence or obstruction. + +Current noun: + +- `BoundedSite` + +This is authoritative for admission. It is not a host DTO and it is not +automatically revealed. + +### 2. Commit-time emitted runtime truth + +This is what Echo actually records or emits as part of execution: + +- tick receipts +- provenance entries +- finalized channel outputs +- materialization frames + +These arise because a tick committed or a finalization step happened. + +### 3. Read-time publication + +This is what Echo derives when a host asks a question about one local site: + +- `ObservationArtifact` +- `NeighborhoodSite` +- `NeighborhoodCore` + +These are not pushed automatically on every tick. They are produced on demand +from existing runtime truth. + +## Publication ladder + +The current stack is: + +1. `BoundedSite` + - admission-side focal closure + - authoritative for coexistence law +2. `ObservationArtifact` + - raw read / revelation artifact at a resolved coordinate +3. `NeighborhoodSite` + - Echo-native publication of one local site +4. `NeighborhoodCore` + - shared Continuum-family projection of that local site +5. ABI / wasm export + - canonical bytes returned to the host + +This ladder is deliberate. + +It lets Echo: + +- keep admission truth separate from observer publication +- keep Echo-native site publication separate from shared family DTOs +- keep receipt shell and reintegration detail separate from neighborhood core + +## What each object is for + +### `ObservationArtifact` + +Use this when the host needs: + +- resolved coordinate metadata +- declared frame/projection +- raw head or snapshot payload +- artifact hash and direct revelation context + +It answers: + +- what was observed? +- at what coordinate? +- under which observation contract? + +It does **not** answer: + +- which lanes define the local site? +- whether the site is singleton or plural? +- what the shared neighborhood-core family object should be? + +### `NeighborhoodSite` + +Use this when the host needs Echo-native local-site truth: + +- site identity +- anchor coordinate +- participant set +- site plurality + +It answers: + +- which lanes participate in the currently observed local site? +- is the site singleton or braided? + +It is still Echo-shaped: + +- worldline ids remain typed kernel ids +- participant ticks remain worldline ticks +- plurality is `Singleton | Braided` + +### `NeighborhoodCore` + +Use this when the host wants the shared Continuum-family publication shape: + +- string lane identities +- string site identity +- shared `AdmissionOutcomeKind` +- shared `NeighborhoodPlurality` +- shared participant roles +- summary text for debugger surfaces + +It answers: + +- what is the shared interoperable publication for this local site? + +It is intentionally narrow: + +- no reintegration detail +- no receipt shell +- no global nearby-alternative enumeration + +## How publication happens + +For the shared family boundary, the current runtime path is: + +1. Host sends `ObservationRequest` +2. `warp-wasm` decodes canonical CBOR +3. `WarpKernel::observe_neighborhood_core(...)` resolves the request +4. `NeighborhoodSiteService::observe(...)` builds `NeighborhoodSite` +5. `NeighborhoodSite::to_core()` projects into `NeighborhoodCore` +6. `NeighborhoodCore::to_abi()` projects into the ABI DTO +7. `warp-wasm` returns canonical CBOR bytes to the host + +This is read-time publication, not commit-time emission. + +## Why Echo publishes both `NeighborhoodSite` and `NeighborhoodCore` + +The system needs both levels. + +`NeighborhoodSite` exists because: + +- Echo needs one honest local-site publication object in runtime truth +- host adapters should not reconstruct participant sets from scattered objects + +`NeighborhoodCore` exists because: + +- shared family consumers should not need to understand Echo-native id types +- the shared Continuum neighborhood-core family has its own stable noun stack + +So the duplication is not accidental. It is a controlled boundary split: + +- Echo-native truth +- shared boundary truth + +## What does **not** happen + +Echo does **not** currently: + +- auto-emit `NeighborhoodCore` as part of every tick +- store `NeighborhoodCore` as independent canonical runtime history +- treat neighborhood core as a replacement for `ObservationArtifact` +- use `NeighborhoodCore` as reintegration detail or receipt shell + +If a host wants `NeighborhoodCore`, it must ask for it explicitly. + +## Host guidance + +Choose the read API by question. + +### Use `observe(...)` when you need + +- raw observation payload +- direct revelation context +- frame/projection specifics + +### Use `observe_neighborhood_site(...)` when you need + +- Echo-native local-site truth +- typed worldline / strand identities +- direct kernel publication + +### Use `observe_neighborhood_core(...)` when you need + +- the shared Continuum family shape +- host/debugger interoperability +- a stable publication object for cross-tool use + +## Relation to reintegration and shell + +Neighborhood core is not the whole debugger object model. + +Still separate: + +- reintegration detail +- receipt shell +- scheduler/provenance explanation +- global nearby-alternative enumeration + +This packet therefore complements, but does not replace: + +- `0005-echo-ttd-witness-surface` +- `0008-strand-settlement` +- `SPEC-0009-wasm-abi-v3` + +## Current implementation note + +The current repo truth is: + +- `BoundedSite` exists as admission-side law +- `NeighborhoodSite` exists as runtime publication truth +- `NeighborhoodCore` exists as shared family projection +- `observe_neighborhood_site(...)` exists at the ABI boundary +- `observe_neighborhood_core(...)` exists at the ABI boundary + +So the next host-side simplification is straightforward: + +- consumers such as `continuum-demo` should stop synthesizing + `NeighborhoodCore` in app code +- they should consume Echo's published neighborhood-core boundary directly + +## Non-goals + +- Define reintegration detail +- Define receipt shell semantics +- Define all nearby alternatives around a site +- Replace `ObservationArtifact` +- Replace `NeighborhoodSite` with only the shared family DTO + +## Backlog Context + +- `docs/design/0005-echo-ttd-witness-surface.md` +- `docs/design/0006-echo-continuum-alignment.md` +- `docs/design/0007-braid-geometry-and-neighborhood-publication.md` +- `docs/design/0010-bounded-site-and-admission-policy.md` +- `docs/spec/SPEC-0009-wasm-abi-v3.md` diff --git a/docs/design/0012-dynamic-footprint-binding-runtime.md b/docs/design/0012-dynamic-footprint-binding-runtime.md new file mode 100644 index 00000000..ea0d6103 --- /dev/null +++ b/docs/design/0012-dynamic-footprint-binding-runtime.md @@ -0,0 +1,126 @@ + + + +# 0012 Dynamic Footprint Binding Runtime + +## One Sentence + +Echo should treat rewrite footprint law as static at the level of slots, +binding sources, closure operators, create/update surfaces, and forbidden +surfaces, while treating concrete node/head/range membership as a runtime +binding problem. + +## Why This Exists + +The current bounded rewrite proof slice closes one important gap: + +- user-authored rewrite logic no longer needs a public native Rust callback + seam +- Wesley can generate a bounded Rust surface from a declared footprint +- Echo can consume that surface and prove undeclared capability access fails at + compile time + +That proof is still flat. Real consumer rewrites, especially hot graph rewrites +like `ReplaceRangeAsTick`, do not operate on pre-known static node identities. +They need runtime bindings such as: + +- worldline id +- base head id +- byte range +- dynamically derived local focus closure +- optionally dynamically derived affected-anchor closure + +This note states how Echo should handle that without reopening arbitrary graph +reach. + +## Core Split + +Echo should preserve this split: + +- **static footprint schema** + - declared slots + - binding sources + - closure operators + - create/update surfaces + - forbidden surfaces +- **dynamic footprint binding** + - concrete ids supplied by the caller + - relation bindings resolved from current runtime truth + - derived closure membership resolved from actual graph state + +Wesley owns the first half. Echo owns the second. + +## Runtime Responsibilities + +Given a Wesley-compiled structured rewrite contract, Echo should be responsible +for: + +1. binding direct slots from invocation arguments +2. binding relation-derived slots from already-bound slots +3. resolving declared closure operators against runtime truth +4. enforcing cardinality and basis validity for those bindings +5. exposing only the declared slot/closure/create/update capabilities to the + rewrite implementation +6. rejecting stale, ambiguous, or invalid bindings at runtime + +Echo should not allow the implementation to compensate for missing bindings by +performing arbitrary extra traversal outside the declared closure grammar. + +## Example Shape + +For a rewrite like `ReplaceRangeAsTick`, the runtime binding problem is: + +- bind `worldline` from `worldlineId` +- bind `baseHead` from `baseHeadId` +- derive `touchedRope` from `baseHead` plus `[startByte, endByte]` +- optionally derive `affectedAnchors` from `worldline`, `baseHead`, and the + edit window +- create the next head, tick, receipt, and any local rope/blob nodes +- update the worldline's canonical-head relation + +The runtime may discover: + +- one touched leaf +- many touched rope nodes +- zero affected anchors +- many affected anchors + +Those are runtime truths. The compile-time honesty claim is only that the +rewrite cannot reach beyond: + +- the bound slots +- the declared closures +- the declared create/update surfaces + +## Failure Split + +Echo should distinguish two failure classes clearly. + +### Compile-time honesty failures + +These are handled by Wesley-generated surfaces and Rust compilation: + +- implementation tries to access undeclared capability +- implementation tries to touch forbidden surfaces +- implementation tries to create/update undeclared graph nouns + +### Runtime binding failures + +These are handled by Echo at invocation or admission time: + +- bound ids do not resolve +- relation-based binding does not exist +- closure cardinality is wrong for the declared contract +- basis/head/worldline relationships are stale or invalid +- logical no-op means no admitted rewrite should be minted + +## Immediate Implication + +The next serious runtime seam is not "more flat footprint tests." It is: + +- define structured runtime binding objects for slots and closures +- bind one real rewrite family against them +- prove that the runtime can resolve dynamic focus honestly without reopening + arbitrary graph traversal + +That is the path from the current proof slice to real hot-graph consumers. diff --git a/docs/design/0013-generic-observer-api-and-plan.md b/docs/design/0013-generic-observer-api-and-plan.md new file mode 100644 index 00000000..fc7dfdb8 --- /dev/null +++ b/docs/design/0013-generic-observer-api-and-plan.md @@ -0,0 +1,286 @@ + + + +# 0013 Generic Observer API And Plan + +## One Sentence + +Echo should host compiled observer plans over generic worldline truth by +returning deterministic tick results and holographic slice inputs while keeping +app-specific observer semantics out of handwritten substrate APIs. + +## Why This Exists + +The current optic handoff is now much clearer: + +- applications author set-side operations as app contracts +- Echo admits those intents against generic substrate truth +- Echo returns deterministic result and receipt envelopes +- applications then obtain readings through observers + +That still leaves one major runtime seam unnamed: + +> what generic Observer API does Echo provide so app-authored observers can be +> hosted lawfully without turning Echo into an app server? + +The answer cannot be: + +- handwritten app-specific methods on Echo +- arbitrary host callbacks running over runtime truth +- implicit full-worldline materialization to satisfy every read + +The answer has to be a generic observer boundary that accepts compiled plans, +works over sliced holographic inputs, and emits observer-relative readings. + +## Core Split + +Echo should preserve four distinct objects: + +1. **Intent envelope** + - compiled app request for the optic's set side +2. **Tick result** + - deterministic admission outcome, receipt envelope, hologram reference, + frontier update +3. **Observer plan** + - compiled app request for the optic's get side +4. **Reading envelope** + - observer-relative result emitted from a hosted or one-shot observer + +These objects serve different jobs and must not be collapsed. + +## What Echo Hosts + +Echo should host: + +- generic worldlines and causal history +- generic tick admission and receipt production +- hologram or boundary-style slice inputs +- observer instances carrying runtime observer state +- sliced reading production over the needed causal cone + +Echo should not host: + +- app-specific observer names as handwritten public methods +- app-specific payload semantics as handwritten runtime APIs +- arbitrary observer callbacks supplied by the host + +## Static Versus Runtime + +Echo should treat these as already-compiled static inputs: + +- observer aperture and slice policy +- basis identifiers or basis plan +- observer state schema identity +- update and emission plan identity +- rights, exposure, and revelation constraints +- materialization budgets + +Echo should treat these as runtime instance state: + +- current observer state value +- current frontier or hologram reference +- current slice input +- emitted reading envelope + +## Proposed Generic API Shape + +The substrate-facing shape should move toward four generic operations: + +1. `submit_intent(...) -> TickResult` +2. `register_observer(plan) -> ObserverHandle` +3. `advance_observer(handle, frontier_or_hologram) -> ReadingEnvelope` +4. `read_once(plan, frontier_or_hologram) -> ReadingEnvelope` + +Possible neighboring helpers: + +- `dispose_observer(handle)` +- `snapshot_observer_state(handle)` +- `resume_observer(handle, state)` + +The exact transport encoding can vary by surface, but the semantic shape should +stay stable. + +## Tick Result Contract + +For this observer-facing substrate handoff, the post-admission result should be +thought of as: + +```text +TickResult = (outcome, receipt, hologram_ref, frontier) +``` + +This does not mean Echo must fully materialize an app view immediately. + +The important point is: + +- admission result is one object +- later reading is a second object + +The same UI may ask for both immediately, but they remain different semantic +jobs. + +## Reading Contract + +Observer reads should be thought of as: + +```text +(ObserverPlan, Frontier | HologramRef) + -> slice causal cone + -> update observer state + -> emit ReadingEnvelope +``` + +The reading envelope should carry at least: + +- observer handle or observer identity +- frontier or hologram reference +- reading payload +- reading payload hash or trace identity where needed + +## One-Shot Versus Hosted Observers + +Echo should support at least two classes of lawful observer use. + +### One-shot observer + +- no long-lived runtime handle required +- may still be memoryless or accumulative from the plan's perspective +- useful for immediate post-tick reads + +### Hosted observer + +- persistent runtime handle +- state advances across several frontiers or holograms +- useful for debugger/session/tooling flows + +The distinction should remain explicit because hosted state is a real runtime +object, not merely a nicer query wrapper. + +## Rights And Exposure + +Observer legality is not only a performance issue. + +Echo's generic Observer API should also enforce compiled constraints such as: + +- exposure tier +- revelation tier +- redaction class +- whether receipt-only or witness-bearing details may surface +- whether the observer may see canonical-only, strand-aware, or braid-aware + truth + +Those rights should come from the compiled observer plan, not from ad hoc host +switches. + +## Relationship To Current ObservationRequest + +The current `ObservationRequest` path is still useful, but it is only an early +substrate read seam. + +It is not yet the full observer lifecycle because it does not explicitly model: + +- compiled observer plan identity +- hosted observer instance state +- one-shot versus persistent observer distinction +- observer-specific update and emission law + +That means `observe(...)` should be treated as the current bridge, not the +finished observer boundary. + +## Immediate Next Step + +The next substrate-facing implementation move should be: + +1. freeze one generic observer plan shape +2. freeze one reading envelope shape +3. prove one memoryless observer through the generic API +4. prove one accumulative observer through the generic API + +The first application-facing proving target should be a `jedit` +`worldlineSnapshot` observer compiled into a generic observer plan rather than +a handwritten Echo method. + +## Human users / jobs / hills + +### Primary human users + +- app/runtime developers wiring observer reads into Echo-backed tools +- debugger users who need stable readings without app-specific substrate APIs + +### Human jobs + +1. Request a current or historical reading through one generic observer shape. +2. Distinguish tick admission from later observer-relative reading. + +### Human hill + +A human can request a lawful observer reading without asking Echo to grow a new +handwritten app method. + +## Agent users / jobs / hills + +### Primary agent users + +- codegen agents compiling observer plans from app contracts +- verification agents auditing read boundaries and exposure constraints + +### Agent jobs + +1. Generate a generic observer plan from app-owned semantics. +2. Verify that reading production is bounded by compiled exposure and slice + policy. + +### Agent hill + +An agent can inspect one observer plan and determine which generic runtime +operation and slice policy it requires. + +## Human playback + +1. The human submits an intent and receives a tick result. +2. The human requests a reading through an observer plan. +3. The output shows the reading envelope without exposing an app-specific Echo + method. + +## Agent playback + +1. The agent reads the observer plan. +2. The agent maps it to one-shot or hosted observer execution. +3. The agent verifies the reading envelope against the plan identity and slice + policy. + +## Implementation outline + +1. Define the generic observer plan and reading envelope shape. +2. Add one one-shot observer path over a bounded hologram/frontier input. +3. Add one hosted observer path with explicit observer state. +4. Prove a `jedit` worldline snapshot observer through the generic path. + +## Tests to write first + +- one-shot memoryless observer returns a deterministic reading envelope +- hosted observer advances state across two frontiers +- observer execution rejects readings outside compiled exposure/slice policy + +## Risks / unknowns + +- The first plan shape may be too narrow for accumulative observers; keep the + first proof small and version the plan envelope. +- Slice policy may need tighter accounting before large hologram inputs are + safe to expose. + +## Postures + +- **Accessibility:** Observer output should preserve machine-readable labels so + host UIs can expose accessible readings. +- **Localization:** Reading payload semantics remain app-owned; Echo should not + localize generic envelopes. +- **Agent inspectability:** Plan identity, slice policy, and reading envelope + hashes must stay explicit. + +## Non-goals + +- This cycle does not add handwritten app-specific observer methods. +- This cycle does not replace the current `ObservationRequest` bridge in one + step. +- This cycle does not define every Continuum observer payload. diff --git a/docs/determinism/RELEASE_POLICY.md b/docs/determinism/RELEASE_POLICY.md index 0795c1dc..9aa41e6e 100644 --- a/docs/determinism/RELEASE_POLICY.md +++ b/docs/determinism/RELEASE_POLICY.md @@ -118,6 +118,13 @@ configuration, **do not allowlist it. Refactor instead.** - The `check_task_lists.sh` pre-commit hook does **not** cover allowlist auditing; this is a manual review gate. +## Declarative Rule Authorship Governance + +Release decisions that touch runtime rule authoring must preserve +[`DECLARATIVE-RULE-AUTHORSHIP`](../invariants/DECLARATIVE-RULE-AUTHORSHIP.md): +default public APIs expose Wesley-compiled declarative IR surfaces, while +native callback rule authoring remains bootstrap-only and feature-gated. + ## Escalation If staging/prod blocker state conflicts with recommendation: diff --git a/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md b/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md new file mode 100644 index 00000000..d9f31868 --- /dev/null +++ b/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md @@ -0,0 +1,131 @@ + + + +# DECLARATIVE-RULE-AUTHORSHIP + +**Status:** Normative | **Legend:** KERNEL | **Cycle:** 0012 + +## Invariant + +Echo's deterministic execution path admits user-authored rewrite logic only as +Wesley-compiled declarative IR. Native executable rewrite code is a +trusted/bootstrap-only engine implementation surface, not a public user +extension boundary. + +For Echo, that declarative boundary is the GraphQL-authored shared family +surface: graph entities, graph rewrites, declared footprints, and any types +that cross Rust, TypeScript, WASM, process, or network boundaries. The rest of +the runtime may remain handwritten Rust as long as it does not become a shadow +authority for user-authored rewrite law. + +## Rulings + +The following rulings are normative. "MUST" and "MUST NOT" follow +RFC 2119 convention. + +### R1 — User-authored rewrite law is declarative + +User-authored rewrite logic MUST enter Echo as Wesley-compiled declarative IR. +User-authored rewrite law MUST NOT enter the deterministic path as handwritten +native callbacks, function pointers, or ad hoc executable host code. + +GraphQL-authored shared families are the intended source for user-authored +graph entities, graph rewrites, declared footprints, and cross-boundary types +that Echo exposes to hosts, `warp-ttd`, or other engines. + +Echo MAY keep handwritten Rust for engine internals, storage, scheduler +mechanics, and other local implementation details, but those details MUST NOT +replace the shared family as the public authoring boundary for rewrite law or +cross-boundary types. + +### R1a — Declared footprints are part of the authored boundary + +When a user-authored graph rewrite enters Echo through the shared family +surface, its declared footprint is part of the contract, not optional +commentary. + +The long-term target is for Wesley-generated Rust surfaces to make those +capability boundaries explicit enough that dishonest footprint use becomes +structurally impossible or a compile-time failure, rather than something Echo +discovers only after arbitrary native code has already been admitted. + +For dynamic rewrites, the declared boundary should be read as static at the +level of slots, binding sources, closure operators, create/update surfaces, +and forbidden surfaces. Echo then owns the runtime problem of binding concrete +ids and resolving those declared closures without widening the capability +surface. + +### R2 — Native rewrite functions are bootstrap-only + +`RewriteRule`, `MatchFn`, and `ExecuteFn` are bootstrap-only trusted-code +surfaces. They MAY exist for engine internals, internal system rules, +transitional bootstrap code, and tests, but they MUST NOT be treated as the +long-term public authoring boundary for application rewrite logic. + +Echo's default public Rust API MUST NOT expose native rewrite authoring as a +supported extension surface. If a temporary bootstrap seam remains, it must be +explicitly internal, non-default, and subordinate to the same declarative +boundary. + +### R3 — Host policy is selected, not authored + +Hosts MAY select engine-defined deterministic policy by reference. Hosts MUST +NOT inject bespoke executable admission law into Echo's deterministic path. + +### R4 — Public host boundaries remain callback-free + +The browser/WASM deterministic boundary MUST remain byte-oriented and +callback-free. Public host adapters MUST NOT smuggle executable host callbacks +or host-authored closures into the deterministic kernel loop. + +### R5 — Ambient-state exemptions do not legitimize native rule authorship + +Allowlist exemptions in determinism scanning MUST NOT be used to legitimize +user-authored native rewrite execution on the deterministic path. If a file is +reachable from user-authored rewrite execution, nondeterministic API usage MUST +be refactored away rather than excused by policy. + +### R6 — Transitional sandboxes remain subordinate to the same law + +If Echo temporarily introduces a sandboxed authoring layer, that layer is +acceptable only if it compiles or lowers to the same lawful declarative +substrate and does not reopen arbitrary executable host-side escape hatches. + +## Rationale + +The wasm/browser boundary is not currently the main determinism hole. The real +escape hatch is the native rule API itself: Echo still runs executable matcher +and executor function pointers directly on the deterministic path. + +That may be acceptable for trusted bootstrap code, but it is not an acceptable +public authoring story. As long as user-authored logic can enter as arbitrary +native code, Echo cannot fully guarantee that ambient state, hidden callbacks, +or impure helper code will stay out of deterministic execution. + +The correct closure is not "be careful." The correct closure is to narrow the +user authoring boundary until it is declarative, inspectable, and compilable by +Wesley into a lawful rewrite substrate. + +That does not require Wesley to compile the entire application. It requires +Wesley to own the lawful graph/rewrite boundary so Echo can keep runtime +internals handwritten without exposing arbitrary native author code on the +deterministic path. + +## Consequences + +- Echo may keep native `RewriteRule` internals for bootstrap and tests. +- Echo MUST move user-facing rewrite authorship toward Wesley-generated + declarative IR instead of stabilizing native rule callbacks as product API. +- Determinism audit gates should scrutinize rule-authoring and host-boundary + files more strictly than ordinary support code. +- A sandboxed language is acceptable only as a front-end to the same + declarative substrate, not as a new executable bypass around it. + +## Cross-references + +- [0010-bounded-site-and-admission-policy](../design/0010-bounded-site-and-admission-policy.md) +- [0012-dynamic-footprint-binding-runtime](../design/0012-dynamic-footprint-binding-runtime.md) +- [TTD-COUNTERFACTUAL-CREATION](./TTD-COUNTERFACTUAL-CREATION.md) +- [FIXED-TIMESTEP](./FIXED-TIMESTEP.md) +- [RELEASE_POLICY](../RELEASE_POLICY.md) +- [KERNEL_determinism-escape-hatches](../method/backlog/up-next/KERNEL_determinism-escape-hatches.md) diff --git a/docs/invariants/STRAND-CONTRACT.md b/docs/invariants/STRAND-CONTRACT.md index d81ed08a..f2dd7e6c 100644 --- a/docs/invariants/STRAND-CONTRACT.md +++ b/docs/invariants/STRAND-CONTRACT.md @@ -28,9 +28,9 @@ RFC 2119 convention. ### INV-S1 — Immutable parent anchor -A strand's `base_ref` MUST NOT change after creation. The `BaseRef` +A strand's `fork_basis_ref` MUST NOT change after creation. The `ForkBasisRef` pins the exact provenance coordinate the strand was forked from: -source worldline ID, fork tick (last included tick in the copied +source lane ID, fork tick (last included tick in the copied prefix), commit hash at fork tick, output boundary hash (state root after applying the patch), and a `ProvenanceRef` handle. @@ -50,16 +50,14 @@ set to the child worldline. A strand MUST NOT outlive the session that created it (v1). No strand persistence across sessions. -### INV-S4 — Manual tick +### INV-S4 — Deterministic Tick -A strand's writer heads MUST be created with -`HeadEligibility::Dormant` and `PlaybackMode::Paused`. They are -ticked only by explicit external command, never by the live -scheduler. Dormant heads do not appear in the `RunnableWriterSet`. +A strand's worldline MUST NOT be ticked by the live scheduler. It MUST +advance only through ordinary ingress + `super_tick()` coordination. -### INV-S5 — Complete base_ref +### INV-S5 — Complete fork_basis_ref -`base_ref` MUST pin: source worldline ID, fork tick, commit hash, +`fork_basis_ref` MUST pin: source lane ID, fork tick, commit hash, boundary hash, and provenance ref. All fields MUST agree with the provenance store at construction time. If any field disagrees, construction MUST fail. @@ -72,7 +70,7 @@ change its quantum. ### INV-S7 — Distinct worldlines -`child_worldline_id` MUST NOT equal `base_ref.source_worldline_id`. +`child_worldline_id` MUST NOT equal `fork_basis_ref.source_lane_id`. A strand is always a distinct worldline from its base. ### INV-S8 — Head ownership diff --git a/docs/invariants/TTD-COUNTERFACTUAL-CREATION.md b/docs/invariants/TTD-COUNTERFACTUAL-CREATION.md new file mode 100644 index 00000000..062b1186 --- /dev/null +++ b/docs/invariants/TTD-COUNTERFACTUAL-CREATION.md @@ -0,0 +1,74 @@ + + + +# TTD-COUNTERFACTUAL-CREATION + +**Status:** Normative | **Legend:** PLATFORM | **Cycle:** 0005 + +## Invariant + +A TTD reading or inspection act does not by itself create new causal truth in +Echo. A counterfactual lane exists only after an explicit fork rooted at one +exact admissible source-lane coordinate. In Echo v1, strands created through +TTD or any comparable debugger surface are session-scoped speculative lanes by +default. + +## Invariants + +The following invariants are normative. "MUST" and "MUST NOT" follow +RFC 2119 convention. + +### INV-TC1 — Observation is read-only + +Read-side observation, replay, and inspection MUST NOT create strands, mutate +the live frontier, or silently rewrite canonical history. + +### INV-TC2 — Explicit fork required + +Continuing from an earlier coordinate or exploring a debugger-driven +counterfactual MUST go through an explicit strand-creation act. Echo MUST NOT +materialise a speculative lane merely because a user sought to a historical +coordinate. + +### INV-TC3 — Exact fork basis + +Every debugger-created strand MUST be rooted at one exact `ForkBasisRef`. +Echo MUST be able to name the source lane, fork tick, commit hash, boundary +hash, and provenance handle for that fork. + +### INV-TC4 — Session-scoped by default in v1 + +In Echo v1, debugger-created strands MUST default to session-scoped scratch or +minimally retained speculative work. They MUST NOT silently become durable +shared history. + +### INV-TC5 — Promotion is separate from creation + +Creating a strand and promoting work into shared admitted history are distinct +acts. Echo MUST NOT treat debugger counterfactual creation as implicit shared +publication. + +### INV-TC6 — Future durable retention must carry provenance posture + +If Echo later supports debugger-created strands that outlive the creating +session, the retained history MUST be able to name creator, tool or session +origin, fork basis, and retention or revelation posture. + +## Rationale + +TTD is a revelation surface. Strands are speculative causal lanes. Collapsing +those two roles would turn inspection into hidden mutation and would destroy +the distinction between read-side explanation and write-side counterfactual +creation. + +The current Echo bootstrap already supports the crucial separation: strands are +explicit objects with exact `ForkBasisRef`s, and the runtime advances them only +through ordinary `super_tick()` admission. What v1 does not yet provide is a +durable author-only retention tier beyond the session boundary. That remains a +policy extension, not an excuse to blur observation and forking. + +## Cross-references + +- [STRAND-CONTRACT](./STRAND-CONTRACT.md) — strand ontology and single tick law +- [0004 — Strand contract](../design/0004-strand-contract.md) +- [0005 — Echo TTD witness surface](../design/0005-echo-ttd-witness-surface.md) diff --git a/docs/method/backlog/up-next/KERNEL_admission-outcome-family.md b/docs/method/backlog/up-next/KERNEL_admission-outcome-family.md new file mode 100644 index 00000000..69a6b396 --- /dev/null +++ b/docs/method/backlog/up-next/KERNEL_admission-outcome-family.md @@ -0,0 +1,22 @@ + + + +# KERNEL - Admission Outcome Family + +Echo now has an explicit `AdmissionOutcomeKind` family, but outcome-shaped truth +still needs to be threaded consistently across tick receipts, settlement reason +classes, and braid/collapse language so the runtime, docs, and future shared +publication surface all point to the same causal vocabulary. + +This cycle should finish threading one lawful witnessed-suffix outcome algebra +through Echo: + +- `Admitted` +- `Staged` +- `Plural` +- `Conflict` +- `Obstructed` + +The remaining work does not need to rewire every subsystem at once. It does +need to finish removing older dialects so `Admitted`, `Staged`, `Plural`, +`Conflict`, and `Obstructed` remain the shared causal fact family. diff --git a/docs/method/backlog/up-next/KERNEL_bounded-site-and-admission-policy.md b/docs/method/backlog/up-next/KERNEL_bounded-site-and-admission-policy.md new file mode 100644 index 00000000..bbaa9fc2 --- /dev/null +++ b/docs/method/backlog/up-next/KERNEL_bounded-site-and-admission-policy.md @@ -0,0 +1,13 @@ + + + +# KERNEL - Bounded Site and Admission Policy + +Echo already has one `super_tick()` law, several real policy surfaces, and +several partial site vocabularies. What it lacks is one explicit admission-law +packet that ties those pieces together. + +This cycle should define Echo's admission-side `BoundedSite`, freeze the rule +that hosts choose engine-defined deterministic policy by reference rather than +injecting bespoke law, and restate `super_tick()` as admission over ingress +claims at bounded sites under explicit policy. diff --git a/docs/method/backlog/up-next/KERNEL_braid-settlement-admission-unification.md b/docs/method/backlog/up-next/KERNEL_braid-settlement-admission-unification.md new file mode 100644 index 00000000..61f072d6 --- /dev/null +++ b/docs/method/backlog/up-next/KERNEL_braid-settlement-admission-unification.md @@ -0,0 +1,18 @@ + + + +# KERNEL - Braid and Settlement Admission Unification + +Strands now obey the one-`super_tick()` law in doctrine and runtime shape, but +braid and settlement still risk feeling like side corridors instead of further +instances of the same admission architecture. + +This cycle should push Echo toward one honest story: + +- braid comparison is a plural object over a common basis +- collapse or settlement is an admission act over that plural object +- lawful outcomes remain `Derived`, `Plural`, `Conflict`, or `Obstruction` +- publication artefacts stay distinct from witness cores + +The first cut should narrow the semantic gap, not solve the entire final braid +ontology. diff --git a/docs/method/backlog/up-next/KERNEL_determinism-escape-hatches.md b/docs/method/backlog/up-next/KERNEL_determinism-escape-hatches.md new file mode 100644 index 00000000..27e9480b --- /dev/null +++ b/docs/method/backlog/up-next/KERNEL_determinism-escape-hatches.md @@ -0,0 +1,100 @@ + + + +# KERNEL - Determinism escape hatches audit and closure + +Echo's browser/WASM boundary currently looks cleaner than the native rule +surface. A targeted audit found no obvious JS callback injection hole in the +public wasm ABI, but it did confirm one real determinism escape hatch inside +the kernel: host-authored native rewrite code still runs directly on the +deterministic path. + +This cycle should turn that audit into repo truth and close the most dangerous +gaps instead of leaving the findings trapped in chat. + +Current findings: + +- **No obvious wasm callback hole.** + `echo-wasm-abi::KernelPort` is byte-oriented and callback-free. The current + host boundary exposes explicit requests such as `dispatch_intent(...)`, + `observe(...)`, `observe_neighborhood_site(...)`, and + `observe_neighborhood_core(...)` rather than host-supplied executable hooks. + `warp-wasm` routes through an installed `KernelPort` object and does not + currently expose `js_sys::Function`, `Closure<...>`, timer callbacks, or + host-injected executable law. + +- **The real hole is the native rule API.** + `warp-core::rule::RewriteRule` still carries executable `MatchFn` and + `ExecuteFn` function pointers. The engine invokes those pointers directly in + both serial and parallel execution paths. They are plain `fn` pointers rather + than captured closures, which is better than arbitrary callback objects, but + they still allow host-authored Rust code to touch ambient state unless the + project treats this API as trusted/bootstrap-only. + +- **Worker count is an ambient knob, but not yet a proven semantic break.** + `warp-core::engine_impl` reads `ECHO_WORKERS` and falls back to + `available_parallelism()`. The audit did not find evidence that different + worker counts change committed state on their own: rewrites are grouped in + canonical order, parallel deltas are merged canonically, and the parallel + executor already tests all policies against a serial oracle. Still, ambient + worker selection widens the blast radius if rule code itself is impure. + +- **Unordered containers exist, but the critical ordering surfaces inspected in + this audit are already being canonicalised.** + `HashMap`, `HashSet`, and `FxHashMap` still appear in engine and scheduler + internals, but the audited hot paths sort, dedupe, or route through `BTree*` + structures before producing committed state. This is a residual risk area, not + a confirmed active break from this pass. + +- **Materialization catches one class of nondeterminism, but not all of it.** + The materialization bus is order-independent and rejects duplicate emissions, + explicitly calling out unordered-source iteration as a structural bug. That is + good. But it only catches some bad rule behavior. It cannot make an impure + rule deterministic if the rule computes different payloads or subkeys from + time, randomness, environment, or other ambient process state. + +Implications: + +- The wasm/browser host bridge is not currently the main determinism problem. +- The determinism boundary is still vulnerable anywhere Echo accepts + host-authored executable rewrite logic. +- The current policy doctrine in `admission.rs` is directionally right: hosts + should choose deterministic engine-defined policy by reference, not inject + executable admission law. +- The long-term closure target should be explicit: user-authored rewrite logic + should enter Echo only as Wesley-compiled declarative IR, not as arbitrary + native callbacks or handwritten executable rule code. + +What should happen next: + +1. Write down, in design/invariant form, that `RewriteRule` is a + trusted/bootstrap-only API until a declarative or sandboxed replacement + exists. +2. Add a DET-critical audit or release gate that rejects obvious ambient-state + access on rule paths: + - time + - randomness + - filesystem/network + - ambient environment + - unordered-container-driven emission without canonicalization +3. Freeze the intended replacement path: + - **primary target:** Wesley-compiled declarative IR as the only + user-authored rewrite source admitted to Echo + - **non-goal:** arbitrary host-authored native execution law on the + deterministic path + - **optional transitional tool:** a deterministic sandboxed rule language, + only if it still compiles down to the same lawful substrate and does not + reopen executable host-side escape hatches +4. Keep the current worker-count and parallel-policy tests, but treat them as + necessary supporting evidence rather than sufficient proof of safety. + +Related: + +- `crates/warp-core/src/rule.rs` +- `crates/warp-core/src/engine_impl.rs` +- `crates/warp-core/src/parallel/exec.rs` +- `crates/warp-core/src/admission.rs` +- `crates/echo-wasm-abi/src/kernel_port.rs` +- `crates/warp-wasm/src/lib.rs` +- `crates/warp-core/src/materialization/emission_port.rs` +- `crates/warp-core/src/materialization/bus.rs` diff --git a/docs/method/backlog/up-next/KERNEL_dynamic-footprint-binding-runtime.md b/docs/method/backlog/up-next/KERNEL_dynamic-footprint-binding-runtime.md new file mode 100644 index 00000000..2586cffb --- /dev/null +++ b/docs/method/backlog/up-next/KERNEL_dynamic-footprint-binding-runtime.md @@ -0,0 +1,51 @@ + + + +# Dynamic Footprint Binding Runtime + +- Lane: `up-next` +- Legend: `KERNEL` +- Rank: `1` + +## Why now + +Echo now has the first compile-checked proof that a Wesley-generated bounded +rewrite interface can prevent undeclared capability access. + +That proof still assumes a flat footprint. Real hot-graph rewrites need +dynamic binding: + +- direct slot binding from args +- relation-based slot binding +- closure derivation over runtime graph truth + +Without an explicit runtime model for those bindings, the stack risks either +freezing at toy footprints or reopening ad hoc traversal in handwritten Rust. + +## Hill + +Echo defines the runtime binding model for structured footprints so that: + +- Wesley owns static slot/closure grammar +- Echo owns concrete binding and closure resolution +- implementations still cannot escape the declared capability surface + +## Done looks like + +- one Echo design note states the static-schema / dynamic-binding split +- one runtime-facing backlog item names the binding steps: + - bind direct slots + - bind relation-derived slots + - resolve declared closures + - enforce cardinality/basis validity +- one motivating rewrite shape, such as `ReplaceRangeAsTick`, is described in + those terms +- the next runtime proof slice is obvious: bind one structured rewrite without + reopening arbitrary traversal + +## Repo Evidence + +- `docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md` +- `docs/design/0012-dynamic-footprint-binding-runtime.md` +- `docs/method/backlog/up-next/PLATFORM_footprint-honesty-rewrite-proof-slice.md` +- `crates/echo-wesley-gen/tests/rewrite_api_contract.rs` diff --git a/docs/method/backlog/up-next/KERNEL_strand-runtime-graph-ontology.md b/docs/method/backlog/up-next/KERNEL_strand-runtime-graph-ontology.md new file mode 100644 index 00000000..cd615ae2 --- /dev/null +++ b/docs/method/backlog/up-next/KERNEL_strand-runtime-graph-ontology.md @@ -0,0 +1,56 @@ + + + +# Strand Runtime Graph Ontology + +- Lane: `up-next` +- Legend: `KERNEL` +- Rank: `2` + +## Why now + +Echo already has the correct execution law for worldlines: + +- one mutable frontier per worldline +- immutable graph truth beneath the frontier +- content-addressed ingress +- deterministic scheduler admission +- one `super_tick()` path for state change + +What it still lacks is one explicit runtime graph ontology for strands. +Until that exists, strands keep drifting toward folklore: + +- manual ticking language survives in the strand contract +- support pins risk becoming accidental truth stores +- braid and settlement lack stable graph-native nouns to attach to + +## Hill + +Land one explicit graph-native control ontology for strands so Echo can +say, without hand-waving, what a strand is, where its current state +lives, what exact basis it was forked from, and which writer heads may +author work for it. + +## Done looks like + +- `docs/design/0009-strand-runtime-graph-ontology.md` exists and + defines the authoritative strand runtime graph schema. +- The packet freezes authoritative node types and edge types for + worldlines, strands, fork bases, current portals, and writer heads. +- The packet states the exact traversal used to obtain current strand + state from graph truth. +- Support pins and braid publication are explicitly marked as derived / + cache objects in the first cut. +- Follow-on runtime work can implement strand truth without inventing + a second execution model. + +## Repo Evidence + +- `docs/design/0004-strand-contract.md` +- `docs/design/0007-braid-geometry-and-neighborhood-publication.md` +- `docs/design/0008-strand-settlement.md` +- `docs/invariants/STRAND-CONTRACT.md` +- `crates/warp-core/src/strand.rs` +- `crates/warp-core/src/worldline_state.rs` +- `crates/warp-core/src/head.rs` +- `crates/warp-core/src/coordinator.rs` diff --git a/docs/method/backlog/up-next/PLATFORM_continuum-admission-family-cutover.md b/docs/method/backlog/up-next/PLATFORM_continuum-admission-family-cutover.md new file mode 100644 index 00000000..7c033e9c --- /dev/null +++ b/docs/method/backlog/up-next/PLATFORM_continuum-admission-family-cutover.md @@ -0,0 +1,36 @@ + + + +# PLATFORM - Continuum admission family cutover + +Echo now has enough doctrine to stop hand-waving about admission at the shared +boundary. The next platform cut is to choose a small admission-facing family +slice and move it toward the Continuum/Wesley proof path. + +This cycle should connect: + +- `BoundedSite` +- admission outcome family +- shell versus witness layering +- policy identity where it affects published causal meaning + +to the existing `PLATFORM_continuum-proof-family-runtime-cutover` work, without +trying to move the whole runtime across the boundary at once. + +The current runtime now has enough truthful mapping to drive that platform cut: + +- `TickReceiptDisposition::Applied` => `Derived` +- `TickReceiptDisposition::Rejected(FootprintConflict)` => `Obstruction` +- `NeighborhoodSite::Singleton` => `Derived` +- `NeighborhoodSite::Braided` => `Plural` +- `SettlementDecision::ImportCandidate` => `Derived` +- `SettlementDecision::ConflictArtifact` => `Conflict` + +The next step is not inventing more local nouns. It is selecting one generated +Continuum family slice that can carry this same outcome algebra and shell / +witness layering across the Echo boundary. + +The first concrete target for that cut is now: + +- Continuum packet: `docs/design/0022-neighborhood-core-and-admission-outcome-family/README.md` +- authored family: `schemas/continuum-neighborhood-core-family.graphql` diff --git a/docs/method/backlog/up-next/PLATFORM_footprint-honesty-rewrite-proof-slice.md b/docs/method/backlog/up-next/PLATFORM_footprint-honesty-rewrite-proof-slice.md new file mode 100644 index 00000000..ed2a8cf8 --- /dev/null +++ b/docs/method/backlog/up-next/PLATFORM_footprint-honesty-rewrite-proof-slice.md @@ -0,0 +1,45 @@ + + + +# Footprint Honesty Rewrite Proof Slice + +- Lane: `up-next` +- Legend: `PLATFORM` +- Rank: `1` + +## Why now + +Echo now closes the default public native rule seam and states that +user-authored rewrite logic must arrive as Wesley-compiled declarative IR. + +That doctrine still needs one compile-checked proof slice showing that the +generated Rust boundary can actually prevent dishonest footprint use before +runtime. + +## Hill + +Echo consumes one Wesley-generated Rust rewrite boundary whose shape makes +undeclared graph access impossible or a compile-time failure. + +The proof slice should stay narrow: + +- one mutation rewrite with one declared footprint +- one valid implementation +- one invalid implementation that fails to compile + +## Done looks like + +- Echo can compile one proof slice against a Wesley-generated bounded Rust + rewrite API +- the valid fixture compiles cleanly +- the invalid fixture fails because the generated surface does not expose an + undeclared capability +- runtime footprint guards remain in place as second-line defense rather than + the only honesty proof + +## Repo Evidence + +- `docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md` +- `crates/echo-wesley-gen` +- `docs/method/backlog/up-next/PLATFORM_continuum-proof-family-runtime-cutover.md` +- `scripts/tests/declarative_rule_authorship_invariant_test.sh` diff --git a/docs/method/backlog/up-next/PLATFORM_jedit-hot-text-runtime-host-surface.md b/docs/method/backlog/up-next/PLATFORM_jedit-hot-text-runtime-host-surface.md new file mode 100644 index 00000000..311fd78c --- /dev/null +++ b/docs/method/backlog/up-next/PLATFORM_jedit-hot-text-runtime-host-surface.md @@ -0,0 +1,97 @@ + + + +# jedit Optic Intent / Observation Handoff + +- Lane: `up-next` +- Legend: `PLATFORM` +- Rank: `1` + +## Why now + +`jedit` now has a real authored GraphQL contract for its hot-text boundary, +including: + +- mutation operations + - `createBufferWorldline` + - `replaceRangeAsTick` + - `createCheckpoint` +- a canonical read operation + - `worldlineSnapshot` + +Wesley can generate TypeScript and Zod operation registries from that +contract, and `jedit` now consumes those registries in its app-owned adapter. + +The next blocker is not in `jedit`. It is in the substrate handoff model. + +Echo's current public wasm/kernel boundary exposes: + +- generic intent ingest +- generic observation +- neighborhood publication +- settlement publication + +That is directionally correct, but the integration posture now needs to match +the optic model more explicitly: + +- the application submits intent through an optic-shaped boundary +- Echo admits or rejects that intent against generic substrate truth +- Echo returns the deterministic result / receipt envelope for that intent +- the application then observes the resulting worldline state and projects that + generic graph truth into app-specific nouns + +What is missing is not a `jedit`-named Echo API. What is missing is the first +clean handoff that connects: + +- Wesley-compiled app intents +- Wesley-compiled observer plans +- generic Echo intent ingest / deterministic result envelopes +- generic Echo observation and hosted observer lifecycle +- app-side projection over observed worldlines + +## Hill + +Define the first substrate handoff that lets a `jedit`-style application use +Echo through optics without: + +- putting app-specific rewrite names on Echo's generic public API +- reopening host-authored native rewrite code +- forcing `jedit` to fake causal state changes entirely in app-local runtime + +## Done looks like + +- one Echo-facing design or spec note states the optic handoff explicitly: + - app submits intent + - Echo returns the deterministic result / receipt envelope plus a + hologram/frontier handoff + - app reads through a generic observer plan or observer handle +- the note explains where app-specific operation names live: + - authored in the app contract + - compiled by Wesley + - encoded into generic substrate intents + - never promoted to handwritten Echo public methods +- the note also explains where app-authored observer behavior lives: + - authored as app observer spec + - compiled by Wesley into generic observer plans + - hosted by Echo without handwritten app callbacks +- one concrete seam is named for the first `jedit` hot-text operations: + - create buffer worldline + - replace range as tick + - create checkpoint + - read canonical worldline snapshot +- repo truth makes clear how those operations travel through: + - Wesley-generated intent / codec artifacts + - Wesley-generated observer plan / reading codec artifacts + - generic Echo intent ingest + - deterministic receipt / result envelopes + - generic observation and observer hosting + - app-side worldline projection + +## Repo evidence + +- `crates/echo-wasm-abi/src/kernel_port.rs` +- `crates/warp-wasm/src/lib.rs` +- `docs/design/0012-dynamic-footprint-binding-runtime.md` +- `docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md` +- external Continuum optics paper: `optics/warp-optic.pdf` +- `docs/design/0013-generic-observer-api-and-plan.md` diff --git a/docs/method/backlog/up-next/PLATFORM_neighborhood-publication-stack.md b/docs/method/backlog/up-next/PLATFORM_neighborhood-publication-stack.md new file mode 100644 index 00000000..646e450b --- /dev/null +++ b/docs/method/backlog/up-next/PLATFORM_neighborhood-publication-stack.md @@ -0,0 +1,26 @@ + + + +# PLATFORM - Neighborhood publication stack documentation + +Echo now has enough real neighborhood publication truth that the stack needs a +single durable explanation, not scattered comments and chat archaeology. + +This cycle should write down: + +- the difference between commit-time emissions and read-time publications +- the role of `BoundedSite`, `ObservationArtifact`, `NeighborhoodSite`, and + `NeighborhoodCore` +- the exact host/publication path through `echo-wasm-abi` and `warp-wasm` +- what the host should request when it wants raw observation, Echo-native local + site truth, or the shared Continuum family projection + +The goal is not new runtime law. The goal is to stop forcing maintainers and +host authors to reverse-engineer the publication stack from code. + +Related: + +- `docs/design/0005-echo-ttd-witness-surface.md` +- `docs/design/0007-braid-geometry-and-neighborhood-publication.md` +- `docs/design/0010-bounded-site-and-admission-policy.md` +- `docs/spec/SPEC-0009-wasm-abi-v3.md` diff --git a/docs/spec/SPEC-0009-wasm-abi-v3.md b/docs/spec/SPEC-0009-wasm-abi-v3.md new file mode 100644 index 00000000..fd43b07f --- /dev/null +++ b/docs/spec/SPEC-0009-wasm-abi-v3.md @@ -0,0 +1,399 @@ + + + +# SPEC-0009: WASM ABI Contract v3 + +> **Status:** Active | **ABI Version:** 3 | **Crate:** `warp-wasm` + +## Overview + +This document specifies the current WASM export surface, wire encoding, and +error protocol for Echo's deterministic simulation boundary. + +ABI v3 makes three boundaries explicit: + +- `observe(request)` is the canonical generic world-state read export; neighborhood-specific read exports expose bounded site/core views. +- `dispatch_intent(...)` is the only public write and control ingress surface. +- `scheduler_status()` is the read-only scheduler metadata export. + +Echo internals do not consume wall-clock time. All clocks in this ABI are +logical monotone integers: + +- `WorldlineTick` is per-worldline append identity. +- `GlobalTick` is runtime cycle correlation metadata. +- `RunId` is a control-plane generation token. + +On the wire, `WorldlineTick`, `GlobalTick`, and `RunId` are canonical-CBOR +unsigned integers using the smallest legal width for their value. `null` +represents `Option<...>::None`. + +`WorldlineTick(0)` is intentionally overloaded by coordinate type: + +- In historical selectors such as `ObservationAt::Tick { worldline_tick: 0 }`, + it names the first committed append. +- In frontier/head metadata such as `HeadInfo`, `HeadObservation`, and + `ResolvedObservationCoordinate`, `worldline_tick = 0` with + `commit_global_tick = null` means the worldline is still at `U0` and has not + committed anything yet. + +Scheduler lifecycle requests are carried as privileged control intents through +the same EINT intake path as domain intents. There is no public `step(...)`, +poll, or tick hook API in ABI v3. + +## Architecture + +```text +┌─────────────────────────────────────────────────┐ +│ JS / Host Adapter │ +│ (submits intents, decodes CBOR envelopes) │ +└───────────────────┬─────────────────────────────┘ + │ wasm-bindgen exports +┌───────────────────▼─────────────────────────────┐ +│ warp-wasm (boundary) │ +│ thread_local RefCell>>│ +│ Encodes Result → CBOR envelope │ +└───────────────────┬─────────────────────────────┘ + │ KernelPort trait +┌───────────────────▼─────────────────────────────┐ +│ WarpKernel (engine feature) │ +│ Wraps warp-core::Engine and ObservationService │ +└─────────────────────────────────────────────────┘ +``` + +## Exports + +All exports are `#[wasm_bindgen]` functions. Return types are CBOR-encoded +`Uint8Array` unless noted otherwise. + +| Export | Signature | Returns | +| ------------------------------------ | ---------------------- | ------------------------------ | +| `init()` | `() → Uint8Array` | `HeadInfo` envelope | +| `dispatch_intent(bytes)` | `(&[u8]) → Uint8Array` | `DispatchResponse` envelope | +| `observe(request)` | `(&[u8]) → Uint8Array` | `ObservationArtifact` envelope | +| `observe_neighborhood_site(request)` | `(&[u8]) → Uint8Array` | `NeighborhoodSite` envelope | +| `observe_neighborhood_core(request)` | `(&[u8]) → Uint8Array` | `NeighborhoodCore` envelope | +| `scheduler_status()` | `() → Uint8Array` | `SchedulerStatus` envelope | +| `get_registry_info()` | `() → Uint8Array` | `RegistryInfo` envelope | +| `get_codec_id()` | `() → JsValue` | `string \| null` | +| `get_registry_version()` | `() → JsValue` | `string \| null` | +| `get_schema_sha256_hex()` | `() → JsValue` | `string \| null` | + +Removed before or by ABI v3: + +- `drain_view_ops()` +- `get_head()` +- `execute_query(id, vars)` +- `snapshot_at(tick)` +- `render_snapshot(bytes)` +- `step(budget)` + +## Intent Intake + +All external writes enter Echo through EINT envelopes. + +- Domain intents use their domain-specific `op_id`. +- Privileged scheduler/control intents use reserved op id `u32::MAX` + (`CONTROL_INTENT_V1_OP_ID`). + +The EINT v1 byte layout is: + +```text +"EINT" (4 bytes) ++ op_id (u32 little-endian) ++ vars_len (u32 little-endian) ++ vars (exactly vars_len bytes) +``` + +For privileged control intents, `op_id` is always `0xffffffff` and `vars` are +canonical-CBOR bytes that decode as `ControlIntentV1`. + +Canonical payload shapes: + +- `Start { mode: UntilIdle { cycle_limit: Option } }` + + ```cbor + { + "kind": "start", + "mode": { + "kind": "until_idle", + "cycle_limit": + } + } + ``` + +- `Stop` + + ```cbor + { "kind": "stop" } + ``` + +- `SetHeadEligibility { head, eligibility }` + + ```cbor + { + "kind": "set_head_eligibility", + "head": { + "worldline_id": WorldlineId, + "head_id": HeadId + }, + "eligibility": "dormant" | "admitted" + } + ``` + +Notes: + +- `cycle_limit`, when present, must be non-zero. +- The current engine-backed implementation supports `UntilIdle` only. +- No wall-clock scheduler mode exists in ABI v3. + +Concrete `Start { mode: UntilIdle { cycle_limit: Some(1) } }` example: + +```text +ControlIntentV1 payload (canonical CBOR hex): +a2646b696e64657374617274646d6f6465a2646b696e646a756e74696c5f69646c656b6379636c655f6c696d697401 + +Packed EINT envelope (hex): +45494e54ffffffff2f000000a2646b696e64657374617274646d6f6465a2646b696e646a756e74696c5f69646c656b6379636c655f6c696d697401 +``` + +## Wire Envelope + +All `Uint8Array` returns use a CBOR envelope with an `ok` discriminator: + +### Success + +```cbor +{ "ok": true, ...response_fields } +``` + +### Error + +```cbor +{ "ok": false, "code": , "message": } +``` + +JS callers check `ok` before decoding the rest. The CBOR encoding follows the +canonical rules in `docs/js-cbor-mapping.md`. + +### Typed Field Encoding + +The scheduler-facing enums use serde's declared shapes directly: + +- `SchedulerState`, `WorkState`, `RunCompletion`, `HeadEligibility`, and + `HeadDisposition` serialize as snake_case text strings. +- `SchedulerMode::UntilIdle { cycle_limit }` serializes as + `{ "kind": "until_idle", "cycle_limit": }`. +- `WorldlineId` and `HeadId` are typed opaque wrappers that serialize as + `bytes(32)`. Array-of-32-integers encodings are invalid for these fields. + +Concrete `scheduler_status()` example: + +```cbor +{ + "state": "inactive", + "active_mode": null, + "work_state": "quiescent", + "run_id": 7, + "latest_cycle_global_tick": 9, + "latest_commit_global_tick": 8, + "last_quiescent_global_tick": 9, + "last_run_completion": "quiesced" +} +``` + +Canonical CBOR hex for that payload: + +```text +a865737461746568696e6163746976656672756e5f6964076a776f726b5f737461746569717569657363656e746b6163746976655f6d6f6465f6736c6173745f72756e5f636f6d706c6574696f6e68717569657363656478186c61746573745f6379636c655f676c6f62616c5f7469636b0978196c61746573745f636f6d6d69745f676c6f62616c5f7469636b08781a6c6173745f717569657363656e745f676c6f62616c5f7469636b09 +``` + +## Response Types + +### ObservationRequest + +The request payload for `observe(request)` is canonical-CBOR bytes that decode +to: + +- `coordinate.worldline_id: WorldlineId` encoded as `bytes(32)` +- `coordinate.at: frontier | tick { worldline_tick }` +- `frame: commit_boundary | recorded_truth | query_view` +- `projection: head | snapshot | truth_channels | query` + +### ObservationArtifact + +| Field | Type | Description | +| --------------- | ------------------------------- | ---------------------------------------------- | +| `resolved` | `ResolvedObservationCoordinate` | Explicit resolved coordinate metadata | +| `frame` | enum | Declared semantic frame | +| `projection` | enum | Declared projection | +| `artifact_hash` | bytes(32) | Canonical observation artifact hash | +| `payload` | tagged union | Head, snapshot, recorded truth, or query bytes | + +`artifact_hash` is computed as +`blake3("echo:observation-artifact:v2\0" || canonical_cbor(hash_input))`. + +### ResolvedObservationCoordinate + +| Field | Type | Description | +| ---------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `observation_version` | u32 | Observation contract version | +| `worldline_id` | `WorldlineId` | Worldline actually observed; serialized as `bytes(32)` | +| `requested_at` | enum | Original coordinate selector | +| `resolved_worldline_tick` | `WorldlineTick` | Resolved coordinate; historical reads use zero-based committed append indices, while `0` plus `commit_global_tick = null` represents empty `U0` frontier metadata | +| `commit_global_tick` | `GlobalTick?` | Commit cycle stamp for the resolved commit; `null` means the resolved coordinate is empty `U0` rather than a committed append | +| `observed_after_global_tick` | `GlobalTick?` | Observation freshness watermark | +| `state_root` | bytes(32) | Canonical full-state root hash; empty `U0` observations still carry the deterministic `U0` materialization root | +| `commit_hash` | bytes(32) | Canonical frontier/commit hash at the resolved point; empty `U0` observations still carry the deterministic `U0` frontier snapshot hash | + +### ObservationProjection + +| Variant | Shape | Description | +| ---------------- | ----------------------------------------------------------- | -------------------------------------------------------- | +| `head` | `{ "kind": "head" }` | Head metadata projection | +| `snapshot` | `{ "kind": "snapshot" }` | Snapshot metadata projection | +| `truth_channels` | `{ "kind": "truth_channels", "channels": bytes(32)[]? }` | Recorded truth channel filter; `null` means all channels | +| `query` | `{ "kind": "query", "query_id": u32, "vars_bytes": bytes }` | Query projection placeholder | + +### ObservationPayload + +| Variant | Shape | Description | +| ---------------- | --------------------------------------------------------- | ------------------------------- | +| `head` | `{ "kind": "head", "head": HeadObservation }` | Head metadata payload | +| `snapshot` | `{ "kind": "snapshot", "snapshot": SnapshotObservation }` | Snapshot metadata payload | +| `truth_channels` | `{ "kind": "truth_channels", "channels": ChannelData[] }` | Recorded truth channel payloads | +| `query_bytes` | `{ "kind": "query_bytes", "data": bytes }` | Query result bytes | + +### HeadObservation + +| Field | Type | Description | +| -------------------- | --------------- | --------------------------------------------------------------------------------------------------------------- | +| `worldline_tick` | `WorldlineTick` | Frontier coordinate; `0` plus `commit_global_tick = null` means the observed frontier is empty `U0` | +| `commit_global_tick` | `GlobalTick?` | Commit cycle stamp for the observed frontier; `null` means no committed append yet | +| `state_root` | bytes(32) | Canonical full-state root hash; empty `U0` observations still carry the deterministic `U0` materialization root | +| `commit_id` | bytes(32) | Canonical frontier hash; empty `U0` observations still carry the deterministic `U0` frontier snapshot hash | + +### SnapshotObservation + +| Field | Type | Description | +| -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `worldline_tick` | `WorldlineTick` | Snapshot coordinate; historical reads use append indices, while frontier snapshot reads may return `0` + `null` for empty `U0` | +| `commit_global_tick` | `GlobalTick?` | Commit cycle stamp; `null` is reserved for an empty-frontier `U0` snapshot resolved from `ObservationAt::Frontier` | +| `state_root` | bytes(32) | Canonical full-state root hash; empty `U0` snapshots still carry the deterministic `U0` materialization root | +| `commit_id` | bytes(32) | Canonical snapshot hash; empty `U0` snapshots still carry the deterministic `U0` frontier snapshot hash | + +### HeadInfo + +Returned by `init()`. + +| Field | Type | Description | +| -------------------- | --------------- | ----------------------------------------------------------------------------------------------------------- | +| `worldline_tick` | `WorldlineTick` | Current committed frontier position; `0` plus `commit_global_tick = null` means empty `U0` | +| `commit_global_tick` | `GlobalTick?` | Cycle stamp for the current commit; `null` means no commits yet | +| `state_root` | bytes(32) | Canonical full-state BLAKE3 root hash; empty `U0` still carries the deterministic `U0` materialization root | +| `commit_id` | bytes(32) | Canonical frontier hash; empty `U0` still carries the deterministic `U0` frontier snapshot hash | + +### DispatchResponse + +| Field | Type | Description | +| ------------------ | ----------------- | ---------------------------------------------- | +| `accepted` | bool | `true` if newly accepted, `false` if duplicate | +| `intent_id` | bytes(32) | Content-addressed intent hash | +| `scheduler_status` | `SchedulerStatus` | Scheduler metadata after ingest/apply | + +### SchedulerStatus + +| Field | Type | Description | +| ---------------------------- | ---------------- | ---------------------------------------------------- | +| `state` | `SchedulerState` | Scheduler lifecycle state | +| `active_mode` | `SchedulerMode?` | Active mode while a run is configured | +| `work_state` | `WorkState` | Whether runnable work exists at the current boundary | +| `run_id` | `RunId?` | Current or latest run generation token | +| `latest_cycle_global_tick` | `GlobalTick?` | Latest completed runtime cycle | +| `latest_commit_global_tick` | `GlobalTick?` | Latest cycle that produced a commit | +| `last_quiescent_global_tick` | `GlobalTick?` | Most recent transition into quiescence | +| `last_run_completion` | `RunCompletion?` | Why the most recent run ended | + +Current engine-backed behavior: + +- `init()` leaves the runtime inert. +- `Start { mode: UntilIdle { ... } }` runs synchronously inside the control + intent handler and returns after the run completes. +- `Stop` is a no-op when the scheduler is already inactive; it does not rewrite + `last_run_completion` for a finished run. +- Hosts normally observe `state = inactive` plus `last_run_completion`, not a + long-lived running scheduler loop. + +### ChannelData + +| Field | Type | Description | +| ------------ | --------- | ---------------------------------- | +| `channel_id` | bytes(32) | Materialization channel identifier | +| `data` | bytes | Raw finalized channel output | + +### RegistryInfo + +| Field | Type | Description | +| ------------------- | ------- | --------------------------------------------- | +| `codec_id` | string? | Codec identifier (e.g. `"cbor-canonical-v1"`) | +| `registry_version` | string? | Registry version | +| `schema_sha256_hex` | string? | Schema hash (hex) | +| `abi_version` | u32 | ABI contract version (currently `3`) | + +## Error Codes + +| Code | Name | Meaning | +| ---- | ------------------------------ | ---------------------------------------------------------- | +| 1 | `NOT_INITIALIZED` | `init()` not called | +| 2 | `INVALID_INTENT` | Malformed EINT intent envelope | +| 3 | `ENGINE_ERROR` | Internal engine failure | +| 4 | `LEGACY_INVALID_TICK` | Reserved for the removed v1 snapshot adapter | +| 5 | `NOT_SUPPORTED` | Operation not implemented | +| 6 | `CODEC_ERROR` | CBOR encode/decode failure | +| 7 | `INVALID_PAYLOAD` | Corrupted input bytes | +| 8 | `INVALID_WORLDLINE` | Requested worldline missing | +| 9 | `INVALID_TICK` | Requested observation tick missing | +| 10 | `UNSUPPORTED_FRAME_PROJECTION` | Invalid frame/projection pair | +| 11 | `UNSUPPORTED_QUERY` | Query observation not yet implemented | +| 12 | `OBSERVATION_UNAVAILABLE` | Valid request but no observation exists at that coordinate | +| 13 | `INVALID_CONTROL` | Malformed or invalid control intent | + +## Rust Boundary + +`KernelPort` is the Rust-side ABI contract for `warp-wasm`. + +- `dispatch_intent(...)` +- `observe(...)` +- `scheduler_status()` +- `registry_info()` + +The trait does not expose the removed v1 read adapters or a public step/pump +surface. Implementors that need head or snapshot data must derive them from +their own observation-backed internals rather than adding parallel public read +methods. + +## Migration Notes for Host Adapters + +### From ABI v2 to ABI v3 + +1. Stop calling `step(...)`; the export is absent in ABI v3. +2. Continue treating `observe(request)` as the canonical generic world-state + read boundary, with neighborhood-specific read exports reserved for bounded + site/core views. +3. Route scheduler lifecycle and admission requests through + `dispatch_intent(...)` using `ControlIntentV1` packed into an EINT envelope. +4. Read `RegistryInfo.abi_version` and reject hosts that still expect the v2 + step surface. +5. Rename host-side field access from bare `tick`-style fields to explicit + `worldline_tick`, `commit_global_tick`, and + `observed_after_global_tick` fields. +6. Treat all ABI clocks as logical coordinates only. They are not wall-clock + durations, timer inputs, or global ordering cursors. +7. Expect query-shaped observations to continue returning + `UNSUPPORTED_QUERY` until a real observation-backed query implementation + lands. + +## Compatibility Note + +ABI v3 is intentionally breaking. The removed step/pump surface is absent, not +deprecated, and hosts must migrate to explicit observation requests plus +intent-shaped scheduler control. diff --git a/scripts/ban-nondeterminism.sh b/scripts/ban-nondeterminism.sh index 07b9283b..a39521b4 100755 --- a/scripts/ban-nondeterminism.sh +++ b/scripts/ban-nondeterminism.sh @@ -66,6 +66,14 @@ PATTERNS=( '\bgetrandom::\b' '\bfastrand::\b' + # Host-supplied callback / network escape hatches + '\bjs_sys::Function\b' + '\bwasm_bindgen::closure::Closure\b' + '\bClosure<' + '\bstd::net::\b' + '\breqwest::\b' + '\bureq::\b' + # Unordered containers that will betray you if they cross a boundary '\bstd::collections::HashMap\b' '\bstd::collections::HashSet\b' diff --git a/scripts/tests/declarative_rule_authorship_invariant_test.sh b/scripts/tests/declarative_rule_authorship_invariant_test.sh new file mode 100644 index 00000000..91a70ce6 --- /dev/null +++ b/scripts/tests/declarative_rule_authorship_invariant_test.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +# +# Tests for cycle 0012: DECLARATIVE-RULE-AUTHORSHIP invariant document. + +set -euo pipefail + +script_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_root}/../.." && pwd)" + +passed=0 +failed=0 + +assert() { + local label="$1" + shift + if (set +e; "$@" >/dev/null 2>&1); then + echo " PASS: ${label}" + ((passed++)) || true + else + echo " FAIL: ${label}" + ((failed++)) || true + fi +} + +invariant="${repo_root}/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md" +release_policy="${repo_root}/docs/determinism/RELEASE_POLICY.md" +warp_core_lib="${repo_root}/crates/warp-core/src/lib.rs" + +echo "=== DECLARATIVE-RULE-AUTHORSHIP invariant tests ===" +echo "" + +echo "1. Invariant document exists" +assert "docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md exists" \ + test -f "${invariant}" + +echo "" +echo "2. Normative language" +assert "contains MUST" \ + grep -q "MUST" "${invariant}" +assert "contains Wesley-compiled declarative IR" \ + grep -qi "Wesley-compiled declarative IR" "${invariant}" +assert "contains bootstrap-only wording" \ + grep -qi "bootstrap-only" "${invariant}" +assert "contains callback-free wording" \ + grep -qi "callback-free" "${invariant}" + +echo "" +echo "3. Release policy cross-reference" +assert "RELEASE_POLICY references DECLARATIVE-RULE-AUTHORSHIP" \ + grep -q "DECLARATIVE-RULE-AUTHORSHIP" "${release_policy}" + +echo "" +echo "4. Default public API does not export native rule authoring" +assert "default lib.rs does not unconditionally pub use RewriteRule" \ + awk ' + /^#\[cfg\(feature = "native_rule_bootstrap"\)\]$/ { gated = 1; next } + gated && (/^[[:space:]]*$/ || /^[[:space:]]*\/\//) { next } + /^pub use rule::\{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule\};$/ { + if (!gated) exit 1 + } + { gated = 0 } + END { exit 0 } + ' "${warp_core_lib}" + +tmp_gated_export="$(mktemp)" +cat >"${tmp_gated_export}" <<'EOF' +#[cfg(feature = "native_rule_bootstrap")] + +// bootstrap export stays gated even when separated by a comment +pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; +EOF +assert "native bootstrap cfg gate survives blank/comment separation" \ + awk ' + /^#\[cfg\(feature = "native_rule_bootstrap"\)\]$/ { gated = 1; next } + gated && (/^[[:space:]]*$/ || /^[[:space:]]*\/\//) { next } + /^pub use rule::\{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule\};$/ { + if (!gated) exit 1 + } + { gated = 0 } + END { exit 0 } + ' "${tmp_gated_export}" +rm -f "${tmp_gated_export}" + +echo "" +echo "=== Results: ${passed} passed, ${failed} failed ===" + +if [ "${failed}" -gt 0 ]; then + exit 1 +fi